diff --git a/docs/plugin_dev/advanced_concepts/DeckController.md b/docs/plugin_dev/advanced_concepts/DeckController.md
index 08b008f..a6f1b21 100644
--- a/docs/plugin_dev/advanced_concepts/DeckController.md
+++ b/docs/plugin_dev/advanced_concepts/DeckController.md
@@ -1,4 +1,4 @@
The [DeckController](DeckController.md) is a high-level object that allows you to control [Stream Decks](https://www.elgato.com/uk/en/s/welcome-to-stream-deck). It builds on top of the [Stream Deck](https://github.com/abcminiuser/python-elgato-streamdeck/blob/master/src/Stream Deck/Devices/Stream Deck.py) class of the [python-elgato-streamdeck](https://github.com/abcminiuser/python-elgato-streamdeck) library.
It manages the backgrounds, videos, labels and gives plugins and StreamController itself a nice interface.
-Under normal conditions you should not need to use this object directly because all important methods are make available through the [ActionBase](../bases/ActionBase_py.md). However, if you want to write an action with more deeply integration like for example changing the background, you will need to call methods on [DeckController](DeckController.md) directly.
\ No newline at end of file
+Under normal conditions you should not need to use this object directly because all important methods are made available through [ActionCore](../bases/ActionCore_py.md). However, if you want to write an action with more deeply integration like for example changing the background, you will need to call methods on [DeckController](DeckController.md) directly.
diff --git a/docs/plugin_dev/advanced_concepts/PageCaching.md b/docs/plugin_dev/advanced_concepts/PageCaching.md
index e013241..a6955c0 100644
--- a/docs/plugin_dev/advanced_concepts/PageCaching.md
+++ b/docs/plugin_dev/advanced_concepts/PageCaching.md
@@ -9,4 +9,4 @@ However, it's important to be aware of its existence to circumvent potential iss
Despite this, it's a common pitfall. Why? Because when a user navigates away from a page and then returns, your action's state might still be in the cache. Consequently, your plugin may falsely assume the correct image is displayed when, in fact, it is not.
: **Solution:**
- : To address this, simply reset the `current_state` variable within the [`on_ready`](../bases/ActionBase_py.md#on_ready) method. The `on_ready` method is invoked each time a page is loaded, allowing you to reset the `current_state` variable.
\ No newline at end of file
+ : To address this, simply reset the `current_state` variable within the [`on_ready`](../bases/ActionCore_py.md#on_ready) method. The `on_ready` method is invoked each time a page is loaded, allowing you to reset the `current_state` variable.
diff --git a/docs/plugin_dev/advanced_concepts/Signals.md b/docs/plugin_dev/advanced_concepts/Signals.md
index 45722c0..5607412 100644
--- a/docs/plugin_dev/advanced_concepts/Signals.md
+++ b/docs/plugin_dev/advanced_concepts/Signals.md
@@ -1,4 +1,4 @@
-Signals are called when special actions are performed in the ui, allowing the plugin to respond to these actions. For example, should your plugin have some kind of page selection in the [config area](../bases/ActionBase_py.md#get_config_rows) your plugin needs to get informed if a page gets renamed. That's exactly what signals are for.
+Signals are called when special actions are performed in the ui, allowing the plugin to respond to these actions. For example, should your plugin have some kind of page selection in the [config area](../bases/ActionCore_py.md#get_config_rows) your plugin needs to get informed if a page gets renamed. That's exactly what signals are for.
## Available signals
@@ -61,7 +61,7 @@ Signals are called when special actions are performed in the ui, allowing the pl
from src.Signals import Signals
```
2. Connect to the signal
-This is done by the [`connect`](../bases/ActionBase_py.md#connect) method of the [`ActionBase`](../bases/ActionBase_py.md):
+This is done by the [`connect`](../bases/ActionCore_py.md#connect) method of [ActionCore](../bases/ActionCore_py.md):
```python
self.connect(signal=Signals.PageRename, callback=self.on_page_rename)
```
@@ -69,4 +69,4 @@ self.connect(signal=Signals.PageRename, callback=self.on_page_rename)
Now every time a page gets renamed the `on_page_rename` method will be called.
## Not enough?
-Should you need a signal that is currently not availble feel free to open a [issue](https://github.com/Core447/StreamController/issues) or work on an own [pull request](https://github.com/Core447/StreamController/pulls).
\ No newline at end of file
+Should you need a signal that is currently not availble feel free to open a [issue](https://github.com/StreamController/StreamController/issues) or work on an own [pull request](https://github.com/StreamController/StreamController/pulls).
diff --git a/docs/plugin_dev/bases/ActionBase_py.md b/docs/plugin_dev/bases/ActionBase_py.md
deleted file mode 100644
index cb325ed..0000000
--- a/docs/plugin_dev/bases/ActionBase_py.md
+++ /dev/null
@@ -1,297 +0,0 @@
-The [ActionBase](ActionBase_py.md) is the base for all actions in [StreamController](https://github.com/Core447/StreamController). Therefore all your actions must extend this class.
-[ActionBase](ActionBase_py.md) gives you easy access to the key(s) controlled by your actions and providing easy wrappers to change images, set labels and getting events.
-
-If you want to learn more by going throught the code click [here](https://github.com/Core447/StreamController/blob/main/src/backend/PluginManager/ActionBase.py).
-
-## Available methods
-### `set_deck_controller`
-: **Arguments**:
-
- |Argument|Default|Description|
- |---|---|---|
- |deck_controller|None|The deck controller of the action.|
-
- **Description**:
-
- !!! warning
- This is an internal method, do not call it manually unless you know what you are doing.
- This method gets called on the initialization of the action and sets the internal variable `deck_controller`.
-
-### `set_page`
-: **Arguments**:
-
- |Argument|Default|Description|
- |---|---|---|
- |page|None|The page of the action.|
-
- **Description**:
-
- !!! warning
- This is an internal method, do not call it manually unless you know what you are doing.
- This method gets called on the initialization of the action and sets the internal variable `page`.
-
-### `set_coords`
-: **Arguments**:
-
- |Argument|Default|Description|
- |---|---|---|
- |coords|None|The coords of the action|
-
- **Description**:
-
- !!! warning
- This is an internal method, do not call it manually unless you know what you are doing.
- This method gets called on the initialization of the action and sets the internal variable `coords`.
-
-### `on_key_down`
-: This method gets called when the action key is pressed. You can override this method in your action and add your own code.
-
- !!! info
- To ensure a lag-free experience for the user, all actions on the pressed keys are executed in a dedicated thread. This means you can add time consuming code here without affecting the application. However, any actions on the button after that will be delayed to ensure that the actions are always called in the same order.
-
-### `on_key_up`
-: This method gets called when the action key is released. You can override this method in your action and add your own code.
-
- !!! info
- To ensure a lag-free experience for the user, all actions on the released keys are executed in a dedicated thread. This means you can add time consuming code here without affecting the application. However, any actions on the button after that will be delayed to ensure that the actions are always called in the same order.
-
-### `on_tick`
-: This method gets called **every second** to allow live updates to the key. You can override this method in your action and add your own code.
- !!! info
- unlike [`on_key_down`](#on_key_down) and [`on_key_up`](#on_key_up) all actions on the same deck will be executed in the same thread. This means you are **not** supposed to add time consuming code here.
- !!! warning
- The next tick loop will start one second after the last one finished. This means should there be some actions that take a bit longer to finish their ticks, the delays will grow. Therefore [`on_tick`](#on_tick) should neither be used for time consuming code nor for precize timing.
-
-
-### `on_ready`
-: This method gets called after the app is fully loaded and the decks are ready to process all types of requests.
- !!! info
- The constructor of all actions gets called before the actual decks are ready to process any requests for image changes. For that reason you should use [`on_ready`](#on_ready) for the intial image change instead of relying on the constructor.
-
-### `set_default_image`
-: **Arguments**:
-
- |Argument|Default|Description|Type|
- |---|---|---|---|
- |image|None|The image to use|[PIL.Image](https://pillow.readthedocs.io/en/stable/reference/Image.html)|
-
- **Description**:
- This sets the **default** image of the key. If the user or any other action tries to change the image their image will be used instead.
-
- !!! warning
- This is not implemented yet. Changes made through this method will be ignored.
-
-### `set_default_label`
-: **Arguments**:
-
- |Argument|Default|Description|Type|
- |---|---|---|---|
- |text|None|The text to display|str|
- |position|bottom|The position of the text relative to the key|str|
- |color|[255, 255, 255]|The color of the text|list[int]|
- |stroke_width|0|The stroke width of the text|int|
- |font_family|""|The font family of the text|str|
- |font_size|18|The font size of the text|int|
-
- **Description**:
- This sets the **default** label of the key. If the user or any other action tries to change the label their label will be used instead.
-
- !!! warning
- This is not implemented yet. Changes made through this method will be ignored.
-
-### `set_media`
-: **Arguments**:
-
- |Argument|Default|Description|Type|
- |---|---|---|---|
- |image|None|The image to use|[PIL.Image.Image](https://pillow.readthedocs.io/en/stable/reference/Image.html)|
- |media_path|None|The path to a media file (can be a video, image or gif)|str|
- |size|1|The size of the image|float|
- |valign|0|The vertical alignment of the image (range -1 to 1)|float|
- |halign|0|The horizontal alignment of the image (range -1 to 1)|float|
- |loop|True|Whether to loop the video|bool|
- |fps|30|The frames per second of the video|int|
- |update|True|Whether to update the key|bool|
-
- **Description**:
- This is the method you can use to change the content of the key.
- As you can see you can show images as well as videos in all major formats.
-
-### `set_background_color`:
-: **Arguments**:
-
- |Argument|Default|Description|Type|
- |---|---|---|---|
- |color|[255, 255, 255, 255]|The color of the background|list[int]|
- |update|True|Whether to update the key|bool|
-
-### `show_error`
-: **Arguments**:
-
- |Argument|Default|Description|Type|
- |---|---|---|---|
- |duration|-1|The duration of the error in seconds. -1 means infinite|float|
-
-
-### `set_label`
-: **Arguments**:
-
- |Argument|Default|Description|Type|
- |---|---|---|---|
- |text|None|The text to display|str|
- |position|bottom|One of the three available positions: `top`, `center` or `bottom`|str|
- |color|[255, 255, 255]|The color of the text|list[int]|
- |stroke_width|0|The stroke width of the text|int|
- |font_family|""|The font family of the text|str|
- |font_size|18|The font size of the text|int|
- |update|True|Whether to update the key|bool|
-
- **Description**:
- This method allows you write text in one of the three available positions: `top`, `center` or `bottom` onto the key.
-
-### `set_top_label`
-: **Arguments**:
-
- |Argument|Default|Description|Type|
- |---|---|---|---|
- |text|None|The text to display|str|
- |color|[255, 255, 255]|The color of the text|list[int]|
- |stroke_width|0|The stroke width of the text|int|
- |font_family|""|The font family of the text|str|
- |font_size|18|The font size of the text|int|
- |update|True|Whether to update the key|bool|
-
- **Description**:
- This method has the same outcome as [`set_label`](#set_label) with `position = "top"`.
-
-### `set_center_label`
-: **Arguments**:
-
- |Argument|Default|Description|Type|
- |---|---|---|---|
- |text|None|The text to display|str|
- |color|[255, 255, 255]|The color of the text|list[int]|
- |stroke_width|0|The stroke width of the text|int|
- |font_family|""|The font family of the text|str|
- |font_size|18|The font size of the text|int|
- |update|True|Whether to update the key|bool|
-
- **Description**:
- This method has the same outcome as [`set_label`](#set_label) with `position = "center"`.
-
-### `set_bottom_label`
-: **Arguments**:
-
- |Argument|Default|Description|Type|
- |---|---|---|---|
- |text|None|The text to display|str|
- |color|[255, 255, 255]|The color of the text|list[int]|
- |stroke_width|0|The stroke width of the text|int|
- |font_family|""|The font family of the text|str|
- |font_size|18|The font size of the text|int|
- |update|True|Whether to update the key|bool|
-
- **Description**:
- This method has the same outcome as [`set_label`](#set_label) with `position = "bottom"`.
-
-### `get_config_rows`
-: **Description**:
- This method can be overritten by your action to show configuration rows in the ui.
-
- {width="300" align=left loading=lazy}
- Example from the [OS Plugin](https://github.com/Core447/OSPlugin)
-
-
- **Returns**:
- A list of [Adw.PreferencesRow](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.PreferencesRow.html) objects.
-
- !!! info
- If you need a brief intro into [GTK4](https://www.gtk.org/) in python you can check out [this tutorial](https://github.com/Taiko2k/GTK4PythonTutorial).
- For more involved information you can also check out the [GTK4 documentation](https://docs.gtk.org/gtk4/).
-
-### `get_custom_config_area`
-: **Description**:
- This method can be overritten by your action to show a custom area in the ui. By allowing all [Gtk.Widgets](https://docs.gtk.org/gtk4/class.Widget.html) you are able to customize the config area completely to your needs.
-
- **Returns**:
- Any [Gtk.Widget](https://docs.gtk.org/gtk4/class.Widget.html)
-
- !!! info
- If you need a brief intro into [GTK4](https://www.gtk.org/) in python you can check out [this tutorial](https://github.com/Taiko2k/GTK4PythonTutorial).
- For more involved information you can also check out the [GTK4 documentation](https://docs.gtk.org/gtk4/).
-
-### `set_settings`
-: **Arguments**:
-
- |Argument|Description|Type|
- |---|---|---|
- |settings|A dictionary with your settings|dict|
-
- **Description**:
- This method allows you to store settings for your actions. The typical usage is to store the user settings made in the [`custom config area`](#get_custom_config_area). You then use [`get_settings`](#get_settings) to retrieve them.
-
- The dict gets directly written into the page json and will be kept if the page gets exported or duplicated. This looks like this:
- ```json hl_lines="4-8"
- "actions": [
- {
- "name": "dev_core447_MediaPlugin::Info",
- "settings": {
- "show_thumbnail": true,
- "show_label": true,
- "seperator_text": ""
- }
- }
- ]
- ```
-
-
-### `get_settings`
-: **Description**:
- This method returns a dictionary with all your set settings for this action.
- For more see [`set_settings`](#set_settings).
-
- **Returns**:
- A dictionary with your settings
-
-
-### `connect`
-: **Arguments**:
-
- |Argument|Default|Description|Type|
- |---|---|---|---|
- |signal|None|The signal to connect to|[Signal](../advanced_concepts/Signals.md)|
- |callback|None|A callback method|callable|
-
- **Description**:
- This method allows you to connect to [signals](../advanced_concepts/Signals.md) allowing you to adapt to important changes made through the ui. For example if you are working with page names you might want to connect to the [page rename signal](../advanced_concepts/Signals.md#pagerename) to get notified when that happens and change the internal references accordingly.
- [How to use signals](../advanced_concepts/Signals.md#how-to-use-signals)
-
-### `launch_backend`
-: **Description**:
-
- Launches a local backend. See [BackendBase](../bases/BackendBase_py.md).
- !!! warning
- The methods waits until the backend is registered.
-
-: **Arguments**:
-
- |Argument|Default|Description|Type|
- |---|---|---|---|
- |backend_path|None|The path of the backend to launch.|str|
- |venv_path|None|The path of the venv to use.|str|
- |open_in_terminal|False|Open the backend in a terminal window. Useful for debugging.|bool
-
-### `get_own_key`
-: **Description**:
- Returns `ControllerKey` object holding this action.
-
-### `get_is_multi_action`
-: **Description**:
- Returns `True` if this action is a multi action.
- If `True` all images operations should be disabled.
-
-## Available Constants
-### `HAS_CONFIGURATION`
-: **Description**:
- Can be set to `True` or `False` to make an Action open the Configuration Page after it got added to a button
-: **Default**: `False`
\ No newline at end of file
diff --git a/docs/plugin_dev/bases/ActionCore_py.md b/docs/plugin_dev/bases/ActionCore_py.md
new file mode 100644
index 0000000..424fa27
--- /dev/null
+++ b/docs/plugin_dev/bases/ActionCore_py.md
@@ -0,0 +1,412 @@
+The [ActionCore](ActionCore_py.md) is the base class for all actions in [StreamController](https://github.com/StreamController/StreamController). All your actions must extend this class.
+
+ActionCore provides access to the key/dial/touchscreen controlled by your action and offers methods to change images, set labels, handle events, and manage settings.
+
+If you want to learn more by going through the code, click [here](https://github.com/StreamController/StreamController/blob/main/src/backend/PluginManager/ActionCore.py).
+
+## Quick Start
+
+```python
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
+
+class MyAction(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Register event handlers
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
+
+ def on_ready(self):
+ self.set_media(media_path=self.get_asset_path("icon.png"))
+
+ def on_key_down(self):
+ print("Key pressed!")
+```
+
+See [Event System](EventSystem.md) for more details on registering event handlers.
+
+## Properties
+
+### `has_configuration`
+: **Type**: `bool`
+ **Default**: `False`
+
+ Set to `True` to enable the configuration UI for this action. When enabled, the action will show a settings panel when selected.
+
+### `allow_event_configuration`
+: **Type**: `bool`
+ **Default**: `True`
+
+ Set to `False` to prevent users from remapping events for this action in the UI.
+
+### `event_manager`
+: **Type**: `EventManager`
+
+ The event manager for this action. Use `add_event_assigner()` to register event handlers. See [Event System](EventSystem.md) for details.
+
+## Lifecycle Methods
+
+### `on_ready`
+: This method is called when the page is ready to process requests made by actions.
+
+ !!! info
+ Always set your default image in `on_ready()`, not in `__init__()`. The deck is not ready to process image changes during construction.
+
+ ```python
+ def on_ready(self):
+ icon_path = self.get_asset_path("icon.png")
+ self.set_media(media_path=icon_path)
+ ```
+
+### `on_update`
+: This method is called when the app wants the action to redraw itself (image, labels, etc.).
+
+ By default, this calls `on_ready()` for backwards compatibility. Override to customize refresh behavior.
+
+### `on_tick`
+: This method gets called **every second** to allow live updates to the key.
+
+ !!! warning
+ Unlike event callbacks, all actions on the same deck execute ticks in the same thread. Do **not** add time-consuming code here. The next tick loop starts one second after the last one finished, so delays will accumulate.
+
+ ```python
+ def on_tick(self):
+ # Update a clock display
+ self.set_center_label(time.strftime("%H:%M:%S"))
+ ```
+
+### `on_remove`
+: Called when the action is fully removed from the page. Use this to clean up resources.
+
+### `on_removed_from_cache`
+: Called when the action is removed from the page cache. Use this for cache-related cleanup.
+
+### `on_backend_ready`
+: Called when the RPyC backend connection is established. Override this to perform initialization that requires the backend.
+
+## Visual Methods
+
+### `set_media`
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |image|None|A PIL Image to display|[PIL.Image.Image](https://pillow.readthedocs.io/en/stable/reference/Image.html)|
+ |media_path|None|Path to an image, video, or gif file|str|
+ |size|None|Scale factor for the image (1.0 = full size)|float|
+ |valign|None|Vertical alignment (-1 to 1, 0 = center)|float|
+ |halign|None|Horizontal alignment (-1 to 1, 0 = center)|float|
+ |fps|30|Frames per second for video playback|int|
+ |loop|True|Whether to loop video playback|bool|
+ |update|True|Whether to immediately update the display|bool|
+
+ **Description**:
+ Sets the content of the key. Supports images, videos, and gifs.
+
+ ```python
+ # Using a file path
+ self.set_media(media_path=self.get_asset_path("icon.png"), size=0.75)
+
+ # Using a PIL Image
+ from PIL import Image
+ img = Image.new("RGB", (72, 72), color="red")
+ self.set_media(image=img)
+ ```
+
+### `set_background_color`
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |color|[0, 0, 0, 0]|RGBA color values (0-255)|list[int]|
+ |update|True|Whether to immediately update the display|bool|
+
+ ```python
+ self.set_background_color([255, 0, 0, 255]) # Red background
+ ```
+
+### `set_label`
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |text|None|The text to display|str|
+ |position|"bottom"|One of: `top`, `center`, `bottom`|str|
+ |color|None|RGB color values (0-255)|list[int]|
+ |font_family|None|Font family name|str|
+ |font_size|None|Font size in points|int|
+ |outline_width|None|Text outline width|int|
+ |outline_color|None|RGB color for outline|list[int]|
+ |font_weight|None|Font weight (e.g., 400, 700)|int|
+ |font_style|None|One of: `normal`, `italic`, `oblique`|str|
+ |update|True|Whether to immediately update the display|bool|
+
+ **Description**:
+ Writes text in one of three positions on the key.
+
+ ```python
+ self.set_label("Hello", position="center", color=[255, 255, 255])
+ ```
+
+### `set_top_label`
+: Convenience method equivalent to `set_label(..., position="top")`.
+
+### `set_center_label`
+: Convenience method equivalent to `set_label(..., position="center")`.
+
+### `set_bottom_label`
+: Convenience method equivalent to `set_label(..., position="bottom")`.
+
+### `show_error`
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |duration|-1|Duration in seconds (-1 = infinite)|int|
+
+ **Description**:
+ Shows an error indicator on the key. Useful for indicating backend failures or invalid states.
+
+ ```python
+ try:
+ result = self.backend.do_something()
+ except Exception as e:
+ self.show_error(duration=3)
+ ```
+
+### `hide_error`
+: Hides the error indicator shown by `show_error()`.
+
+### `show_overlay`
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |image|None|PIL Image to overlay|[PIL.Image.Image](https://pillow.readthedocs.io/en/stable/reference/Image.html)|
+ |duration|-1|Duration in seconds (-1 = infinite)|int|
+
+ **Description**:
+ Shows an overlay image on top of the current key content.
+
+### `hide_overlay`
+: Hides the overlay shown by `show_overlay()`.
+
+## Configuration Methods
+
+### `get_config_rows`
+: **Returns**: `list[Adw.PreferencesRow]`
+
+ Override this method to provide configuration rows in the UI.
+
+
+ {width="300" align=left loading=lazy}
+ Example from the [OS Plugin](https://github.com/StreamController/OSPlugin)
+
+
+ ```python
+ def get_config_rows(self) -> list:
+ spinner = Adw.SpinRow.new_with_range(1, 100, 1)
+ spinner.set_title("Increment by")
+ return [spinner]
+ ```
+
+ !!! tip
+ Consider using [GenerativeUI](GenerativeUI.md) widgets instead of manual GTK widgets. They automatically handle saving and loading settings.
+
+### `get_custom_config_area`
+: **Returns**: `Gtk.Widget` or `None`
+
+ Override this method to provide a fully custom configuration area. This allows any GTK widget, giving complete control over the config UI.
+
+ !!! info
+ For GTK tutorials, check out [GTK4 Python Tutorial](https://github.com/Taiko2k/GTK4PythonTutorial) or the [GTK4 documentation](https://docs.gtk.org/gtk4/).
+
+### `get_settings`
+: **Returns**: `dict`
+
+ Returns the settings dictionary for this action. Settings are stored in the page JSON and persist across restarts.
+
+ ```python
+ settings = self.get_settings()
+ value = settings.get("my_key", "default_value")
+ ```
+
+### `set_settings`
+: **Arguments**:
+
+ |Argument|Description|Type|
+ |---|---|---|
+ |settings|Dictionary of settings to store|dict|
+
+ **Description**:
+ Stores settings for this action. The dict is written directly into the page JSON.
+
+ ```python
+ settings = self.get_settings()
+ settings["my_key"] = "new_value"
+ self.set_settings(settings)
+ ```
+
+ The settings appear in the page JSON like this:
+ ```json
+ "actions": [
+ {
+ "name": "com_example_MyPlugin::MyAction",
+ "settings": {
+ "my_key": "new_value"
+ }
+ }
+ ]
+ ```
+
+## Event Methods
+
+### `add_event_assigner`
+: **Arguments**:
+
+ |Argument|Description|Type|
+ |---|---|---|
+ |event_assigner|The event assigner to register|[EventAssigner](EventSystem.md)|
+
+ **Description**:
+ Registers an event handler for this action. See [Event System](EventSystem.md) for details.
+
+ ```python
+ from src.backend.PluginManager.EventAssigner import EventAssigner
+ from src.backend.DeckManagement.InputIdentifier import Input
+
+ self.add_event_assigner(EventAssigner(
+ id="my-action",
+ ui_label="Do Something",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.do_something
+ ))
+ ```
+
+## Asset Methods
+
+### `get_asset_path`
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |asset_name|None|Name of the asset file|str|
+ |subdirs|None|List of subdirectories|list[str]|
+ |asset_folder|"assets"|Name of the assets folder|str|
+
+ **Returns**: `str` - Full path to the asset
+
+ ```python
+ icon_path = self.get_asset_path("icon.png")
+ # Returns: /path/to/plugin/assets/icon.png
+
+ icon_path = self.get_asset_path("play.png", subdirs=["icons"])
+ # Returns: /path/to/plugin/assets/icons/play.png
+ ```
+
+### `get_icon`
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |key|None|Icon key registered with plugin|str|
+ |skip_override|False|Skip user overrides|bool|
+
+ **Returns**: `Icon` or `None`
+
+ Gets an icon registered with the plugin's asset manager.
+
+### `get_color`
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |key|None|Color key registered with plugin|str|
+ |skip_override|False|Skip user overrides|bool|
+
+ **Returns**: `Color` or `None`
+
+ Gets a color registered with the plugin's asset manager.
+
+### `get_translation`
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |key|None|Translation key|str|
+ |fallback|None|Fallback if key not found|str|
+
+ **Returns**: `str`
+
+ Gets a localized string from the plugin's locale manager.
+
+ ```python
+ title = self.get_translation("action.title", "Default Title")
+ ```
+
+## Backend Methods
+
+### `launch_backend`
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |backend_path|None|Path to the backend Python file|str|
+ |venv_path|None|Path to a virtual environment|str|
+ |open_in_terminal|False|Open backend in terminal (for debugging)|bool|
+
+ **Description**:
+ Launches a local backend process. The backend communicates with the action via RPyC.
+
+ !!! warning
+ This method blocks until the backend is registered. For plugin-wide backends, use `PluginBase.launch_backend()` instead.
+
+ ```python
+ backend_path = os.path.join(self.plugin_base.PATH, "backend", "backend.py")
+ self.launch_backend(backend_path=backend_path)
+ ```
+
+ After launching, access the backend via `self.backend`:
+ ```python
+ result = self.backend.some_method()
+ ```
+
+ See [BackendBase](BackendBase_py.md) for backend implementation details.
+
+## Utility Methods
+
+### `get_is_multi_action`
+: **Returns**: `bool`
+
+ Returns `True` if this action is part of a multi-action setup (multiple actions on the same key). When `True`, image operations should generally be disabled.
+
+### `get_is_present`
+: **Returns**: `bool`
+
+ Returns `True` if this action is currently visible on the active page.
+
+### `connect`
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |signal|None|The signal to connect to|[Signal](../advanced_concepts/Signals.md)|
+ |callback|None|Callback function|callable|
+
+ **Description**:
+ Connects to application signals for responding to UI events like page renames.
+
+ ```python
+ from src.Signals import Signals
+
+ self.connect(signal=Signals.PageRename, callback=self.on_page_rename)
+ ```
+
+ See [Signals](../advanced_concepts/Signals.md) for available signals.
diff --git a/docs/plugin_dev/bases/EventSystem.md b/docs/plugin_dev/bases/EventSystem.md
new file mode 100644
index 0000000..c836f77
--- /dev/null
+++ b/docs/plugin_dev/bases/EventSystem.md
@@ -0,0 +1,253 @@
+The Event System allows actions to respond to hardware inputs (key presses, dial rotations, touchscreen swipes) in a flexible and user-configurable way.
+
+## Overview
+
+When a user interacts with a Stream Deck, the hardware generates **input events**. These events are routed through **EventAssigners** to your action's callback methods. Users can remap events in the UI, allowing them to customize how actions respond to inputs.
+
+```
+Hardware Input → Input Event → EventAssigner → Your Callback
+ ↑
+ (User can remap)
+```
+
+## EventAssigner
+
+The `EventAssigner` class maps input events to callback functions.
+
+```python
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
+
+self.add_event_assigner(EventAssigner(
+ id="my-action",
+ ui_label="Do Something",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.do_something
+))
+```
+
+### Constructor
+
+| Argument | Type | Description |
+|----------|------|-------------|
+| `id` | `str` | Unique identifier for this event assigner |
+| `ui_label` | `str` | Label displayed in the UI for event remapping |
+| `callback` | `callable` | Function to call when the event fires |
+| `default_event` | `InputEvent` | Single default event (use this OR `default_events`) |
+| `default_events` | `list[InputEvent]` | Multiple default events |
+| `tooltip` | `str` | Optional tooltip shown in UI |
+
+### Multiple Events
+
+You can map multiple events to the same callback:
+
+```python
+self.add_event_assigner(EventAssigner(
+ id="activate",
+ ui_label="Activate",
+ default_events=[
+ Input.Key.Events.DOWN,
+ Input.Dial.Events.DOWN
+ ],
+ callback=self.activate
+))
+```
+
+## Input Events Reference
+
+### Key Events
+
+Available via `Input.Key.Events.*`:
+
+| Event | Description |
+|-------|-------------|
+| `DOWN` | Key pressed down |
+| `UP` | Key released |
+| `SHORT_UP` | Quick tap (released without hold) |
+| `HOLD_START` | Hold begins (after holding threshold) |
+| `HOLD_STOP` | Hold ends |
+
+### Dial Events
+
+Available via `Input.Dial.Events.*`:
+
+| Event | Description |
+|-------|-------------|
+| `DOWN` | Dial pressed down |
+| `UP` | Dial released |
+| `SHORT_UP` | Quick tap |
+| `HOLD_START` | Hold begins |
+| `HOLD_STOP` | Hold ends |
+| `TURN_CW` | Clockwise rotation |
+| `TURN_CCW` | Counter-clockwise rotation |
+| `SHORT_TOUCH_PRESS` | Quick tap on touchscreen above dial |
+| `LONG_TOUCH_PRESS` | Long press on touchscreen above dial |
+
+### Touchscreen Events
+
+Available via `Input.Touchscreen.Events.*`:
+
+| Event | Description |
+|-------|-------------|
+| `DRAG_LEFT` | Swipe left |
+| `DRAG_RIGHT` | Swipe right |
+
+## Registering Events
+
+Register event handlers in your action's `__init__` method using `add_event_assigner()`:
+
+```python
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
+
+class MyAction(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="toggle",
+ ui_label="Toggle State",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.toggle
+ ))
+
+ self.add_event_assigner(EventAssigner(
+ id="reset",
+ ui_label="Reset",
+ default_event=Input.Key.Events.HOLD_START,
+ callback=self.reset,
+ tooltip="Hold to reset"
+ ))
+
+ def toggle(self):
+ # Called on key press
+ pass
+
+ def reset(self):
+ # Called when hold starts
+ pass
+```
+
+## Callback Arguments
+
+Some callbacks receive additional data:
+
+```python
+def on_dial_turn(self, data: dict):
+ # data may contain rotation amount, direction, etc.
+ pass
+```
+
+For most key events, callbacks receive no arguments or can ignore them:
+
+```python
+def on_key_down(self):
+ pass
+
+# Or with optional data parameter
+def on_key_down(self, data=None):
+ pass
+```
+
+## Disabling Event Configuration
+
+By default, users can remap events in the UI. To disable this:
+
+```python
+class MyAction(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.allow_event_configuration = False
+```
+
+## Example: Volume Control
+
+A complete example showing dial events for volume control:
+
+```python
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
+
+class VolumeControl(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.volume = 50
+
+ self.add_event_assigner(EventAssigner(
+ id="volume-up",
+ ui_label="Volume Up",
+ default_event=Input.Dial.Events.TURN_CW,
+ callback=self.volume_up
+ ))
+
+ self.add_event_assigner(EventAssigner(
+ id="volume-down",
+ ui_label="Volume Down",
+ default_event=Input.Dial.Events.TURN_CCW,
+ callback=self.volume_down
+ ))
+
+ self.add_event_assigner(EventAssigner(
+ id="toggle-mute",
+ ui_label="Toggle Mute",
+ default_event=Input.Dial.Events.DOWN,
+ callback=self.toggle_mute
+ ))
+
+ def on_ready(self):
+ self.update_display()
+
+ def volume_up(self):
+ self.volume = min(100, self.volume + 5)
+ self.update_display()
+ self.backend.set_volume(self.volume)
+
+ def volume_down(self):
+ self.volume = max(0, self.volume - 5)
+ self.update_display()
+ self.backend.set_volume(self.volume)
+
+ def toggle_mute(self):
+ self.backend.toggle_mute()
+
+ def update_display(self):
+ self.set_center_label(f"{self.volume}%")
+```
+
+## Example: Multi-Function Key
+
+A key with different actions for tap vs hold:
+
+```python
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
+
+class MultiFunction(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Tap to play/pause
+ self.add_event_assigner(EventAssigner(
+ id="play-pause",
+ ui_label="Play/Pause",
+ default_event=Input.Key.Events.SHORT_UP,
+ callback=self.play_pause
+ ))
+
+ # Hold to skip
+ self.add_event_assigner(EventAssigner(
+ id="skip",
+ ui_label="Skip Track",
+ default_event=Input.Key.Events.HOLD_START,
+ callback=self.skip_track
+ ))
+
+ def play_pause(self):
+ self.backend.toggle_playback()
+
+ def skip_track(self):
+ self.backend.next_track()
+```
diff --git a/docs/plugin_dev/bases/GenerativeUI.md b/docs/plugin_dev/bases/GenerativeUI.md
new file mode 100644
index 0000000..0193663
--- /dev/null
+++ b/docs/plugin_dev/bases/GenerativeUI.md
@@ -0,0 +1,473 @@
+GenerativeUI is a system for creating configuration widgets that automatically save and load their values from the action's settings. This eliminates boilerplate code for managing config state.
+
+## Why GenerativeUI?
+
+**Without GenerativeUI** (manual approach):
+```python
+def get_config_rows(self) -> list:
+ self.spinner = Adw.SpinRow.new_with_range(1, 100, 1)
+ self.spinner.set_title("Count")
+
+ # Load saved value
+ settings = self.get_settings()
+ self.spinner.set_value(settings.get("count", 1))
+
+ # Save on change
+ self.spinner.connect("changed", self.on_spinner_changed)
+
+ return [self.spinner]
+
+def on_spinner_changed(self, spinner):
+ settings = self.get_settings()
+ settings["count"] = int(spinner.get_value())
+ self.set_settings(settings)
+```
+
+**With GenerativeUI**:
+```python
+def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.count_row = SpinRow(
+ action_core=self,
+ var_name="count",
+ default_value=1,
+ title="Count",
+ min=1, max=100, step=1
+ )
+
+def get_config_rows(self) -> list:
+ return [self.count_row.widget]
+```
+
+## Available Widgets
+
+All widgets are in `GtkHelper.GenerativeUI`:
+
+```python
+from GtkHelper.GenerativeUI.EntryRow import EntryRow
+from GtkHelper.GenerativeUI.SpinRow import SpinRow
+from GtkHelper.GenerativeUI.SwitchRow import SwitchRow
+# etc.
+```
+
+| Widget | Purpose | Value Type |
+|--------|---------|------------|
+| `EntryRow` | Text input | `str` |
+| `SpinRow` | Numeric spinner | `int` or `float` |
+| `SwitchRow` | Toggle switch | `bool` |
+| `ComboRow` | Dropdown select | varies |
+| `ScaleRow` | Slider | `float` |
+| `ColorButtonRow` | Color picker | `list[int]` (RGBA) |
+| `PasswordEntryRow` | Password input | `str` |
+| `FileDialogRow` | File chooser | `str` (path) |
+| `ToggleRow` | Toggle button | `bool` |
+| `ExpanderRow` | Expandable section | N/A (container) |
+
+## Common Parameters
+
+All GenerativeUI widgets share these constructor parameters:
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `action_core` | `ActionCore` | The action this widget belongs to |
+| `var_name` | `str` | Key used in settings dict |
+| `default_value` | varies | Default value if not set |
+| `can_reset` | `bool` | Show reset button (default: `True`) |
+| `auto_add` | `bool` | Auto-register with action (default: `True`) |
+| `complex_var_name` | `bool` | Enable dot notation (default: `False`) |
+| `on_change` | `callable` | Callback when value changes |
+
+## Widget Reference
+
+### EntryRow
+
+Text input field.
+
+```python
+from GtkHelper.GenerativeUI.EntryRow import EntryRow
+
+self.name_row = EntryRow(
+ action_core=self,
+ var_name="username",
+ default_value="",
+ title="Username",
+ filter_func=lambda s: s.lower() # Optional: transform input
+)
+```
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `title` | `str` | Row title (can be a translation key) |
+| `filter_func` | `callable` | Function to filter/transform text |
+
+### SpinRow
+
+Numeric spinner with increment/decrement.
+
+```python
+from GtkHelper.GenerativeUI.SpinRow import SpinRow
+
+self.count_row = SpinRow(
+ action_core=self,
+ var_name="count",
+ default_value=10,
+ title="Count",
+ min=1,
+ max=100,
+ step=1
+)
+```
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `title` | `str` | Row title |
+| `min` | `int/float` | Minimum value |
+| `max` | `int/float` | Maximum value |
+| `step` | `int/float` | Increment step |
+
+### SwitchRow
+
+Toggle switch for boolean values.
+
+```python
+from GtkHelper.GenerativeUI.SwitchRow import SwitchRow
+
+self.enabled_row = SwitchRow(
+ action_core=self,
+ var_name="enabled",
+ default_value=True,
+ title="Enable Feature"
+)
+```
+
+### ComboRow
+
+Dropdown selection.
+
+```python
+from GtkHelper.GenerativeUI.ComboRow import ComboRow
+
+self.mode_row = ComboRow(
+ action_core=self,
+ var_name="mode",
+ default_value="auto",
+ title="Mode",
+ options=[
+ ("auto", "Automatic"),
+ ("manual", "Manual"),
+ ("disabled", "Disabled")
+ ]
+)
+```
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `options` | `list[tuple]` | List of `(value, label)` tuples |
+
+### ScaleRow
+
+Slider for continuous values.
+
+```python
+from GtkHelper.GenerativeUI.ScaleRow import ScaleRow
+
+self.volume_row = ScaleRow(
+ action_core=self,
+ var_name="volume",
+ default_value=50,
+ title="Volume",
+ min=0,
+ max=100,
+ step=1
+)
+```
+
+### ColorButtonRow
+
+Color picker.
+
+```python
+from GtkHelper.GenerativeUI.ColorButtonRow import ColorButtonRow
+
+self.color_row = ColorButtonRow(
+ action_core=self,
+ var_name="highlight_color",
+ default_value=[255, 0, 0, 255], # RGBA
+ title="Highlight Color"
+)
+```
+
+### PasswordEntryRow
+
+Password input with hidden text.
+
+```python
+from GtkHelper.GenerativeUI.PasswordEntryRow import PasswordEntryRow
+
+self.api_key_row = PasswordEntryRow(
+ action_core=self,
+ var_name="api_key",
+ default_value="",
+ title="API Key"
+)
+```
+
+### FileDialogRow
+
+File chooser dialog.
+
+```python
+from GtkHelper.GenerativeUI.FileDialogRow import FileDialogRow
+
+self.file_row = FileDialogRow(
+ action_core=self,
+ var_name="script_path",
+ default_value="",
+ title="Script File"
+)
+```
+
+## Using Widgets in Actions
+
+### Basic Pattern
+
+```python
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
+from GtkHelper.GenerativeUI.EntryRow import EntryRow
+from GtkHelper.GenerativeUI.SwitchRow import SwitchRow
+
+class MyAction(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.has_configuration = True # Enable config UI
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
+
+ self.message_row = EntryRow(
+ action_core=self,
+ var_name="message",
+ default_value="Hello",
+ title="Message"
+ )
+
+ self.show_time_row = SwitchRow(
+ action_core=self,
+ var_name="show_time",
+ default_value=False,
+ title="Show Timestamp"
+ )
+
+ def get_config_rows(self) -> list:
+ return [
+ self.message_row.widget,
+ self.show_time_row.widget
+ ]
+
+ def on_key_down(self):
+ message = self.message_row.get_value()
+ if self.show_time_row.get_value():
+ message = f"[{time.strftime('%H:%M')}] {message}"
+ print(message)
+```
+
+### Accessing Values
+
+```python
+# Get current value
+value = self.message_row.get_value()
+
+# Set value programmatically
+self.message_row.set_value("New message")
+
+# Reset to default
+self.message_row.reset_value()
+```
+
+### Change Callbacks
+
+React to value changes:
+
+```python
+self.volume_row = SpinRow(
+ action_core=self,
+ var_name="volume",
+ default_value=50,
+ title="Volume",
+ min=0, max=100, step=1,
+ on_change=self.on_volume_change
+)
+
+def on_volume_change(self, widget, new_value, old_value):
+ self.backend.set_volume(new_value)
+ self.set_center_label(f"{new_value}%")
+```
+
+## Nested Settings
+
+Use `complex_var_name=True` for nested settings structures:
+
+```python
+self.channel_row = EntryRow(
+ action_core=self,
+ var_name="twitch.channel_name",
+ default_value="",
+ title="Channel",
+ complex_var_name=True
+)
+
+self.message_row = EntryRow(
+ action_core=self,
+ var_name="twitch.message",
+ default_value="",
+ title="Message",
+ complex_var_name=True
+)
+```
+
+This creates nested settings:
+```json
+{
+ "twitch": {
+ "channel_name": "mychannel",
+ "message": "Hello chat!"
+ }
+}
+```
+
+## Localization
+
+Widget titles can use translation keys:
+
+```python
+self.message_row = EntryRow(
+ action_core=self,
+ var_name="message",
+ default_value="",
+ title="action.message.title" # Translation key
+)
+```
+
+The widget automatically looks up the translation using `action_core.get_translation()`.
+
+## Disabling Reset Button
+
+```python
+self.api_key_row = PasswordEntryRow(
+ action_core=self,
+ var_name="api_key",
+ default_value="",
+ title="API Key",
+ can_reset=False # No reset button
+)
+```
+
+## Manual Widget Control
+
+If you need more control, set `auto_add=False`:
+
+```python
+self.custom_row = EntryRow(
+ action_core=self,
+ var_name="custom",
+ default_value="",
+ title="Custom",
+ auto_add=False
+)
+
+# Manually manage the widget lifecycle
+def get_config_rows(self) -> list:
+ self.custom_row.load_ui_value() # Manually load value
+ return [self.custom_row.widget]
+```
+
+## Complete Example
+
+```python
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
+from GtkHelper.GenerativeUI.EntryRow import EntryRow
+from GtkHelper.GenerativeUI.SpinRow import SpinRow
+from GtkHelper.GenerativeUI.SwitchRow import SwitchRow
+from GtkHelper.GenerativeUI.ComboRow import ComboRow
+
+class NotificationAction(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.has_configuration = True
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
+
+ self.title_row = EntryRow(
+ action_core=self,
+ var_name="notification.title",
+ default_value="Alert",
+ title="Title",
+ complex_var_name=True
+ )
+
+ self.body_row = EntryRow(
+ action_core=self,
+ var_name="notification.body",
+ default_value="",
+ title="Body",
+ complex_var_name=True
+ )
+
+ self.duration_row = SpinRow(
+ action_core=self,
+ var_name="duration",
+ default_value=5,
+ title="Duration (seconds)",
+ min=1, max=30, step=1
+ )
+
+ self.sound_row = SwitchRow(
+ action_core=self,
+ var_name="play_sound",
+ default_value=True,
+ title="Play Sound"
+ )
+
+ self.urgency_row = ComboRow(
+ action_core=self,
+ var_name="urgency",
+ default_value="normal",
+ title="Urgency",
+ options=[
+ ("low", "Low"),
+ ("normal", "Normal"),
+ ("critical", "Critical")
+ ]
+ )
+
+ def get_config_rows(self) -> list:
+ return [
+ self.title_row.widget,
+ self.body_row.widget,
+ self.duration_row.widget,
+ self.sound_row.widget,
+ self.urgency_row.widget
+ ]
+
+ def on_key_down(self):
+ self.backend.send_notification(
+ title=self.title_row.get_value(),
+ body=self.body_row.get_value(),
+ duration=self.duration_row.get_value(),
+ sound=self.sound_row.get_value(),
+ urgency=self.urgency_row.get_value()
+ )
+```
diff --git a/docs/plugin_dev/bases/PluginBase_py.md b/docs/plugin_dev/bases/PluginBase_py.md
index 74a6e58..cbe0851 100644
--- a/docs/plugin_dev/bases/PluginBase_py.md
+++ b/docs/plugin_dev/bases/PluginBase_py.md
@@ -1,104 +1,235 @@
-The [PluginBase](PluginBase_py.md) is the base for all plugins in [StreamController](https://github.com/Core447/StreamController).
+The [PluginBase](PluginBase_py.md) is the base for all plugins in [StreamController](https://github.com/StreamController/StreamController).
-If you want to learn more by going throught the code click [here](https://github.com/Core447/StreamController/blob/main/src/backend/PluginManager/PluginBase.py).
+If you want to learn more by going through the code click [here](https://github.com/StreamController/StreamController/blob/main/src/backend/PluginManager/PluginBase.py).
+
+## Constructor
+
+```python
+class MyPlugin(PluginBase):
+ def __init__(self):
+ super().__init__()
+```
+
+The constructor accepts an optional parameter:
+
+| Parameter | Default | Description |
+|-----------|---------|-------------|
+| `use_legacy_locale` | `True` | Set to `False` to use CSV-based localization instead of JSON |
+
+```python
+class MyPlugin(PluginBase):
+ def __init__(self):
+ super().__init__()
+ # For CSV localization, just use the locale_manager directly
+ self.lm = self.locale_manager
+ self.lm.set_to_os_default()
+```
+
+## Properties
+
+### `locale_manager`
+: The locale manager for handling translations. Access with `self.locale_manager` or create a shorthand like `self.lm = self.locale_manager`.
+
+### `asset_manager`
+: Manages icons and colors registered with the plugin. Access registered assets via `self.asset_manager.icons` and `self.asset_manager.colors`.
+
+### `backend`
+: The RPyC backend connection if `launch_backend()` was called. Use to call methods on your backend.
+
+### `PATH`
+: The absolute path to your plugin's directory.
+
+## Available Methods
-## Available methods
### `register`
: **Description**:
- Registers the plugin.
+ Registers the plugin with StreamController.
: **Arguments**:
|Argument|Default|Description|Type|
|---|---|---|---|
- |plugin_name|None|The name of the plugin. (can be localized)|str|
- |github_repo|None|The link to your github repository.|str|
- |plugin_version|None|The version of the plugin.|str|
- |app_version|None|The version of the app the plugin is compatible with.|str|
+ |plugin_name|None|The name of the plugin (can be localized)|str|
+ |github_repo|None|The link to your github repository|str|
+ |plugin_version|None|The version of the plugin|str|
+ |app_version|None|The minimum StreamController version required|str|
-### `do_versions_match`
+### `add_action_holder`
: **Description**:
- Checks if the version of the plugin and the app are compatible.
- !!! info
- This is an internal method and there should be no need to use it manually
+ Adds an action holder to the plugin, making the action available in the UI.
: **Arguments**:
|Argument|Default|Description|Type|
|---|---|---|---|
- |app_version_to_check|None|The version of the app to check.|str|
+ |action_holder|None|The action holder to add|ActionHolder|
-### `add_action_holder`
+### `add_action_holders`
: **Description**:
- Adds an action holder to the plugin.
+ Adds multiple action holders at once.
: **Arguments**:
|Argument|Default|Description|Type|
|---|---|---|---|
- |action_holder|None|The action holder to add.|ActionHolder|
+ |action_holders|None|List of action holders to add|list[ActionHolder]|
-### `set_settings`
+### `add_action_holder_group`
+: **Description**:
+ Adds a group of related actions that will be displayed together in the UI.
+
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |action_holder_group|None|The action holder group to add|ActionHolderGroup|
+
+### `add_icon`
+: **Description**:
+ Registers an icon with the plugin's asset manager for use in actions.
+
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |key|None|Unique identifier for the icon|str|
+ |path|None|Path to the icon file|str|
+
+ ```python
+ def __init__(self):
+ super().__init__()
+ self.add_icon("main", self.get_asset_path("icon.png"))
+ self.add_icon("muted", self.get_asset_path("muted.png"))
+ ```
+
+### `add_color`
: **Description**:
+ Registers a color with the plugin's asset manager for use in actions.
+
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |key|None|Unique identifier for the color|str|
+ |color|None|RGBA color values (0-255)|list[int]|
- This settings stores plugin specific settings under `plugin_dir/settings.json`
+ ```python
+ def __init__(self):
+ super().__init__()
+ self.add_color("default", [0, 0, 0, 0])
+ self.add_color("warning", [255, 244, 79, 255])
+ ```
+
+### `get_asset_path`
+: **Description**:
+ Helper method to construct paths to plugin assets.
: **Arguments**:
|Argument|Default|Description|Type|
|---|---|---|---|
- |settings|None|The settings to store.|dict|
+ |asset_name|None|Name of the asset file|str|
+ |subdirs|None|List of subdirectories|list[str]|
+ |asset_folder|"assets"|Name of the assets folder|str|
+
+: **Returns**: `str` - Full path to the asset
+
+ ```python
+ icon_path = self.get_asset_path("icon.png")
+ # Returns: /path/to/plugin/assets/icon.png
+ ```
+
+### `set_settings`
+: **Description**:
+ Stores plugin-specific settings under `plugin_dir/settings.json`.
+
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |settings|None|The settings to store|dict|
### `get_settings`
: **Description**:
-
- This method returns a dictionary with all your set settings for this plugin.
- For more see [`set_settings`](#set_settings).
+ Returns a dictionary with all stored settings for this plugin.
+ See [`set_settings`](#set_settings).
### `add_css_stylesheet`
: **Description**:
-
- Adds a css stylesheet to the plugin, allowing actions to further style their config areas.
+ Adds a CSS stylesheet to the plugin, allowing actions to further style their config areas.
!!! warning
- The stylesheet will be loaded to the main StreamController window. Be careful to not override any existing styles. Therefore it is recomended to start all names with an unique prefix.
+ The stylesheet will be loaded to the main StreamController window. Be careful to not override any existing styles. It is recommended to start all class names with a unique prefix.
: **Arguments**:
|Argument|Default|Description|Type|
|---|---|---|---|
- |path|None|The path of the stylesheet to add.|str|
+ |path|None|The path of the stylesheet to add|str|
### `register_page`
: **Description**:
-
Adds a page to StreamController.
: **Arguments**:
|Argument|Default|Description|Type|
|---|---|---|---|
- |path|None|The path of the page to add.|str|
+ |path|None|The path of the page to add|str|
### `launch_backend`
: **Description**:
+ Launches a plugin-wide backend process. See [BackendBase](../bases/BackendBase_py.md).
- Launches a plugin wide backend. See [BackendBase](../bases/BackendBase_py.md).
+ After launching, access the backend via `self.backend`:
+ ```python
+ result = self.backend.some_method()
+ ```
: **Arguments**:
|Argument|Default|Description|Type|
|---|---|---|---|
- |backend_path|None|The path of the backend to launch.|str|
- |venv_path|None|The path of the venv to use.|str|
- |open_in_terminal|False|Open the backend in a terminal window. Useful for debugging.|bool
+ |backend_path|None|The path of the backend to launch|str|
+ |venv_path|None|The path of the venv to use|str|
+ |open_in_terminal|False|Open the backend in a terminal window (useful for debugging)|bool|
### `get_selector_image`
: **Description**:
-
- Returns the icon used for the plugin selector in the ui.
+ Returns the icon used for the plugin selector in the UI.
### `on_uninstall`
: **Description**:
+ Called when the plugin is uninstalled. Disconnects and stops the backend if launched.
- Disconnects and stops own backend if launched.
\ No newline at end of file
+### `do_versions_match`
+: **Description**:
+ Checks if the version of the plugin and the app are compatible.
+
+ !!! info
+ This is an internal method and there should be no need to use it manually.
+
+: **Arguments**:
+
+ |Argument|Default|Description|Type|
+ |---|---|---|---|
+ |app_version_to_check|None|The version of the app to check|str|
+
+## Plugin Settings UI
+
+Plugins can provide a settings page accessible from the plugin list:
+
+```python
+class MyPlugin(PluginBase):
+ def __init__(self):
+ super().__init__()
+ self.has_plugin_settings = True # Enable settings button
+
+ def get_settings_area(self) -> Adw.PreferencesGroup:
+ pref_group = Adw.PreferencesGroup()
+ pref_group.set_title("My Plugin Settings")
+
+ # Add your settings widgets here
+
+ return pref_group
+```
diff --git a/docs/plugin_dev/modify_template/AddCounter.md b/docs/plugin_dev/modify_template/AddCounter.md
index 25e34c6..abc803f 100644
--- a/docs/plugin_dev/modify_template/AddCounter.md
+++ b/docs/plugin_dev/modify_template/AddCounter.md
@@ -15,14 +15,20 @@ Now let's add the actual action to the `counter.py` file.
```python title="counter.py"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
-class Counter(ActionBase):
+class Counter(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
```
That's it, at least for now. You just created a basic action without any functionality. However, this would not be shown it the ui yet.
@@ -34,6 +40,8 @@ All actions of a plugin have to be registered in the plugin's [plugin base](../b
# Import StreamController modules
from src.backend.PluginManager.PluginBase import PluginBase
from src.backend.PluginManager.ActionHolder import ActionHolder
+from src.backend.PluginManager.ActionInputSupport import ActionInputSupport
+from src.backend.DeckManagement.InputIdentifier import Input
# Import actions
from .actions.SimpleAction.SimpleAction import SimpleAction
@@ -44,19 +52,24 @@ class PluginTemplate(PluginBase):
## Register actions
self.simple_action_holder = ActionHolder(
- plugin_base = self,
- action_base = SimpleAction,
- action_id = "dev_core447_Template::SimpleAction", # Change this to your own plugin id
- action_name = "Simple Action",
+ plugin_base=self,
+ action_core=SimpleAction,
+ action_id_suffix="SimpleAction",
+ action_name="Simple Action",
+ action_support={
+ Input.Key: ActionInputSupport.SUPPORTED,
+ Input.Dial: ActionInputSupport.UNSUPPORTED,
+ Input.Touchscreen: ActionInputSupport.UNSUPPORTED
+ }
)
self.add_action_holder(self.simple_action_holder)
# Register plugin
self.register(
- plugin_name = "Template",
- github_repo = "https://github.com/StreamController/PluginTemplate",
- plugin_version = "1.0.0",
- app_version = "1.1.1-alpha"
+ plugin_name="Template",
+ github_repo="https://github.com/StreamController/PluginTemplate",
+ plugin_version="1.0.0",
+ app_version="1.5.0-beta.9"
)
```
The good news is that you just have to add a couple of lines to make your new action available.
@@ -64,11 +77,13 @@ The first step is to import the newly created action. To do so you can just add
```python
from .actions.counter.counter import Counter
```
-The only thing left to do is to register the action by creating an [ActionHolder]() and adding it to the plugin:
-```python title="main.py" hl_lines="22-28"
+The only thing left to do is to register the action by creating an `ActionHolder` and adding it to the plugin:
+```python title="main.py" hl_lines="10 28-37"
# Import StreamController modules
from src.backend.PluginManager.PluginBase import PluginBase
from src.backend.PluginManager.ActionHolder import ActionHolder
+from src.backend.PluginManager.ActionInputSupport import ActionInputSupport
+from src.backend.DeckManagement.InputIdentifier import Input
# Import actions
from .actions.SimpleAction.SimpleAction import SimpleAction
@@ -80,71 +95,108 @@ class PluginTemplate(PluginBase):
## Register actions
self.simple_action_holder = ActionHolder(
- plugin_base = self,
- action_base = SimpleAction,
- action_id = "dev_core447_Template::SimpleAction", # Change this to your own plugin id
- action_name = "Simple Action",
+ plugin_base=self,
+ action_core=SimpleAction,
+ action_id_suffix="SimpleAction",
+ action_name="Simple Action",
+ action_support={
+ Input.Key: ActionInputSupport.SUPPORTED,
+ Input.Dial: ActionInputSupport.UNSUPPORTED,
+ Input.Touchscreen: ActionInputSupport.UNSUPPORTED
+ }
)
self.add_action_holder(self.simple_action_holder)
self.counter_action_holder = ActionHolder(
- plugin_base = self,
- action_base = Counter,
- action_id = "dev_core447_Template::Counter", # Change this to your own plugin id
- action_name = "Counter",
+ plugin_base=self,
+ action_core=Counter,
+ action_id_suffix="Counter",
+ action_name="Counter",
+ action_support={
+ Input.Key: ActionInputSupport.SUPPORTED,
+ Input.Dial: ActionInputSupport.UNSUPPORTED,
+ Input.Touchscreen: ActionInputSupport.UNSUPPORTED
+ }
)
self.add_action_holder(self.counter_action_holder)
# Register plugin
self.register(
- plugin_name = "Template",
- github_repo = "https://github.com/StreamController/PluginTemplate",
- plugin_version = "1.0.0",
- app_version = "1.1.1-alpha"
+ plugin_name="Template",
+ github_repo="https://github.com/StreamController/PluginTemplate",
+ plugin_version="1.0.0",
+ app_version="1.5.0-beta.9"
)
```
!!! note
- The `action_id` must be unique and in the following format: {reverse-domain with underscores}::{action_name}
+ The `action_id_suffix` combined with your plugin ID creates a unique action identifier. You can also use `action_id` for full control over the ID format: `{reverse_domain_with_underscores}::{action_name}`
### 5. Do something!!!
What is the point of an action that does nothing? None at all. But we're going to change that now.
-Let's change the action to cound the presses and show the number on the key.
+Let's change the action to count the presses and show the number on the key.
For that we have to modify `counter.py`:
```python title="counter.py"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
-class Counter(ActionBase):
+class Counter(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
```
#### 1. The first thing we need to do is to add a counter variable:
-```python title="counter.py (partial)" hl_lines="7"
-class Counter(ActionBase):
+```python title="counter.py (partial)" hl_lines="16"
+class Counter(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
-
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
+
self.counter: int = 0
```
#### 2. Now we need to increase the counter if the action key gets pressed:
-```python title="counter.py (partial)" hl_lines="9 10"
-class Counter(ActionBase):
+```python title="counter.py (partial)" hl_lines="14-15"
+class Counter(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
-
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
+
self.counter: int = 0
def on_key_down(self):
self.counter += 1
```
#### 3. Update the label on the key if the counter changes:
-```python title="counter.py (partial)" hl_lines="11"
-class Counter(ActionBase):
+```python title="counter.py (partial)" hl_lines="16"
+class Counter(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
-
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
+
self.counter: int = 0
def on_key_down(self):
@@ -152,11 +204,18 @@ class Counter(ActionBase):
self.set_center_label(str(self.counter))
```
#### 4. Show the initial counter on load up:
-```python title="counter.py (partial)" hl_lines="9-10"
-class Counter(ActionBase):
+```python title="counter.py (partial)" hl_lines="14-15"
+class Counter(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
-
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
+
self.counter: int = 0
def on_ready(self):
@@ -170,15 +229,21 @@ class Counter(ActionBase):
The final `counter.py` looks like this:
```python title="counter.py"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
-class Counter(ActionBase):
+class Counter(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
-
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
+
self.counter: int = 0
def on_ready(self):
@@ -187,4 +252,4 @@ class Counter(ActionBase):
def on_key_down(self):
self.counter += 1
self.set_center_label(str(self.counter))
-```
\ No newline at end of file
+```
diff --git a/docs/plugin_dev/modify_template/add_a_backend_action.md b/docs/plugin_dev/modify_template/add_a_backend_action.md
index 4383c6c..9f73d3b 100644
--- a/docs/plugin_dev/modify_template/add_a_backend_action.md
+++ b/docs/plugin_dev/modify_template/add_a_backend_action.md
@@ -36,7 +36,7 @@ backend = Backend() #(2)!
1. Import the [BackendBase](../bases/BackendBase_py.md)
2. Create an instance of the class
-The backend will automatically connect to your action. This is possible because [launch_backend](../bases/ActionBase_py.md#launch_backend) starts `backend.py` with the [rpyc](https://rpyc.readthedocs.io/en/latest/) port as an argument.
+The backend will automatically connect to your action. This is possible because [launch_backend](../bases/ActionCore_py.md#launch_backend) starts `backend.py` with the [rpyc](https://rpyc.readthedocs.io/en/latest/) port as an argument.
### 4. Add counter methods to the backend
Now we can add methods to retrive the current number and increment it.
@@ -63,16 +63,20 @@ Now we can remove the old counter code from [Counter](AddCounter.md) because we
This results into:
```python title="counter.py"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
-class Counter(ActionBase):
- def __init__(self, action_id: str, action_name: str,
- deck_controller: DeckController, page: Page, coords: str, plugin_base: PluginBase):
- super().__init__(action_id=action_id, action_name=action_name,
- deck_controller=deck_controller, page=page, coords=coords, plugin_base=plugin_base)
+class Counter(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
def on_ready(self):
pass
@@ -82,21 +86,25 @@ class Counter(ActionBase):
```
### 6. Launch the backend from the action
-The next step is to launch the backend from the action. To do this, we will use the [launch_backend](../bases/ActionBase_py.md#launch_backend) method of the [ActionBase](../bases/ActionBase_py.md). This method will start the backend with the [rpyc](https://rpyc.readthedocs.io/en/latest/) port of the action as an argument.
-```python title="counter.py" hl_lines="7 15-16"
+The next step is to launch the backend from the action. To do this, we will use the [launch_backend](../bases/ActionCore_py.md#launch_backend) method of [ActionCore](../bases/ActionCore_py.md). This method will start the backend with the [rpyc](https://rpyc.readthedocs.io/en/latest/) port of the action as an argument.
+```python title="counter.py" hl_lines="5 18-19"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
import os
-class Counter(ActionBase):
- def __init__(self, action_id: str, action_name: str,
- deck_controller: DeckController, page: Page, coords: str, plugin_base: PluginBase):
- super().__init__(action_id=action_id, action_name=action_name,
- deck_controller=deck_controller, page=page, coords=coords, plugin_base=plugin_base)
+class Counter(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
backend_path = os.path.join(self.plugin_base.PATH, "actions", "counter", "backend", "backend.py") #(1)!
self.launch_backend(backend_path=backend_path, open_in_terminal=True) #(2)!
@@ -121,20 +129,24 @@ If you encounter any problems feel free to open an issue on the [StreamControlle
### 8. Use the backend
Now that we have a backend, we can use it methods to manage the counter state.
-```python title="counter.py" hl_lines="18-23"
+```python title="counter.py" hl_lines="22-25"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
import os
-class Counter(ActionBase):
- def __init__(self, action_id: str, action_name: str,
- deck_controller: DeckController, page: Page, coords: str, plugin_base: PluginBase):
- super().__init__(action_id=action_id, action_name=action_name,
- deck_controller=deck_controller, page=page, coords=coords, plugin_base=plugin_base)
+class Counter(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
backend_path = os.path.join(self.plugin_base.PATH, "actions", "counter", "backend", "backend.py")
self.launch_backend(backend_path=backend_path, open_in_terminal=True)
@@ -147,20 +159,26 @@ class Counter(ActionBase):
self.set_center_label(str(self.backend.get_count()))
```
### 9. Add error handling
-With a new component in our plugin than might break or crash, it is always a good idea to inform the user about any errors that might occur. We can do this by using the [show_error](../bases/ActionBase_py.md#show_error) method of the [ActionBase](../bases/ActionBase_py.md).
-```python title="counter.py" hl_lines="8 20-27 30-38"
+With a new component in our plugin than might break or crash, it is always a good idea to inform the user about any errors that might occur. We can do this by using the [show_error](../bases/ActionCore_py.md#show_error) method of [ActionCore](../bases/ActionCore_py.md).
+```python title="counter.py" hl_lines="6 24-31 34-41"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
import os
from loguru import logger as log #(1)!
-class Counter(ActionBase):
+class Counter(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
backend_path = os.path.join(self.plugin_base.PATH, "actions", "counter", "backend", "backend.py")
self.launch_backend(backend_path=backend_path, open_in_terminal=True)
@@ -192,4 +210,4 @@ class Counter(ActionBase):
This code shows communication errors between the frontend and the backend on the deck.
If you still have `open_in_terminal` set to `True`, you can easily test the code by closing the terminal window. This will lead to an error on the next key press.
!!! note "Try/Catch"
- If you use try/except to catch such errors, it is important to log the errors in some sort to allow easy debugging.
\ No newline at end of file
+ If you use try/except to catch such errors, it is important to log the errors in some sort to allow easy debugging.
diff --git a/docs/plugin_dev/modify_template/add_a_plugin_backend.md b/docs/plugin_dev/modify_template/add_a_plugin_backend.md
index ee16596..cfd1138 100644
--- a/docs/plugin_dev/modify_template/add_a_plugin_backend.md
+++ b/docs/plugin_dev/modify_template/add_a_plugin_backend.md
@@ -40,21 +40,27 @@ backend = Backend()
!!! note
As you can see there is no difference in the backend code between a plugin and an action backend.
-### 4. Remove [backend launch](../bases/ActionBase_py.md#launch_backend) from the [counter action](AddCounter.md)
+### 4. Remove backend launch from the counter action
To do this remove the highlighted lines:
-```python title="counter.py" hl_lines="16-17"
+```python title="counter.py" hl_lines="19-20"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
import os
from loguru import logger as log
-class Counter(ActionBase):
+class Counter(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
backend_path = os.path.join(self.plugin_base.PATH, "actions", "counter", "backend", "backend.py")
self.launch_backend(backend_path=backend_path, open_in_terminal=True)
@@ -83,10 +89,14 @@ class Counter(ActionBase):
### 5. Launch the backend
Now we can launch the backend from within the plugin:
-```python title="main.py" hl_lines="13-15"
+```python title="main.py" hl_lines="7 15-18"
# Import StreamController modules
from src.backend.PluginManager.PluginBase import PluginBase
from src.backend.PluginManager.ActionHolder import ActionHolder
+from src.backend.PluginManager.ActionInputSupport import ActionInputSupport
+from src.backend.DeckManagement.InputIdentifier import Input
+
+import os
# Import actions
from .actions.SimpleAction.SimpleAction import SimpleAction
@@ -102,45 +112,60 @@ class PluginTemplate(PluginBase):
## Register actions
self.simple_action_holder = ActionHolder(
- plugin_base = self,
- action_base = SimpleAction,
- action_id = "dev_core447_Template::SimpleAction", # Change this to your own plugin id
- action_name = "Simple Action",
+ plugin_base=self,
+ action_core=SimpleAction,
+ action_id_suffix="SimpleAction",
+ action_name="Simple Action",
+ action_support={
+ Input.Key: ActionInputSupport.SUPPORTED,
+ Input.Dial: ActionInputSupport.UNSUPPORTED,
+ Input.Touchscreen: ActionInputSupport.UNSUPPORTED
+ }
)
self.add_action_holder(self.simple_action_holder)
self.counter_action_holder = ActionHolder(
- plugin_base = self,
- action_base = Counter,
- action_id = "dev_core447_Template::Counter", # Change this to your own plugin id
- action_name = "Counter",
+ plugin_base=self,
+ action_core=Counter,
+ action_id_suffix="Counter",
+ action_name="Counter",
+ action_support={
+ Input.Key: ActionInputSupport.SUPPORTED,
+ Input.Dial: ActionInputSupport.UNSUPPORTED,
+ Input.Touchscreen: ActionInputSupport.UNSUPPORTED
+ }
)
self.add_action_holder(self.counter_action_holder)
# Register plugin
self.register(
- plugin_name = "Template",
- github_repo = "https://github.com/StreamController/PluginTemplate",
- plugin_version = "1.0.0",
- app_version = "1.1.1-alpha"
+ plugin_name="Template",
+ github_repo="https://github.com/StreamController/PluginTemplate",
+ plugin_version="1.0.0",
+ app_version="1.5.0-beta.9"
)
```
### 6. Use the backend
Now we can modify `counter.py` to use the new plugin backend:
-```python title="counter.py" hl_lines="18 28-29"
+```python title="counter.py" hl_lines="23 27-28"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
-import os
from loguru import logger as log
-class Counter(ActionBase):
+class Counter(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
def on_ready(self):
try:
@@ -162,4 +187,4 @@ class Counter(ActionBase):
return
self.set_center_label(count)
-```
\ No newline at end of file
+```
diff --git a/docs/plugin_dev/modify_template/config/add_config_rows.md b/docs/plugin_dev/modify_template/config/add_config_rows.md
index eecacf7..64e37e8 100644
--- a/docs/plugin_dev/modify_template/config/add_config_rows.md
+++ b/docs/plugin_dev/modify_template/config/add_config_rows.md
@@ -2,6 +2,9 @@ This example will go over how to add a config row to the [Counter action](../Add
We will use the [Adw.SpinRow](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.SpinRow.html) to control the increment of the counter.
+!!! tip
+ For a simpler approach, consider using [GenerativeUI](../../bases/GenerativeUI.md) widgets which handle saving and loading automatically.
+
## 1. Setup
!!! warning
This example is for the [Counter action](../AddCounter.md) without any backends but you can easily adapt it to your needs.
@@ -9,16 +12,20 @@ We will use the [Adw.SpinRow](https://gnome.pages.gitlab.gnome.org/libadwaita/do
The `counter.py` looks like this:
```python title="counter.py"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
-
-class Counter(ActionBase):
- def __init__(self, action_id: str, action_name: str,
- deck_controller: DeckController, page: Page, coords: str, plugin_base: PluginBase):
- super().__init__(action_id=action_id, action_name=action_name,
- deck_controller=deck_controller, page=page, coords=coords, plugin_base=plugin_base)
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
+
+class Counter(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
self.counter: int = 0
@@ -31,13 +38,12 @@ class Counter(ActionBase):
```
## 2. Add the row
-You can add config rows by overwiding the [`get_config_rows`](../../bases/ActionBase_py.md#get_config_rows) method.
-```python title="counter.py" hl_lines="7-11 28-31"
+You can add config rows by overriding the [`get_config_rows`](../../bases/ActionCore_py.md#get_config_rows) method.
+```python title="counter.py" hl_lines="6-10 28-31"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
# Import gtk
import gi
@@ -45,11 +51,16 @@ gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw #(1)!
-class Counter(ActionBase):
- def __init__(self, action_id: str, action_name: str,
- deck_controller: DeckController, page: Page, coords: str, plugin_base: PluginBase):
- super().__init__(action_id=action_id, action_name=action_name,
- deck_controller=deck_controller, page=page, coords=coords, plugin_base=plugin_base)
+class Counter(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
self.counter: int = 0
@@ -79,12 +90,11 @@ As you can see the spinner is now visible in the config area, but it looks weird
## 4. Add a title
You can add a title to the [Adw.SpinRow](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.SpinRow.html) by using the [`set_title`](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/method.PreferencesRow.set_title.html) method. It is also recommended to add a subtitle as well by using [`set_subtitle`](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/method.ActionRow.set_subtitle.html).
-```python title="counter.py" hl_lines="30-31"
+```python title="counter.py" hl_lines="34-35"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
# Import gtk
import gi
@@ -92,11 +102,16 @@ gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw
-class Counter(ActionBase):
- def __init__(self, action_id: str, action_name: str,
- deck_controller: DeckController, page: Page, coords: str, plugin_base: PluginBase):
- super().__init__(action_id=action_id, action_name=action_name,
- deck_controller=deck_controller, page=page, coords=coords, plugin_base=plugin_base)
+class Counter(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
self.counter: int = 0
@@ -138,9 +153,9 @@ To store the value we have to connect to it's `changed` signal:
```
1. Connect the `changed` signal
-2. Get the settings via [`get_settings()`](../../bases/ActionBase_py.md#get_settings)
+2. Get the settings via [`get_settings()`](../../bases/ActionCore_py.md#get_settings)
3. Set the value of the spinner
-4. Set the new settings via [`set_settings()`](../../bases/ActionBase_py.md#set_settings)
+4. Set the new settings via [`set_settings()`](../../bases/ActionCore_py.md#set_settings)
## 6. Restore the value after reload
If you leave the action area and re-enter it, the value will be reset to 1. To change this we have to retrive the stored value and set it to the spinner:
@@ -183,10 +198,9 @@ def on_key_down(self):
The full `counter.py` looks like this:
```python title="counter.py"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
# Import gtk
import gi
@@ -194,11 +208,16 @@ gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw
-class Counter(ActionBase):
- def __init__(self, action_id: str, action_name: str,
- deck_controller: DeckController, page: Page, coords: str, plugin_base: PluginBase):
- super().__init__(action_id=action_id, action_name=action_name,
- deck_controller=deck_controller, page=page, coords=coords, plugin_base=plugin_base)
+class Counter(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
self.counter: int = 0
@@ -229,4 +248,49 @@ class Counter(ActionBase):
settings = self.get_settings()
settings["increment_by"] = int(spinner.get_value())
self.set_settings(settings)
-```
\ No newline at end of file
+```
+
+## Alternative: Using GenerativeUI
+
+The same functionality can be achieved with much less code using [GenerativeUI](../../bases/GenerativeUI.md):
+
+```python title="counter.py"
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
+from GtkHelper.GenerativeUI.SpinRow import SpinRow
+
+class Counter(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.has_configuration = True
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
+
+ self.counter: int = 0
+
+ self.increment_row = SpinRow(
+ action_core=self,
+ var_name="increment_by",
+ default_value=1,
+ title="Increment by",
+ min=1, max=100, step=1
+ )
+
+ def on_ready(self):
+ self.set_center_label(str(self.counter))
+
+ def on_key_down(self):
+ self.counter += self.increment_row.get_value()
+ self.set_center_label(str(self.counter))
+
+ def get_config_rows(self) -> list:
+ return [self.increment_row.widget]
+```
+
+GenerativeUI automatically handles saving, loading, and resetting values.
diff --git a/docs/plugin_dev/modify_template/config/gtk_intro.md b/docs/plugin_dev/modify_template/config/gtk_intro.md
index 2ee9d6b..e17b873 100644
--- a/docs/plugin_dev/modify_template/config/gtk_intro.md
+++ b/docs/plugin_dev/modify_template/config/gtk_intro.md
@@ -13,8 +13,9 @@ Good resources to get started with [GTK](https://www.gtk.org) in python:
If you feel overwhelmed by [GTK](https://www.gtk.org) I have good news for you:
1. You don't need to know much about [GTK](https://www.gtk.org) to add config rows to your actions.
- 2. You can check out [other plugins](../../intro.md#official-plugins) to see how they implemented config rows.
- 3. Feel free to ask any questions on the [StreamController Discord](https://discord.gg/MSyHM8TN3u)
+ 2. You can use [GenerativeUI](../../bases/GenerativeUI.md) widgets which handle saving/loading automatically.
+ 3. You can check out [other plugins](../../intro.md#official-plugins) to see how they implemented config rows.
+ 4. Feel free to ask any questions on the [StreamController Discord](https://discord.gg/MSyHM8TN3u)
## Action Configuration Area
@@ -23,8 +24,8 @@ Good resources to get started with [GTK](https://www.gtk.org) in python:
The configuration area is splitted into two parts:
: #### Custom config rows (marked blue)
Here plugins can add [Adw.PreferencesRow](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.PreferencesRow.html) widgets.
-See [`get_config_rows`](../../bases/ActionBase_py.md#get_config_rows) for more information about the implementation.
+See [`get_config_rows`](../../bases/ActionCore_py.md#get_config_rows) for more information about the implementation.
: #### Custom config area (marked green)
Here plugins can add any [Gtk.Widgets](https://docs.gtk.org/gtk4/class.Widget.html) widgets allowing more options for customization but also requiring more work.
-See [`get_custom_config_area`](../../bases/ActionBase_py.md#get_custom_config_area) for more information about the implementation.
\ No newline at end of file
+See [`get_custom_config_area`](../../bases/ActionCore_py.md#get_custom_config_area) for more information about the implementation.
diff --git a/docs/plugin_dev/modify_template/localization.md b/docs/plugin_dev/modify_template/localization.md
index 7103c6b..ef99b48 100644
--- a/docs/plugin_dev/modify_template/localization.md
+++ b/docs/plugin_dev/modify_template/localization.md
@@ -1,57 +1,79 @@
-The [Adw.SpinRow](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.SpinRow.html) added in [Add Config Rows](config/add_config_rows.md) has an English title and subtitle. This is fine for English users, but for other languages the title and subtitle are not localized. This is why plugins should use StreamControllers [LocaleManager](https://github.com/StreamController/StreamController/blob/main/locales/LocaleManager.py).
+The [Adw.SpinRow](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.SpinRow.html) added in [Add Config Rows](config/add_config_rows.md) has an English title and subtitle. This is fine for English users, but for other languages the title and subtitle are not localized. This is why plugins should use StreamController's LocaleManager.
-[LocaleManager](https://github.com/StreamController/StreamController/blob/main/locales/LocaleManager.py) chooses the right localized string based on the language the user is using.
+The LocaleManager chooses the right localized string based on the language the user is using.
-The [LocaleManager](https://github.com/StreamController/StreamController/blob/main/locales/LocaleManager.py) of your plugin can be reached with `self.locale_manager` in the [`PluginBase`](../bases/PluginBase_py.md) and with `self.plugin_base.locale_manager` in your [`Actions`](../bases/ActionBase_py.md).
+The LocaleManager of your plugin can be reached with `self.locale_manager` in the [`PluginBase`](../bases/PluginBase_py.md) and with `self.plugin_base.locale_manager` in your actions.
!!! info
Each plugin must be available in English.
-## 1. How to localize
-- Locals are placed in the `locales` subfolder of your plugin (you might have to create it if it doesn't exist) and in the format of `json` files.
-- The json cannot contain keys containing a new dictionary.
-This is valid:
-```json
-{
- "plugin.name": "Name"
-}
+## Localization Formats
+
+StreamController supports two localization formats:
+
+### CSV Format (Recommended)
+
+The modern approach uses a single `locales.csv` file:
+
+```csv title="locales.csv"
+key;en_US;de_DE;fr_FR
+plugin.name;My Plugin;Mein Plugin;Mon Plugin
+actions.counter.name;Counter;Zähler;Compteur
+actions.counter.spinner.title;Increment by;Erhöhen um;Incrémenter de
```
-This isn't:
-```json
-{
- "plugin": {
- "name": "Name"
- }
-}
+
+To use CSV localization, initialize your plugin with:
+```python
+class MyPlugin(PluginBase):
+ def __init__(self):
+ super().__init__()
+ self.lm = self.locale_manager
+ self.lm.set_to_os_default()
```
-- The values can be retrieved with `self.locale_manager.get("key")`
-
-## 2. Localize the plugin
-In this example we will localize the [counter action](AddCounter.md) in [this](config/add_config_rows.md#7-use-the-value) state.
-### 2.1 Localize the [PluginBase](../bases/PluginBase_py.md)
-#### Add a language file
-Create a new `locales` subfolder in your plugin by typing:
-```bash
-mkdir locales
+
+### JSON Format (Legacy)
+
+The older approach uses separate JSON files for each language in a `locales` folder:
+
```
-Add a new `en_US.json` file to the `locales` subfolder by typing:
-```bash
-touch locales/en_US.json
+locales/
+├── en_US.json
+├── de_DE.json
+└── fr_FR.json
```
-Add the needed language keys:
-```json
+
+```json title="en_US.json"
{
- "plugin.name": "Template",
- "actions.simple.name": "Simple Action",
- "actions.counter.name": "Counter",
+ "plugin.name": "My Plugin",
+ "actions.counter.name": "Counter"
}
```
-#### Use the language file
-```python title="main.py" hl_lines="13 20 28 34"
+!!! note
+ JSON locales cannot contain nested dictionaries. Use flat key names like `"actions.counter.name"` instead of nested objects.
+
+## How to Localize
+
+### 1. Create the locale file
+
+For CSV format, create `locales.csv` in your plugin root:
+```csv
+key;en_US
+plugin.name;Template
+actions.simple.name;Simple Action
+actions.counter.name;Counter
+actions.counter.spinner.title;Increment by
+actions.counter.spinner.subtitle;How much to increment the counter by
+```
+
+### 2. Use locale strings in PluginBase
+
+```python title="main.py" hl_lines="14 22 32"
# Import StreamController modules
from src.backend.PluginManager.PluginBase import PluginBase
from src.backend.PluginManager.ActionHolder import ActionHolder
+from src.backend.PluginManager.ActionInputSupport import ActionInputSupport
+from src.backend.DeckManagement.InputIdentifier import Input
# Import actions
from .actions.SimpleAction.SimpleAction import SimpleAction
@@ -62,54 +84,137 @@ class PluginTemplate(PluginBase):
super().__init__()
self.lm = self.locale_manager #(1)!
+ self.lm.set_to_os_default()
## Register actions
self.simple_action_holder = ActionHolder(
- plugin_base = self,
- action_base = SimpleAction,
- action_id = "dev_core447_Template::SimpleAction", # Change this to your own plugin id
- action_name = self.lm.get("actions.simple.name")
+ plugin_base=self,
+ action_core=SimpleAction,
+ action_id_suffix="SimpleAction",
+ action_name=self.lm.get("actions.simple.name"),
+ action_support={
+ Input.Key: ActionInputSupport.SUPPORTED,
+ Input.Dial: ActionInputSupport.UNSUPPORTED,
+ Input.Touchscreen: ActionInputSupport.UNSUPPORTED
+ }
)
self.add_action_holder(self.simple_action_holder)
self.counter_action_holder = ActionHolder(
- plugin_base = self,
- action_base = Counter,
- action_id = "dev_core447_Template::Counter", # Change this to your own plugin id
- action_name = self.lm.get("actions.counter.name")
+ plugin_base=self,
+ action_core=Counter,
+ action_id_suffix="Counter",
+ action_name=self.lm.get("actions.counter.name"),
+ action_support={
+ Input.Key: ActionInputSupport.SUPPORTED,
+ Input.Dial: ActionInputSupport.UNSUPPORTED,
+ Input.Touchscreen: ActionInputSupport.UNSUPPORTED
+ }
)
self.add_action_holder(self.counter_action_holder)
# Register plugin
self.register(
- plugin_name = self.lm.get("plugin.name"),
- github_repo = "https://github.com/StreamController/PluginTemplate",
- plugin_version = "1.0.0",
- app_version = "1.1.1-alpha"
+ plugin_name=self.lm.get("plugin.name"),
+ github_repo="https://github.com/StreamController/PluginTemplate",
+ plugin_version="1.0.0",
+ app_version="1.5.0-beta.9"
)
```
-1. Make the [LocaleManager](https://github.com/StreamController/StreamController/blob/main/locales/LocaleManager.py) available under a shorter name
+1. Make the LocaleManager available under a shorter name
!!! warning
- Do **not** localize the `action_ids`. This will result in disabled action if the user switches to another language.
+ Do **not** localize the `action_id` or `action_id_suffix`. This will result in disabled actions if the user switches to another language.
Only localize visible strings.
-## 2.2 Localize the [counter](AddCounter.md)
-Extend `en_US.json` by the following keys:
-```json
+### 3. Use locale strings in Actions
+
+```python title="counter.py" hl_lines="27-28"
+# Import StreamController modules
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
+
+# Import gtk
+import gi
+gi.require_version("Gtk", "4.0")
+gi.require_version("Adw", "1")
+from gi.repository import Gtk, Adw
+
+class Counter(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
+
+ self.counter: int = 0
+
+ def get_config_rows(self) -> list:
+ self.spinner = Adw.SpinRow.new_with_range(1, 100, 1)
+ self.spinner.set_title(self.plugin_base.lm.get("actions.counter.spinner.title"))
+ self.spinner.set_subtitle(self.plugin_base.lm.get("actions.counter.spinner.subtitle"))
+
+ self.load_config_values()
+ self.spinner.connect("changed", self.on_spinner_value_changed)
+
+ return [self.spinner]
+
+ # ... rest of the action code
+```
+
+### 4. Using GenerativeUI with Localization
+
+[GenerativeUI](../bases/GenerativeUI.md) widgets automatically look up translations for their titles:
+
+```python
+from GtkHelper.GenerativeUI.SpinRow import SpinRow
+
+self.increment_row = SpinRow(
+ action_core=self,
+ var_name="increment_by",
+ default_value=1,
+ title="actions.counter.spinner.title", # Translation key
+ min=1, max=100, step=1
+)
+```
+
+The widget will call `get_translation("actions.counter.spinner.title")` automatically.
+
+## Adding More Languages
+
+### CSV Format
+
+Simply add more columns:
+
+```csv
+key;en_US;de_DE;fr_FR;es_ES
+plugin.name;My Plugin;Mein Plugin;Mon Plugin;Mi Plugin
+```
+
+### JSON Format
+
+Create additional JSON files:
+
+```json title="de_DE.json"
{
- "actions.counter.spinner.title": "Increment by",
- "actions.counter.spinner.subtitle": "How much to increment the counter by"
+ "plugin.name": "Mein Plugin",
+ "actions.counter.name": "Zähler"
}
```
-Now we have to modify `counter.py` in order to use the new keys:
-```python title="counter.py" hl_lines="31-32"
+
+## Complete Example
+
+```python title="counter.py"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase
-from src.backend.DeckManagement.DeckController import DeckController
-from src.backend.PageManagement.Page import Page
-from src.backend.PluginManager.PluginBase import PluginBase
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
# Import gtk
import gi
@@ -117,9 +222,16 @@ gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw
-class Counter(ActionBase):
+class Counter(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+
+ self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
self.counter: int = 0
@@ -150,4 +262,4 @@ class Counter(ActionBase):
settings = self.get_settings()
settings["increment_by"] = int(spinner.get_value())
self.set_settings(settings)
-```
\ No newline at end of file
+```
diff --git a/docs/plugin_dev/plugin_template/SimpleAction_py.md b/docs/plugin_dev/plugin_template/SimpleAction_py.md
index fd2c7bc..19573e4 100644
--- a/docs/plugin_dev/plugin_template/SimpleAction_py.md
+++ b/docs/plugin_dev/plugin_template/SimpleAction_py.md
@@ -1,41 +1,128 @@
```python title="SimpleAction.py"
# Import StreamController modules
-from src.backend.PluginManager.ActionBase import ActionBase #(1)!
-from src.backend.DeckManagement.DeckController import DeckController #(2)!
-from src.backend.PageManagement.Page import Page #(3)!
-from src.backend.PluginManager.PluginBase import PluginBase #(4)!
+from src.backend.PluginManager.ActionCore import ActionCore #(1)!
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
# Import python modules
import os
-# Import gtk modules - used for the config rows
-import gi
-gi.require_version("Gtk", "4.0") #(5)!
-gi.require_version("Adw", "1") #(6)!
-from gi.repository import Gtk, Adw #(7)!
-
-class SimpleAction(ActionBase):
+class SimpleAction(ActionCore):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+ self.add_event_assigner(EventAssigner( #(2)!
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+ ))
+
+ self.add_event_assigner(EventAssigner(
+ id="key-up",
+ ui_label="Key Up",
+ default_event=Input.Key.Events.UP,
+ callback=self.on_key_up
+ ))
+
def on_ready(self) -> None:
icon_path = os.path.join(self.plugin_base.PATH, "assets", "info.png")
- self.set_media(media_path=icon_path, size=0.75) #(8)!
+ self.set_media(media_path=icon_path, size=0.75) #(3)!
def on_key_down(self) -> None:
- print("Key down") #(9)!
+ print("Key down") #(4)!
def on_key_up(self) -> None:
- print("Key up") #(10)!
+ print("Key up") #(5)!
```
-1. Import the [ActionBase](../bases/ActionBase_py.md) class
-2. Import the [DeckController](../advanced_concepts/DeckController.md) class - just used for typing
-3. Import the Page class - just used for typing
-4. Import the [PluginBase](../bases/PluginBase_py.md) class - just used for typing
-5. Set the [GTK](https://www.gtk.org) version to [4.0](https://docs.gtk.org/gtk4/)
-6. Set the [Adw](https://www.gtk.org) version to [1](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/)
-7. Import [GTK](https://www.gtk.org) and [Adw](https://www.gtk.org)
-8. Set an icon for the action
-9. Print "Key down" if the key is pressed
-10. Print "Key up" if the key is released
\ No newline at end of file
+1. Import [ActionCore](../bases/ActionCore_py.md) and [EventAssigner](../bases/EventSystem.md) for event handling
+2. Register event handlers using `add_event_assigner()` - this maps input events to callbacks
+3. Set an icon for the action in `on_ready()` (not in `__init__`)
+4. Called when the key is pressed
+5. Called when the key is released
+
+## Understanding the Code
+
+### Imports
+
+```python
+from src.backend.PluginManager.ActionCore import ActionCore
+from src.backend.PluginManager.EventAssigner import EventAssigner
+from src.backend.DeckManagement.InputIdentifier import Input
+```
+
+- [ActionCore](../bases/ActionCore_py.md) is the base class for all actions
+- [EventAssigner](../bases/EventSystem.md) maps input events to callback functions
+- `Input` provides constants for input types and events
+
+### The Action Class
+
+```python
+class SimpleAction(ActionCore):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+```
+
+Your action extends `ActionCore`. The `*args, **kwargs` pattern lets the framework pass required parameters without you needing to specify them.
+
+### Registering Event Handlers
+
+```python
+self.add_event_assigner(EventAssigner(
+ id="key-down",
+ ui_label="Key Down",
+ default_event=Input.Key.Events.DOWN,
+ callback=self.on_key_down
+))
+```
+
+Use `add_event_assigner()` to register callbacks for input events. Each `EventAssigner` needs:
+
+| Parameter | Description |
+|-----------|-------------|
+| `id` | Unique identifier for this event handler |
+| `ui_label` | Label shown in the UI |
+| `default_event` | The input event to respond to |
+| `callback` | Function to call when event fires |
+
+### Setting the Icon
+
+```python
+def on_ready(self) -> None:
+ icon_path = os.path.join(self.plugin_base.PATH, "assets", "info.png")
+ self.set_media(media_path=icon_path, size=0.75)
+```
+
+!!! warning
+ Always set images in `on_ready()`, not `__init__()`. The deck isn't ready to process image changes during construction.
+
+You can also use the helper method:
+```python
+def on_ready(self) -> None:
+ self.set_media(media_path=self.get_asset_path("info.png"), size=0.75)
+```
+
+### Handling Events
+
+```python
+def on_key_down(self) -> None:
+ print("Key down")
+
+def on_key_up(self) -> None:
+ print("Key up")
+```
+
+These methods are called when the corresponding `EventAssigner` triggers.
+
+### Common Key Events
+
+| Event | Description |
+|-------|-------------|
+| `Input.Key.Events.DOWN` | Key pressed |
+| `Input.Key.Events.UP` | Key released |
+| `Input.Key.Events.SHORT_UP` | Quick tap (no hold) |
+| `Input.Key.Events.HOLD_START` | Hold begins |
+| `Input.Key.Events.HOLD_STOP` | Hold ends |
+
+See [Event System](../bases/EventSystem.md) for more details on available events and dial/touchscreen support.
diff --git a/docs/plugin_dev/plugin_template/main_py.md b/docs/plugin_dev/plugin_template/main_py.md
index 9d528dd..4a1fc27 100644
--- a/docs/plugin_dev/plugin_template/main_py.md
+++ b/docs/plugin_dev/plugin_template/main_py.md
@@ -1,5 +1,5 @@
The [main.py](main_py.md) is the main file of your plugin and will be executed when the plugin is loaded.
-Therefore this is the place where you can add your actions to your plugin.
+Therefore this is the place where you register your actions with the plugin.
Per default the file looks like this:
@@ -7,38 +7,48 @@ Per default the file looks like this:
# Import StreamController modules
from src.backend.PluginManager.PluginBase import PluginBase #(1)!
from src.backend.PluginManager.ActionHolder import ActionHolder #(2)!
+from src.backend.PluginManager.ActionInputSupport import ActionInputSupport #(3)!
+from src.backend.DeckManagement.InputIdentifier import Input #(4)!
# Import actions
-from .actions.SimpleAction.SimpleAction import SimpleAction #(3)!
+from .actions.SimpleAction.SimpleAction import SimpleAction #(5)!
class PluginTemplate(PluginBase):
def __init__(self):
super().__init__()
## Register actions
- self.simple_action_holder = ActionHolder( #(4)!
- plugin_base = self,
- action_base = SimpleAction,
- action_id = "dev_core447_Template::SimpleAction", # Change this to your own plugin id
- action_name = "Simple Action",
+ self.simple_action_holder = ActionHolder( #(6)!
+ plugin_base=self,
+ action_core=SimpleAction,
+ action_id_suffix="SimpleAction", # Results in: com_example_Template::SimpleAction
+ action_name="Simple Action",
+ action_support={ #(7)!
+ Input.Key: ActionInputSupport.SUPPORTED,
+ Input.Dial: ActionInputSupport.UNSUPPORTED,
+ Input.Touchscreen: ActionInputSupport.UNSUPPORTED
+ }
)
- self.add_action_holder(self.simple_action_holder) #(5)!
+ self.add_action_holder(self.simple_action_holder) #(8)!
# Register plugin
self.register(
- plugin_name = "Template",
- github_repo = "https://github.com/StreamController/PluginTemplate",
- plugin_version = "1.0.0",
- app_version = "1.1.1-alpha"
- ) #(5)!
+ plugin_name="Template",
+ github_repo="https://github.com/StreamController/PluginTemplate",
+ plugin_version="1.0.0",
+ app_version="1.5.0-beta.9"
+ ) #(9)!
```
1. Import the [PluginBase](../bases/PluginBase_py.md) class which is the base for all plugins.
-2. Import the `ActionHolder` class which holds [ActionBases] until creation.
-3. Import the [SimpleAction]() which is a sample action with no backend.
-4. Create a new [ActionHolder]() class for the [SimpleAction] action.
-5. Add the [ActionHolder]() to the plugin.
-6. Register the plugin.
+2. Import the `ActionHolder` class which holds action metadata until instantiation.
+3. Import `ActionInputSupport` to declare which input types your action supports.
+4. Import `Input` for input type constants.
+5. Import the [SimpleAction](SimpleAction_py.md) which is a sample action.
+6. Create a new `ActionHolder` for the SimpleAction.
+7. Declare which input types this action supports.
+8. Add the `ActionHolder` to the plugin.
+9. Register the plugin.
## Let's go over the code:
@@ -47,42 +57,81 @@ class PluginTemplate(PluginBase):
```python
from src.backend.PluginManager.PluginBase import PluginBase
```
-imports the [PluginBase]() which is the base class for all plugins.
+imports the [PluginBase](../bases/PluginBase_py.md) which is the base class for all plugins.
```python
from src.backend.PluginManager.ActionHolder import ActionHolder
```
-imports the [ActionHolder]() which holds [ActionBases] until creation.
+imports the `ActionHolder` which holds action classes and their metadata until they need to be instantiated.
+
+```python
+from src.backend.PluginManager.ActionInputSupport import ActionInputSupport
+from src.backend.DeckManagement.InputIdentifier import Input
+```
+imports the types needed to declare input support for your actions.
### Import actions
```python
from .actions.SimpleAction.SimpleAction import SimpleAction
```
imports the [SimpleAction](SimpleAction_py.md).
-This is the sample action with no backend.
### The plugin class
```python
self.simple_action_holder = ActionHolder(
- plugin_base = self,
- action_base = SimpleAction,
- action_id = "dev_core447_Template::SimpleAction", # Change this to your own plugin id
- action_name = "Simple Action",
- )
+ plugin_base=self,
+ action_core=SimpleAction,
+ action_id_suffix="SimpleAction",
+ action_name="Simple Action",
+ action_support={
+ Input.Key: ActionInputSupport.SUPPORTED,
+ Input.Dial: ActionInputSupport.UNSUPPORTED,
+ Input.Touchscreen: ActionInputSupport.UNSUPPORTED
+ }
+)
```
-creates an [ActionHolder]() for the [SimpleAction] action.
+creates an `ActionHolder` for the SimpleAction.
+
+| Parameter | Description |
+|-----------|-------------|
+| `plugin_base` | Reference to your plugin |
+| `action_core` | The action class (extends [ActionCore](../bases/ActionCore_py.md)) |
+| `action_id_suffix` | Unique suffix for the action ID (combined with plugin ID) |
+| `action_name` | Human-readable name shown in the UI |
+| `action_support` | Dict declaring which input types are supported |
+
+!!! tip
+ You can also use `action_id` instead of `action_id_suffix` if you want full control over the action ID. The format should be: `{reverse_domain_with_underscores}::{action_name}`
+
+### Input Support
+
+The `action_support` dict tells StreamController which input types your action works with:
+
+| Value | Meaning |
+|-------|---------|
+| `ActionInputSupport.SUPPORTED` | Fully tested and working |
+| `ActionInputSupport.UNTESTED` | May work, not tested (default if omitted) |
+| `ActionInputSupport.UNSUPPORTED` | Does not work with this input type |
```python
self.add_action_holder(self.simple_action_holder)
```
-adds the [ActionHolder]() to the plugin.
+adds the `ActionHolder` to the plugin, making the action available in the UI.
+### Plugin Registration
```python
self.register(
- plugin_name = "Template",
- github_repo = "https://github.com/StreamController/PluginTemplate",
- plugin_version = "1.0.0",
- app_version = "1.1.1-alpha"
- )
+ plugin_name="Template",
+ github_repo="https://github.com/StreamController/PluginTemplate",
+ plugin_version="1.0.0",
+ app_version="1.5.0-beta.9"
+)
```
-registers the plugin. See [register](../bases/PluginBase_py.md#register).
\ No newline at end of file
+registers the plugin with StreamController. See [register](../bases/PluginBase_py.md#register).
+
+| Parameter | Description |
+|-----------|-------------|
+| `plugin_name` | Display name (can be localized) |
+| `github_repo` | Link to your repository |
+| `plugin_version` | Your plugin's version |
+| `app_version` | Minimum StreamController version required |
diff --git a/mkdocs.yml b/mkdocs.yml
index 8aa964a..2caeb80 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -12,7 +12,9 @@ nav:
- 1. The Idea: plugin_dev/idea.md
- 2. Setting Up Your Environment: plugin_dev/setup.md
- 3. Introduction To Bases:
- - ActionBase.py: plugin_dev/bases/ActionBase_py.md
+ - ActionCore.py: plugin_dev/bases/ActionCore_py.md
+ - Event System: plugin_dev/bases/EventSystem.md
+ - GenerativeUI: plugin_dev/bases/GenerativeUI.md
- BackendBase.py: plugin_dev/bases/BackendBase_py.md
- PluginBase.py: plugin_dev/bases/PluginBase_py.md
- 4. The Plugin Template: