]> skyeroc.xyz Git - obs-ctl/commitdiff
Generalize the settings panes
authorsbkelley <sb24kelley@gmail.com>
Fri, 5 Dec 2025 22:45:15 +0000 (17:45 -0500)
committersbkelley <sb24kelley@gmail.com>
Fri, 5 Dec 2025 22:45:15 +0000 (17:45 -0500)
notes.txt
src/obs_ctl/app.py
src/obs_ctl/interface/molecule/settings_block/__init__.py
src/obs_ctl/interface/molecule/settings_block/pane.py [new file with mode: 0644]
src/obs_ctl/interface/molecule/settings_block/pane/__init__.py [deleted file]
src/obs_ctl/interface/molecule/settings_block/pane/obs_settings.py [deleted file]
src/obs_ctl/interface/molecule/settings_block/settings_block.py
src/obs_ctl/interface/obs/obs.py

index 8c357fac5edfe657bdf2e549beb7208a0d1c3724..ef1e579ff2763346ea8eefc09b08fe5c1c71585d 100644 (file)
--- 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()
index b20b448dde870c5994e2e98cfeaaf91e9ca7eada..8f314693b625a9957f39b8cd2b7075369ae936c6 100644 (file)
@@ -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
index fc1939d5cfdfe8be0501c618a07f297ae3fe911b..e414d1d413f312842b0385829476868383a1206f 100644 (file)
@@ -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 (file)
index 0000000..b7909ac
--- /dev/null
@@ -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 (file)
index df9cf12..0000000
+++ /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 (file)
index 14436ad..0000000
+++ /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
index 3ef0750019a87aa65326d15752e7e66cb71ea2a5..d8ef9778a1b57ab1811b6b432d66ed43f12c5dbe 100644 (file)
@@ -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")
index caf2d4c01db99e7e62de5181b1f87a42ecd754b2..6c77f6c1a10bb0eafe4a716ba64feccb113f9d27 100644 (file)
@@ -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