diff --git a/actions/ToggleFilter/ToggleFilter.py b/actions/ToggleFilter/ToggleFilter.py new file mode 100644 index 0000000..a4340dd --- /dev/null +++ b/actions/ToggleFilter/ToggleFilter.py @@ -0,0 +1,217 @@ +from plugins.com_core447_OBSPlugin.OBSActionBase import OBSActionBase +from src.backend.DeckManagement.DeckController import DeckController +from src.backend.PageManagement.Page import Page +from src.backend.PluginManager.PluginBase import PluginBase +from GtkHelper.GtkHelper import ComboRow + +import os +import threading + +# Import gtk modules +import gi +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Gtk, Adw + +class ToggleFilter(OBSActionBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.current_state = -1 + + def on_ready(self): + self.current_state = -1 + # Connect to obs if not connected + if self.plugin_base.backend is not None: + if not self.plugin_base.get_connected(): + self.reconnect_obs() + + # Show current scene filter status + threading.Thread(target=self.show_current_filter_status, daemon=True, name="show_current_filter_status").start() + + def show_current_filter_status(self, new_paused = False): + if self.plugin_base.backend is None: + self.current_state = -1 + self.show_error() + return + if not self.plugin_base.backend.get_connected(): + self.current_state = -1 + self.show_error() + return + if not self.get_settings().get("filter"): + self.current_state = -1 + self.show_error() + return + + status = self.plugin_base.backend.get_source_filter(self.get_settings().get("scene"), self.get_settings().get("filter")) + if status is None: + self.current_state = -1 + self.show_error() + return + if status["filterEnabled"]: + self.show_for_state(1) + else: + self.show_for_state(0) + + def show_for_state(self, state: int): + """ + 0: Filter disabled + 1: Filter enabled + """ + if state == self.current_state: + return + + self.current_state = state + image = "scene_item_disabled.png" + + if state == -1: + self.show_error() + else: + self.hide_error() + + if state == 0: + image = "scene_item_disabled.png" + elif state == 1: + image = "scene_item_enabled.png" + + self.set_media(media_path=os.path.join(self.plugin_base.PATH, "assets", image), size=0.75) + + def get_config_rows(self) -> list: + super_rows = super().get_config_rows() + + self.scene_model = Gtk.StringList() + self.scene_row = Adw.ComboRow(model=self.scene_model, title=self.plugin_base.lm.get("actions.switch.scene-row.label")) + + self.filter_model = Gtk.StringList() + self.filter_row = Adw.ComboRow(model=self.filter_model, title=self.plugin_base.lm.get("actions.toggle-scene-filter-enabled-row.label")) + + self.connect_signals() + + self.load_filter_model() + self.load_configs() + + super_rows.append(self.scene_row) + super_rows.append(self.filter_row) + return super_rows + + def connect_signals(self): + self.scene_row.connect("notify::selected", self.on_scene_selected) + self.filter_row.connect("notify::selected", self.on_filter_selected) + + def disconnect_signals(self): + try: + self.scene_row.disconnect_by_func(self.on_scene_selected) + self.filter_row.disconnect_by_func(self.on_filter_selected) + except TypeError as e: + pass + + def load_filter_model(self): + self.disconnect_signals() + # Clear model + while self.scene_model.get_n_items() > 0: + self.scene_model.remove(0) + while self.filter_model.get_n_items() > 0: + self.filter_model.remove(0) + + # Load model + if self.plugin_base.backend.get_connected(): + scenes = self.plugin_base.backend.get_scene_names() + if scenes is None: + return + for scene in scenes: + self.scene_model.append(scene) + # Ensure selection is made if there's only one scene + if len(scenes) == 1: + self.get_settings()["scene"] = scenes[0] + self.scene_row.set_selected(0) + self.load_filters_for_scene(scenes[0]) + + self.connect_signals() + + def load_filters_for_scene(self, scene_name): + # Clear filters model + while self.filter_model.get_n_items() > 0: + self.filter_model.remove(0) + + if self.plugin_base.backend.get_connected(): + filters = self.plugin_base.backend.get_source_filters(scene_name) + if filters is None: + self.show_error() + return + for item in filters: + self.filter_model.append(item.get("filterName")) + # Ensure selection is made if there's only one item + if len(filters) == 1: + self.filter_row.set_selected(0) + + def load_configs(self): + self.load_config_values() + + def load_config_values(self): + self.disconnect_signals() + settings = self.get_settings() + for i, scene_name in enumerate(self.scene_model): + if scene_name.get_string() == settings.get("scene"): + self.scene_row.set_selected(i) + self.load_filters_for_scene(scene_name.get_string()) + for j, item_name in enumerate(self.filter_model): + if item_name.get_string() == settings.get("filter"): + self.filter_row.set_selected(j) + break + self.connect_signals() + return + + self.scene_row.set_selected(Gtk.INVALID_LIST_POSITION) + self.filter_row.set_selected(Gtk.INVALID_LIST_POSITION) + self.connect_signals() + + def on_scene_selected(self, *args): + settings = self.get_settings() + selected_index_scene = self.scene_row.get_selected() + if selected_index_scene != Gtk.INVALID_LIST_POSITION: + scene_name = self.scene_model[selected_index_scene].get_string() + settings["scene"] = scene_name + self.set_settings(settings) + self.load_filters_for_scene(scene_name) + + def on_filter_selected(self, *args): + settings = self.get_settings() + selected_index_item = self.filter_row.get_selected() + if selected_index_item != Gtk.INVALID_LIST_POSITION: + settings["filter"] = self.filter_model[selected_index_item].get_string() + self.set_settings(settings) + + def on_key_down(self): + if self.plugin_base.backend is None: + self.current_state = -1 + self.show_error() + self.set_media(media_path=os.path.join(self.plugin_base.PATH, "assets", "error.png")) + return + if not self.plugin_base.backend.get_connected(): + self.current_state = -1 + self.show_error() + self.set_media(media_path=os.path.join(self.plugin_base.PATH, "assets", "error.png")) + return + + scene_name = self.get_settings().get("scene") + filter_name = self.get_settings().get("filter") + if scene_name in [None, ""]: + self.set_media(media_path=os.path.join(self.plugin_base.PATH, "assets", "error.png")) + return + if filter_name in [None, ""]: + self.set_media(media_path=os.path.join(self.plugin_base.PATH, "assets", "error.png")) + return + + if self.current_state == 0: + self.plugin_base.backend.set_source_filter_enabled(scene_name, filter_name, True) + else: + self.plugin_base.backend.set_source_filter_enabled(scene_name, filter_name, False) + self.on_tick() + + def on_tick(self): + self.show_current_filter_status() + + def reconnect_obs(self): + super().reconnect_obs() + if hasattr(self, "scene_model") and hasattr(self, "filter_model"): + self.load_filter_model() + self.load_configs() \ No newline at end of file diff --git a/backend/OBSController.py b/backend/OBSController.py index 6a39ab4..9e6cd25 100644 --- a/backend/OBSController.py +++ b/backend/OBSController.py @@ -333,5 +333,25 @@ class OBSController(obsws): def set_current_scene_collection(self, sceneCollectionName: str) -> None: try: self.call(requests.SetCurrentSceneCollection(sceneCollectionName=sceneCollectionName)) + except (obswebsocket.exceptions.MessageTimeout, websocket._exceptions.WebSocketConnectionClosedException, KeyError) as e: + log.error(e) + + def get_source_filters(self, sourceName: str) -> list: + try: + source_filters = self.call(requests.GetSourceFilterList(sourceName=sourceName)).getfilters() + return source_filters + except (obswebsocket.exceptions.MessageTimeout, websocket._exceptions.WebSocketConnectionClosedException, KeyError) as e: + log.error(e) + + def set_source_filter_enabled(self, sourceName: str, filterName: str, enabled: bool) -> None: + try: + self.call(requests.SetSourceFilterEnabled(sourceName=sourceName, filterName=filterName, filterEnabled=enabled)) + except (obswebsocket.exceptions.MessageTimeout, websocket._exceptions.WebSocketConnectionClosedException, KeyError) as e: + log.error(e) + + def get_source_filter(self, sourceName: str, filterName: str) -> None: + try: + source_filter = self.call(requests.GetSourceFilter(sourceName=sourceName, filterName=filterName)).datain + return source_filter except (obswebsocket.exceptions.MessageTimeout, websocket._exceptions.WebSocketConnectionClosedException, KeyError) as e: log.error(e) \ No newline at end of file diff --git a/backend/backend.py b/backend/backend.py index 3102e97..e14eafc 100644 --- a/backend/backend.py +++ b/backend/backend.py @@ -185,4 +185,13 @@ class Backend(BackendBase): def set_current_scene_collection(self, sceneCollectionName: str): return self.OBSController.set_current_scene_collection(sceneCollectionName) + def get_source_filters(self, sourceName: str) -> list: + return self.OBSController.get_source_filters(sourceName) + + def set_source_filter_enabled(self, sourceName: str, filterName: str, enabled: bool): + self.OBSController.set_source_filter_enabled(sourceName, filterName, enabled) + + def get_source_filter(self, sourceName: str, filterName: str) -> None: + return self.OBSController.get_source_filter(sourceName, filterName) + backend = Backend() \ No newline at end of file diff --git a/locales/en_US.json b/locales/en_US.json index c5e164d..08d81fc 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -31,6 +31,9 @@ "actions.base.status.no-connection": "Could not connect to OBS.", "actions.base.status.connected": "Successfully connected to OBS.", + "actions.toggle-scene-filter-enabled-row.label": "Filter:", + "actions.toggle-filter.name": "Toggle Filter", + "actions.base.ip.label": "IP:", "actions.base.port.label": "Port:", "actions.base.password.label": "Password:" diff --git a/main.py b/main.py index 27d5f9d..f95bef9 100644 --- a/main.py +++ b/main.py @@ -39,6 +39,7 @@ from actions.InputDial.InputDial import InputDial from actions.SwitchScene.SwitchScene import SwitchScene from actions.ToggleSceneItemEnabled.ToggleSceneItemEnabled import ToggleSceneItemEnabled +from actions.ToggleFilter.ToggleFilter import ToggleFilter from actions.SwitchSceneCollection.SwitchSceneCollection import SwitchSceneCollection class OBS(PluginBase): @@ -239,6 +240,19 @@ class OBS(PluginBase): ) self.add_action_holder(switch_scene_collection_action_holder) + toggle_filter_holder = ActionHolder( + plugin_base=self, + action_base=ToggleFilter, + action_id_suffix="ToggleSceneFilter", + action_name=self.lm.get("actions.toggle-filter.name"), + action_support={ + Input.Key: ActionInputSupport.SUPPORTED, + Input.Dial: ActionInputSupport.SUPPORTED, + Input.Touchscreen: ActionInputSupport.SUPPORTED, + } + ) + self.add_action_holder(toggle_filter_holder) + # Load custom css self.add_css_stylesheet(os.path.join(self.PATH, "style.css"))