]> skyeroc.xyz Git - obs-ctl/commitdiff
Slowly making the pane factory more useful
authorsbkelley <sb24kelley@gmail.com>
Sun, 7 Dec 2025 15:19:04 +0000 (10:19 -0500)
committersbkelley <sb24kelley@gmail.com>
Sun, 7 Dec 2025 15:19:04 +0000 (10:19 -0500)
src/obs_ctl/interface/molecule/settings_block/pane.py
src/obs_ctl/interface/molecule/settings_block/pane.pyi [new file with mode: 0644]
src/obs_ctl/interface/molecule/settings_block/settings_block.py

index abf522ba7ff47f7f4842b0725285fb8292146109..1607f6de1a74f6cd4401519927c11031c09522e2 100644 (file)
@@ -1,11 +1,13 @@
 from enum import Enum
 from functools import partial
+from itertools import zip_longest
 from logging import getLogger
-from typing import Any
+from typing import Any, Mapping, Sequence, overload
 from textual import on
+from textual.app import ComposeResult
 from textual.containers import ScrollableContainer, Grid
 from textual.reactive import reactive
-from textual.widgets import TabPane, Input, Label
+from textual.widgets import TabPane, Input, Label, Select
 from ...message import MSGBase
 from textual.widget import Widget
 
@@ -17,45 +19,70 @@ class SettingsMSGBase(MSGBase):
         self.value = value
 
 class PaneBase(TabPane):
-    def disable_contents(self):
-        container = self.query_exactly_one(".--container")
-        container.disabled = True
+    contents_disabled: reactive[bool] = reactive(False)
+
+    def watch_contents_disabled(self, value):
+        self._set_contents_disabled_value(value)
+
+    def _disable_contents(self):
+        self._set_contents_disabled_value(True)
     
-    def enable_contents(self):
+    def _enable_contents(self):
+        self._set_contents_disabled_value(False)
+    
+    def _set_contents_disabled_value(self, value: bool):
         container = self.query_exactly_one(".--container")
-        container.disabled = False
+        container.disabled = value
 
-SettingsTypeWidget = partial[Input]
-SettingsTypeType = type[int | float | str]
-SettingsTypeMsg = type[Input.Submitted]
-SettingsTypeExpectedType = type[str]
-class SettingsType(Enum):
+type SettingsWidgetWidget = type[Input | Select[Any]]
+type SettingsWidgetType = type[int | float | str]
+type SettingsWidgetMsg = type[Input.Submitted | Select.Changed]
+type SettingsWidgetExpectedType = type[str]
+class SettingsWidget(Enum):
     """(widget_class, setting type, listener message, widget-expected type)"""
-    STRING = (partial(Input, compact=True), str, Input.Submitted, str)
-    FLOAT = (partial(Input, compact=True), float, Input.Submitted, str)
-    INT = (partial(Input, compact=True), int, Input.Submitted, str)
+    STRING = (Input, str, Input.Submitted, str)
+    FLOAT = (Input, float, Input.Submitted, str)
+    INT = (Input, int, Input.Submitted, str)
+    LIST = (Select, str, Select.Changed, str)
 
     @property
-    def widget(self) -> SettingsTypeWidget:
+    def widget(self) -> SettingsWidgetWidget:
         """The widget this SettingsType uses"""
         return self.value[0]
     
     @property
-    def type(self) -> SettingsTypeType:
+    def type(self) -> SettingsWidgetType:
         """The type of the reactive this SettingsType interacts with"""
         return self.value[1]
     
     @property
-    def msg(self) -> SettingsTypeMsg:
+    def msg(self) -> SettingsWidgetMsg:
         """The message this SettingsType's widget posts when user interacts"""
         return self.value[2]
     
     @property
-    def expected_type(self) -> SettingsTypeExpectedType:
+    def expected_type(self) -> SettingsWidgetExpectedType:
         """The type this SettingsType's widget expects for its value"""
         return self.value[3]
 
-def make_pane_subclass(cls_name: str, settings_list: list[str], settings_type_list: list[SettingsType], defaults_list: list[Any]) -> type[PaneBase]:
+
+type MakeWidgetOutputType = tuple[str, SettingsWidget, Any, ParamsType]
+def make_settings_widget(name: str, type: SettingsWidget, default: Any, *args, **kwargs) -> MakeWidgetOutputType:
+    return (name, type, default, (args, kwargs))
+
+
+type ArgsType = Sequence[Any]
+type KWArgsType = Mapping[str, Any]
+type ParamsType = tuple[ArgsType, KWArgsType] | None
+type ParamsListType = Sequence[ParamsType]
+type LoopListType = Sequence[MakeWidgetOutputType]
+def make_pane_subclass(
+        cls_name: str,
+        settings_name_list: list[str] | LoopListType,
+        settings_type_list: list[SettingsWidget] | None=None,
+        settings_default_list: list[Any] | None=None,
+        settings_type_params_list: ParamsListType | None=None,
+        ) -> type[PaneBase]:
     """Creates a new TabPane subclass with cls_name as its name.
     
      settings_list should be a list of names for settings.
@@ -70,7 +97,12 @@ def make_pane_subclass(cls_name: str, settings_list: list[str], settings_type_li
      function, which emits a RecordPathChanged message containing a value when the user
      submits the input."""
     row_height = 1
-    loop_list: list[tuple[str, SettingsType, Any]] = list(zip(settings_list, settings_type_list, defaults_list))
+    if settings_type_list is None and settings_default_list is None and settings_type_params_list is None:
+        loop_list: LoopListType = settings_name_list # type: ignore
+    else:
+        settings_type_params_list = settings_type_params_list if settings_type_params_list is not None else ( ((), dict()), )
+        loop_list: LoopListType = list(zip_longest(settings_name_list, settings_type_list, settings_default_list, settings_type_params_list)) # type: ignore
+
     DEFAULT_CSS = f"Grid {{grid-size:2;grid-rows:{row_height};min-height:{len(loop_list)*row_height}}}"
 
     def make_watch_reactive(watch_id: str):
@@ -81,27 +113,37 @@ def make_pane_subclass(cls_name: str, settings_list: list[str], settings_type_li
         return watch_func
 
 
-    def make_on_control_message(listener_message: SettingsTypeMsg, listener_id: str, reactive: reactive[Any], post_message: type[SettingsMSGBase]):
-        def on_func(self: PaneBase, event: SettingsTypeMsg):
+    def make_on_control_message(
+            listener_message: SettingsWidgetMsg,
+            listener_id: str,
+            reactive: reactive[Any],
+            post_message: type[SettingsMSGBase]):
+        def on_func(self: PaneBase, event: SettingsWidgetMsg):
             self.set_reactive(reactive, event.value)
             self.post_message(post_message(self, event.value))
         return on(listener_message, listener_id)(on_func)
 
 
     def make_compose():
-        def compose(self: PaneBase):
+        def compose(self: PaneBase) -> ComposeResult:
             with ScrollableContainer(classes="--container"):
                 with Grid():
-                    for setting, setting_type, setting_default in loop_list:
-                        ctrl_id = f"{setting_type.widget.func.__name__.lower()}_{setting}"
+                    for setting, setting_type, setting_default, param_spec in loop_list:
+                        # param_spec might be None if it was a short list or had to be padded
+                        param_spec = param_spec if param_spec is not None else ((), dict())
+                        ctrl_id = f"{setting_type.widget.__name__.lower()}_{setting}"
                         yield Label(setting)
-                        yield setting_type.widget(value=setting_type.expected_type(setting_default), id=ctrl_id)
+                        yield setting_type.widget(
+                            *param_spec[0],
+                            **param_spec[1],
+                            value=setting_type.expected_type(setting_default),
+                            id=ctrl_id)
         return compose
         
 
     new_dict: dict[str, Any] = dict()
-    for setting_name, setting_type, setting_default in loop_list:
-        control_setting_id: str = f"{setting_type.widget.func.__name__.lower()}_{setting_name}"
+    for setting_name, setting_type, setting_default, _ in loop_list:
+        control_setting_id: str = f"{setting_type.widget.__name__.lower()}_{setting_name}"
         SettingChanged_name: str = f"{snake_to_camel(setting_name)}Changed"
         watch_setting_name: str = f"watch_{setting_name}"
         on_setting_changed_name: str = f"on_{setting_name}_changed"
@@ -116,7 +158,7 @@ def make_pane_subclass(cls_name: str, settings_list: list[str], settings_type_li
         # Set up on_record_path_changed function to update record_path
         # and post RecordPathChanged
         new_dict[on_setting_changed_name] = make_on_control_message(
-            listener_message=setting_type.msg,
+            listener_message=setting_type.msg, 
             listener_id=f"#{control_setting_id}",
             reactive=new_dict[setting_name],
             post_message=new_dict[SettingChanged_name],
@@ -129,6 +171,6 @@ def make_pane_subclass(cls_name: str, settings_list: list[str], settings_type_li
     # Construct the actual class and return it
     new_pane: type[PaneBase] = type(cls_name, (PaneBase,), 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.pyi b/src/obs_ctl/interface/molecule/settings_block/pane.pyi
new file mode 100644 (file)
index 0000000..0bc115c
--- /dev/null
@@ -0,0 +1,50 @@
+from enum import Enum
+from typing import Any, ClassVar, Literal, Mapping, Self, Sequence, overload
+
+from textual.widgets import Input, Select, TabPane
+from textual.reactive import reactive
+
+type SettingsWidgetWidget = type[Input | Select[Any]]
+type SettingsWidgetType = type[int | float | str]
+type SettingsWidgetMsg = type[Input.Submitted | Select.Changed]
+type SettingsWidgetExpectedType = type[str]
+class SettingsWidget(Enum):
+    STRING: ClassVar[SettingsWidget]
+    FLOAT: ClassVar[SettingsWidget]
+    INT: ClassVar[SettingsWidget]
+    LIST: ClassVar[SettingsWidget]
+    @property
+    def widget(self: Self) -> SettingsWidgetWidget: ...
+    @property
+    def type(self: Self) -> SettingsWidgetType: ...
+    @property
+    def msg(self: Self) -> SettingsWidgetMsg: ...
+    @property
+    def expected_type(self: Self) -> SettingsWidgetExpectedType: ...
+
+
+class PaneBase(TabPane):
+    contents_disabled: ClassVar[reactive[bool]]
+    def watch_contents_disabled(self: Self, value: bool) -> None: ...
+
+
+type ArgsType = Sequence[Any]
+type KWArgsType = Mapping[str, Any]
+type ParamsType = tuple[ArgsType, KWArgsType]
+type ParamsListType = Sequence[ParamsType | None]
+@overload
+def make_pane_subclass(
+        cls_name: str,
+        settings_list: Sequence[str],
+        settings_type_list: Sequence[SettingsWidget],
+        defaults_list: Sequence[Any],
+        params_list: ParamsListType | None=None,
+        ) -> type[PaneBase]: ...
+@overload
+def make_pane_subclass(
+        cls_name: str,
+        settings_list: Sequence[MakeWidgetOutputType],
+        ) -> type[PaneBase]: ...
+
+type MakeWidgetOutputType = tuple[str, SettingsWidget, Any, ParamsType]
+def make_settings_widget(name: str, type: SettingsWidget, default: Any, *args: Any, **kwargs: Any) -> MakeWidgetOutputType: ...
\ No newline at end of file
index 0cb9520518bd8df5b5723ee8fd0b239e2c44fa33..f6aeb944f32a915d1a9ef39f3f51aed66aa5271d 100644 (file)
@@ -9,21 +9,25 @@ logger = getLogger(__name__)
 OBSSettingsPane = pane.make_pane_subclass(
     "OBSSettingsPane",
     ["record_path"],
-    [pane.SettingsType.STRING, pane.SettingsType.STRING],
-    [""]
+    [pane.SettingsWidget.STRING],
+    [""],
+    [((), {"compact":True}),]
 )
 
 OBSWebSocketPane = pane.make_pane_subclass(
     "OBSWebSocketPane",
-    ["host", "port", "password", "timeout"],
-    [pane.SettingsType.STRING, pane.SettingsType.INT, pane.SettingsType.STRING, pane.SettingsType.FLOAT],
-    ["localhost", 4455, "", 1.0]
+    [
+        pane.make_settings_widget("host", pane.SettingsWidget.STRING, "localhost", compact=True),
+        pane.make_settings_widget("port", pane.SettingsWidget.INT, 4455, compact=True),
+        pane.make_settings_widget("password", pane.SettingsWidget.STRING, "", compact=True),
+        pane.make_settings_widget("timeout", pane.SettingsWidget.FLOAT, 1.0, compact=True)
+    ]
 )
 
 OBSCTLPane = pane.make_pane_subclass(
     "OBSCTLPane",
     ["nothing"],
-    [pane.SettingsType.STRING],
+    [pane.SettingsWidget.STRING],
     ["zilch"]
 )
 
@@ -32,4 +36,4 @@ class SettingsBlock(Container):
         with TabbedContent(classes="settings_block--tabbed-content"):
             yield OBSSettingsPane("OBS Settings", id="obs_settings_pane")
             yield OBSWebSocketPane("OBS WebSocket", id="obs_websocket_pane")
-            yield OBSCTLPane("obs-ctl Settings", id="obs_ctl_pane")
+            yield OBSCTLPane("obs-ctl Settings", id="obs_ctl_pane")