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
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.
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):
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"
# 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],
# 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
--- /dev/null
+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
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"]
)
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")