From: sbkelley Date: Sun, 7 Dec 2025 15:19:04 +0000 (-0500) Subject: Slowly making the pane factory more useful X-Git-Url: https://skyeroc.xyz/gitweb/?a=commitdiff_plain;h=ffd22a41d3066a046502aad84bacdaa3b53ddef6;p=obs-ctl Slowly making the pane factory more useful --- diff --git a/src/obs_ctl/interface/molecule/settings_block/pane.py b/src/obs_ctl/interface/molecule/settings_block/pane.py index abf522b..1607f6d 100644 --- a/src/obs_ctl/interface/molecule/settings_block/pane.py +++ b/src/obs_ctl/interface/molecule/settings_block/pane.py @@ -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 index 0000000..0bc115c --- /dev/null +++ b/src/obs_ctl/interface/molecule/settings_block/pane.pyi @@ -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 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 0cb9520..f6aeb94 100644 --- a/src/obs_ctl/interface/molecule/settings_block/settings_block.py +++ b/src/obs_ctl/interface/molecule/settings_block/settings_block.py @@ -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")