From 10370455152cdf00e974d5947a47a9441c6c42aa Mon Sep 17 00:00:00 2001 From: sbkelley Date: Fri, 5 Dec 2025 17:45:15 -0500 Subject: [PATCH] Generalize the settings panes --- notes.txt | 22 +++- src/obs_ctl/app.py | 13 +-- .../molecule/settings_block/__init__.py | 2 +- .../interface/molecule/settings_block/pane.py | 102 ++++++++++++++++++ .../molecule/settings_block/pane/__init__.py | 1 - .../settings_block/pane/obs_settings.py | 46 -------- .../molecule/settings_block/settings_block.py | 15 ++- src/obs_ctl/interface/obs/obs.py | 2 +- 8 files changed, 145 insertions(+), 58 deletions(-) create mode 100644 src/obs_ctl/interface/molecule/settings_block/pane.py delete mode 100644 src/obs_ctl/interface/molecule/settings_block/pane/__init__.py delete mode 100644 src/obs_ctl/interface/molecule/settings_block/pane/obs_settings.py diff --git a/notes.txt b/notes.txt index 8c357fa..ef1e579 100644 --- a/notes.txt +++ b/notes.txt @@ -15,4 +15,24 @@ Textual Widget Docstring template: # Parameters |pos/kw |name |type |description| |--- |--- |--- |--- | -""" \ No newline at end of file +""" + + +Panes +OBS Settings + Record path +OBS-WS Connection + Host + Port + Password + Timeout + + +create_pane(settings={setting_name:setting_type, [...])) -> TabPane + + +for each setting_name, we need also a reactive attribute and a message for when it changes + +pane_factory() + reactive_factory() + on_input_submitted_factory() diff --git a/src/obs_ctl/app.py b/src/obs_ctl/app.py index b20b448..8f31469 100644 --- a/src/obs_ctl/app.py +++ b/src/obs_ctl/app.py @@ -4,7 +4,7 @@ from logging import getLogger from textual import on from textual.app import App from textual.containers import HorizontalGroup, VerticalGroup, ScrollableContainer -from textual.widgets import Footer, Header +from textual.widgets import Footer, Header, Input # need to import log to initalize logging from . import data, log @@ -39,6 +39,8 @@ class OBSCtlApp(App): @on(obs.API.Report, "#api") def on_api_report(self, message: obs.API.Report): self.update_monitors(message.data) + + # @on(obs.API.Report, "#api")(on_api_report) @on(RecordControls.RecordStarted, "#record_controls") @@ -85,16 +87,15 @@ class OBSCtlApp(App): api: obs.API = self.query_exactly_one("#api") getattr(api, query)(*args, **kwargs) - - @on(settings_block.pane.OBSSettings.RecordPathChanged, "#settings-content") - def on_settings_record_path_changed(self, message: settings_block.pane.OBSSettings.RecordPathChanged): + @on(settings_block.OBSSettingsPane.RecordPathChanged, "#settings-content") + def on_settings_block_record_path_changed(self, message): api: obs.API = self.query_exactly_one("#api") - api.set_record_path(message.record_path) + api.set_record_path(message.value) def update_monitors(self, obs_stats: obs.OBSStats): record_controls: RecordControls = self.query_exactly_one("#record_controls") - obs_settings: settings_block.pane.OBSSettings.RecordPathChanged = self.query_exactly_one("#settings-content") + obs_settings = self.query_exactly_one("#settings-content") stats_display: StatsDisplay = self.query_exactly_one("#stats_display") record_controls.obsws_connected = obs_stats.obsws_connection_active diff --git a/src/obs_ctl/interface/molecule/settings_block/__init__.py b/src/obs_ctl/interface/molecule/settings_block/__init__.py index fc1939d..e414d1d 100644 --- a/src/obs_ctl/interface/molecule/settings_block/__init__.py +++ b/src/obs_ctl/interface/molecule/settings_block/__init__.py @@ -1 +1 @@ -from .settings_block import SettingsBlock \ No newline at end of file +from .settings_block import SettingsBlock, OBSSettingsPane \ No newline at end of file diff --git a/src/obs_ctl/interface/molecule/settings_block/pane.py b/src/obs_ctl/interface/molecule/settings_block/pane.py new file mode 100644 index 0000000..b7909ac --- /dev/null +++ b/src/obs_ctl/interface/molecule/settings_block/pane.py @@ -0,0 +1,102 @@ +from enum import Enum +from functools import partial +from logging import getLogger +from typing import Any +from textual import on +from textual.containers import ScrollableContainer, Grid +from textual.reactive import reactive +from textual.widgets import TabPane, Input, Label +from textual.widget import Widget +from ...message import MSGBase + +logger = getLogger(__name__) +class SettingsMSGBase(MSGBase): + value = None + + def __init__(self, control, value, *, _log_me=True): + super().__init__(control, _log_me=_log_me) + self.value = value + +class SettingsType(Enum): + STRING = (partial(Input, compact=True), str, Input.Submitted) + FLOAT = (partial(Input, compact=True), float, Input.Submitted) + INT = (partial(Input, compact=True), int, Input.Submitted) + + @property + def widget(self): + return self.value[0] + + @property + def type(self): + return self.value[1] + + @property + def msg(self): + return self.value[2] + + +def pane_factory(cls_name: str, settings_list: list, settings_type_list: list, defaults_list) -> TabPane: + """Creates a new TabPane subclass with cls_name as its name. + + settings_list should be a list of names for settings. + settings_type_list should be a list of pane.SettingsType members + settings_default_list should be a list of defaults, for the settings. + + Returned widget will have appropriate messages for each input field. + + For example, pane_factory("Settings", ["record_path"], [pane.SettingsType.STRING], [""]) + will return a TabPane with a Label and an Input box, a reactive attribute "record_path", + a watch_record_path function to update the Input value, and an on_record_path_changed + function, which emits a RecordPathChanged message containing a value when the user + submits the input.""" + DEFAULT_CSS = """ + Grid { + grid-size:2; + grid-rows:2; + } + """ + def watch_func_factory(watch_id: str): + def watch_func(self, value): + input = self.query_exactly_one(watch_id) + input.value = value + + return watch_func + + def on_func_factory(reactive, msg_type, post_msg, _ctrl_id): + def on_func(self, event): + self.set_reactive(reactive, event.value) + self.post_message(post_msg(self, event.value)) + + return on(msg_type, _ctrl_id)(on_func) + + def compose_factory(): + def output_func(self): + with ScrollableContainer(): + with Grid(): + for setting, control_and_type, setting_default in zip(settings_list, settings_type_list, defaults_list): + ctrl_id = f"{settings_type.widget.func.__name__.lower()}_{setting}" + yield Label(setting) + yield control_and_type.widget(value=setting_default, id=ctrl_id) + + return output_func + + new_dict = dict() + for setting, control_and_type, setting_default in zip(settings_list, settings_type_list, defaults_list): + settings_type: SettingsType = control_and_type + ctrl_id = f"{settings_type.widget.func.__name__.lower()}_{setting}" + msg_name = f"{snake_to_camel(setting)}Changed" + watch_x = f"watch_{setting}" + on_x = f"on_{setting}_changed" + new_dict[setting] = reactive(settings_type.type(setting_default)) # eg. record_path + new_dict[watch_x] = watch_func_factory(f"#{ctrl_id}") # watch_record_path + new_dict[msg_name] = type(msg_name, (SettingsMSGBase, ), {}) # RecordPathChanged + new_dict[on_x] = on_func_factory(new_dict[setting], settings_type.msg, new_dict[msg_name], f"#{ctrl_id}") # on_record_path_changed + new_dict["DEFAULT_CSS"] = DEFAULT_CSS + new_dict["compose"] = compose_factory() # Adds all the Inputs + + new_pane = type(cls_name, (TabPane,), new_dict) + + return new_pane + +def snake_to_camel(string: str): + return str.join("", [str.capitalize(x) for x in string.split("_")]) \ No newline at end of file diff --git a/src/obs_ctl/interface/molecule/settings_block/pane/__init__.py b/src/obs_ctl/interface/molecule/settings_block/pane/__init__.py deleted file mode 100644 index df9cf12..0000000 --- a/src/obs_ctl/interface/molecule/settings_block/pane/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .obs_settings import OBSSettings \ No newline at end of file diff --git a/src/obs_ctl/interface/molecule/settings_block/pane/obs_settings.py b/src/obs_ctl/interface/molecule/settings_block/pane/obs_settings.py deleted file mode 100644 index 14436ad..0000000 --- a/src/obs_ctl/interface/molecule/settings_block/pane/obs_settings.py +++ /dev/null @@ -1,46 +0,0 @@ -from textual.containers import ScrollableContainer, Grid -from textual.message import Message -from textual.reactive import reactive -from textual.widgets import Label, Input, TabPane - -from logging import getLogger - -logger = getLogger(__name__) - -class OBSSettings(TabPane): - COMPONENT_CLASSES = { - "obs_settings_pane--input", - "obs_settings_pane--output", - "obs_settings_pane--content", - "obs_settings_pane--scrollbox" - } - record_path: reactive[str] = reactive("") - - class RecordPathChanged(Message): - control: "OBSSettings" = None - record_path: str = None - def __init__(self, ctrl, path): - super().__init__() - self.control = ctrl - self.record_path = path - - - def __init__(self, *args, **kwargs): - super().__init__("OBS Settings", *args, **kwargs) - - - def compose(self): - with ScrollableContainer(classes="obs_settings_pane--scrollbox"): - with Grid(classes="obs_settings_pane--content"): - yield Label("Record path:", classes="obs_settings_pane--label") - yield Input("path", id="input_record_path", classes="obs_settings_pane--input", compact=True) - - - def on_input_submitted(self, event:Input.Submitted): - self.set_reactive(OBSSettings.record_path, event.value) - if event.control.id == "input_record_path": - self.post_message(OBSSettings.RecordPathChanged(self, event.value)) - - def watch_record_path(self, value): - input: Input = self.query_exactly_one("#input_record_path") - input.value = value diff --git a/src/obs_ctl/interface/molecule/settings_block/settings_block.py b/src/obs_ctl/interface/molecule/settings_block/settings_block.py index 3ef0750..d8ef977 100644 --- a/src/obs_ctl/interface/molecule/settings_block/settings_block.py +++ b/src/obs_ctl/interface/molecule/settings_block/settings_block.py @@ -1,14 +1,25 @@ +from logging import getLogger +from textual import on from textual.widgets import TabbedContent, TabPane, Placeholder from textual.containers import Container from . import pane +logger = getLogger(__name__) + +OBSSettingsPane = pane.pane_factory( + "OBSSettingsPane", + ["record_path"], + [pane.SettingsType.STRING, pane.SettingsType.STRING], + [""] +) class SettingsBlock(Container): def compose(self): with TabbedContent(classes="settings_block--tabbed-content"): - yield pane.OBSSettings(id="settings-content", classes="settings_block--obs_settings") + yield OBSSettingsPane("OBS Settings", id="settings-content") + # yield pane.OBSSettings(id="settings-content", classes="settings_block--obs_settings") with TabPane("OBS Connection", id="connection-tab"): yield Placeholder("CONNECTION BLOCK", id="conection-plc") with TabPane("obs-ctl Settings", id="clients-tab"): - yield Placeholder("CLIENT SETTINGS BLOCK", id="client-plc") \ No newline at end of file + yield Placeholder("CLIENT SETTINGS BLOCK", id="client-plc") diff --git a/src/obs_ctl/interface/obs/obs.py b/src/obs_ctl/interface/obs/obs.py index caf2d4c..6c77f6c 100644 --- a/src/obs_ctl/interface/obs/obs.py +++ b/src/obs_ctl/interface/obs/obs.py @@ -364,7 +364,7 @@ class API(Static): name = request.func.__name__ else: name = request.__name__ - logger.info(f"Sending request: {name}") + # logger.info(f"Sending request: {name}") output = None _sentry = 3 -- 2.47.3