From 0f0fbcdfffb9ed6267f4c984f960b6a2060ad4a2 Mon Sep 17 00:00:00 2001 From: sbkelley Date: Sun, 30 Nov 2025 00:53:23 -0500 Subject: [PATCH] Rename and clean up bool_control --- src/obs_ctl/__init__.py | 26 +-- src/obs_ctl/data/layout.tcss | 2 +- src/obs_ctl/widget/element/bool_control.py | 167 ++++++++++++++++++ .../widget/element/single_status_control.py | 158 ----------------- 4 files changed, 181 insertions(+), 172 deletions(-) create mode 100644 src/obs_ctl/widget/element/bool_control.py delete mode 100644 src/obs_ctl/widget/element/single_status_control.py diff --git a/src/obs_ctl/__init__.py b/src/obs_ctl/__init__.py index 0350709..c409d33 100644 --- a/src/obs_ctl/__init__.py +++ b/src/obs_ctl/__init__.py @@ -13,7 +13,7 @@ from textual.css.query import NoMatches from . import log, data from .widget import LabeledLabel, OBSSettingsPane -from .widget.element.single_status_control import SingleStatusControl +from .widget.element.bool_control import BoolControl logger = log.getLogger(__name__) obs_req_client = obs.ReqClient() @@ -70,9 +70,9 @@ class OBSCtlApp(App): with Vertical(id="blocks_v_container", classes="blocks_containers"): with Horizontal(id="blocks_h_container1", classes="blocks_h_containers"): with Grid(id="controls_container", classes="blocks gridblocks"): - yield SingleStatusControl(label="Recording", id="record_control", classes="controls") - yield SingleStatusControl(label="Paused", on_label="Pause", off_label="Unpause", swap_red=True, id="pause_control", classes="controls") - yield SingleStatusControl(label="Streaming", id="stream_control", classes="controls") + yield BoolControl(False, label="Recording", id="record_control", classes="controls") + yield BoolControl(False, label="Paused", true_label="Pause", false_label="Unpause", swap_red=True, id="pause_control", classes="controls") + yield BoolControl(False, label="Streaming", id="stream_control", classes="controls") with TabbedContent(id="settings_container", classes="blocks"): with TabPane("OBS Settings", id="settings-tab"): yield OBSSettingsPane(id="settings-content") @@ -92,7 +92,7 @@ class OBSCtlApp(App): yield LabeledLabel("Encdng frames dropped", "-/- (-.-%)", id="encoding_lag", classes="stats") - @on(SingleStatusControl.TurnedOff, "#stream_control") + @on(BoolControl.SetFalse, "#stream_control") def stream_off(self, message): logger.debug(f"Stop message rcv'd from {message.control}") try: @@ -103,7 +103,7 @@ class OBSCtlApp(App): else: raise - @on(SingleStatusControl.TurnedOn, "#stream_control") + @on(BoolControl.SetTrue, "#stream_control") def stream_on(self, message): logger.debug("Stream Started message rcv'd") try: @@ -114,7 +114,7 @@ class OBSCtlApp(App): else: raise - @on(SingleStatusControl.TurnedOff, "#record_control") + @on(BoolControl.SetFalse, "#record_control") def record_off(self, message): logger.debug("Record Stopped message rcv'd") try: @@ -125,7 +125,7 @@ class OBSCtlApp(App): else: raise - @on(SingleStatusControl.TurnedOn, "#record_control") + @on(BoolControl.SetTrue, "#record_control") def record_on(self, message): logger.debug("Record Started message rcv'd") try: @@ -137,7 +137,7 @@ class OBSCtlApp(App): raise - @on(SingleStatusControl.TurnedOn, "#pause_control") + @on(BoolControl.SetTrue, "#pause_control") def pause_on(self, message): logger.debug("Record Paused message rcv'd") try: @@ -149,7 +149,7 @@ class OBSCtlApp(App): raise - @on(SingleStatusControl.TurnedOff, "#pause_control") + @on(BoolControl.SetFalse, "#pause_control") def pause_off(self, message): logger.debug("Record Unpaused message rcv'd") try: @@ -207,9 +207,9 @@ class OBSCtlApp(App): def update_monitors(self, obs_stats: OBSStats): - stream_ctl: SingleStatusControl = self.query_exactly_one("#stream_control") - record_ctl: SingleStatusControl = self.query_exactly_one("#record_control") - pause_ctl: SingleStatusControl = self.query_exactly_one("#pause_control") + stream_ctl: BoolControl = self.query_exactly_one("#stream_control") + record_ctl: BoolControl = self.query_exactly_one("#record_control") + pause_ctl: BoolControl = self.query_exactly_one("#pause_control") cpu_usage: LabeledLabel = self.query_exactly_one("#cpu_usage") memory_usage: LabeledLabel = self.query_exactly_one("#memory_usage") active_fp: LabeledLabel = self.query_exactly_one("#fps") diff --git a/src/obs_ctl/data/layout.tcss b/src/obs_ctl/data/layout.tcss index ef662b2..b2d87d9 100644 --- a/src/obs_ctl/data/layout.tcss +++ b/src/obs_ctl/data/layout.tcss @@ -35,7 +35,7 @@ min-height:8; } -SingleStatusControl { +BoolControl { background:$surface; border:blank; max-height:5; diff --git a/src/obs_ctl/widget/element/bool_control.py b/src/obs_ctl/widget/element/bool_control.py new file mode 100644 index 0000000..7a03158 --- /dev/null +++ b/src/obs_ctl/widget/element/bool_control.py @@ -0,0 +1,167 @@ +from textual import on +from textual.app import ComposeResult +from textual.containers import Container +from textual.css import query +from textual.message import Message +from textual.reactive import reactive +from textual.widgets import Button, RadioButton + +from ...log import getLogger + +logger = getLogger(__name__) + +class BoolControl(Container): + """BoolControl manages a boolean that belongs to something else. + + In other words, it can display true or false and this can be set + on it directly, but its buttons do nothing other than post a + message. Designed as i/o for an API""" + DEFAULT_CSS = """ + .bool_control--light { + background:$panel; + text-align:center; + align:center bottom; + width:1fr; + height:1; + } + .bool_control--true_button { + width:1fr; + height:1fr; + } + .bool_control--false_button { + width:1fr; + height:1fr; + } + """ + COMPONENT_CLASSES = { + "bool_control--light", + "bool_control--true_button", + "bool_control--false_button", + } + value = reactive(False) + label = reactive("Status") + true_label = reactive("Start") + false_label = reactive("Stop") + swap_red = reactive(False) + compact = reactive(True) + control_layout = reactive("vertical") + + class MsgBase(Message): + control: "BoolControl" = None + def __init__(self, control): + super().__init__() + self.control = control + class SetTrue(MsgBase): ... + class SetFalse(MsgBase): ... + + + def __init__( + self, + value, + label: str = "Boolean", + true_label = "Start", + false_label = "Stop", + swap_red = False, + compact = True, + *children, + layout = "vertical", + name = None, + id = None, + classes = None, + disabled = False, + markup = True): + super().__init__(*children, name=name, id=id, classes=classes, disabled=disabled, markup=markup) + self.set_reactive(BoolControl.value, value) + self.set_reactive(BoolControl.label, label) + self.set_reactive(BoolControl.true_label, true_label) + self.set_reactive(BoolControl.false_label, false_label) + self.set_reactive(BoolControl.swap_red, swap_red) + self.set_reactive(BoolControl.compact, compact) + self.set_reactive(BoolControl.control_layout, layout) + + def compose(self) -> ComposeResult: + with Container(classes="bool_control--light"): + yield RadioButton( + label="", + value=False, + button_first=False, + id="light", + compact=True, + disabled=True, + classes="bool_control--light", + ) + yield Button( + label="Start", + id="true", + variant="success", + compact=True, + classes="bool_control--true_button" + ) + yield Button( + label="Stop", + id="false", + variant="error", + compact=True, + classes = "bool_control--false_button" + ) + + + def _on_mount(self, event): + # super()._on_mount(event) + self.watch_value(self.value) + self.watch_label(self.label) + self.watch_true_label(self.true_label) + self.watch_false_label(self.false_label) + self.watch_swap_red(self.swap_red) + self.watch_compact(self.compact) + self.watch_control_layout(self.control_layout) + + + @on(Button.Pressed, "#false") + def false_pressed(self, event: Button.Pressed): + self.post_message(self.SetFalse(self)) + + + @on(Button.Pressed, "#true") + def true_pressed(self): + self.post_message(self.SetTrue(self)) + + + def watch_value(self, value: bool) -> None: + light: RadioButton = self.query_exactly_one("#light") + light.value = value + + + def watch_label(self, value): + light: RadioButton = self.query_exactly_one("#light") + light.label = value + + + def watch_true_label(self, value): + true: Button = self.query_exactly_one("#true") + true.label = value + + + def watch_false_label(self, value): + false: Button = self.query_exactly_one("#false") + false.label = value + + + def watch_swap_red(self, value): + true: Button = self.query_exactly_one("#true") + false: Button = self.query_exactly_one("#false") + false.variant = "success" if value else "error" + true.variant = "error" if value else "success" + + + def watch_compact(self, value): + light: RadioButton = self.query_exactly_one("#light") + true: Button = self.query_exactly_one("#true") + false: Button = self.query_exactly_one("#false") + light.compact = value + true.compact = value + false.compact = value + + + def watch_control_layout(self, value): + self.set_styles(layout=value) \ No newline at end of file diff --git a/src/obs_ctl/widget/element/single_status_control.py b/src/obs_ctl/widget/element/single_status_control.py deleted file mode 100644 index bb3f4e1..0000000 --- a/src/obs_ctl/widget/element/single_status_control.py +++ /dev/null @@ -1,158 +0,0 @@ -from textual.containers import Container -from textual.message import Message -from textual.reactive import reactive -from textual.css import query -from textual.widget import ComposeResult -from textual.widgets import Button, RadioButton - -from ...log import getLogger - -logger = getLogger() - -class SingleStatusControl(Container): - DEFAULT_CSS = """ - .singlestatuscontrol--light { - background:$panel; - text-align:center; - align:center bottom; - width:1fr; - height:1fr; - } - .singlestatuscontrol--on_button { - width:1fr; - height:1fr; - } - .singlestatuscontrol--off_button { - width:1fr; - height:1fr; - } - """ - COMPONENT_CLASSES = { - "singlestatuscontrol--light", - "singlesttauscontrol--on_button", - "singlestatuscontrol--off_button", - } - status = reactive(False) - label = reactive("Status") - on_label = reactive("Start") - off_label = reactive("Stop") - swap_red = reactive(False) - - class MsgBase(Message): - control: "SingleStatusControl" = None - def __init__(self, control): - super().__init__() - self.control = control - class TurnedOn(MsgBase): ... - class TurnedOff(MsgBase): ... - - - def __init__( - self, - *children, - label: str = "Status", - on_label = "Start", - off_label = "Stop", - swap_red = False, - layout = "vertical", - name = None, - id = None, - classes = None, - disabled = False, - markup = True): - super().__init__(*children, name=name, id=id, classes=classes, disabled=disabled, markup=markup) - self.status = False - self.label = label - self.on_label = on_label - self.off_label = off_label - self.swap_red = swap_red - self.set_styles(layout=layout) - - def compose(self) -> ComposeResult: - logger.debug("Composing SingleStatusControl") - with Container(classes="singlestatuscontrol--light"): - yield RadioButton( - label="", - value=False, - button_first=False, - id="light", - compact=True, - disabled=True, - ) - yield Button( - label="Start", - id="on", - variant="success", - compact=True, - classes="singlestatuscontrol--on_button" - ) - yield Button( - label="Stop", - id="off", - variant="error", - compact=True, - classes = "singlestatuscontrol--off_button" - ) - - - def _on_mount(self, event): - # super()._on_mount(event) - self.watch_status(self.status) - self.watch_label(self.label) - self.watch_on_label(self.on_label) - self.watch_off_label(self.off_label) - self.watch_swap_red(self.swap_red) - self.refresh() - - - def on_button_pressed(self, event: Button.Pressed): - if event.button.id == "on": - logger.debug(f"'On' pressed on {event.control}") - self.post_message(self.TurnedOn(self)) - elif event.button.id == "off": - logger.debug(f"'Off' pressed on {event.control}") - self.post_message(self.TurnedOff(self)) - - - def watch_status(self, value: bool) -> None: - try: - light: RadioButton = self.query_exactly_one("#light") - light.value = value - except query.NoMatches: - if self.is_mounted: - raise - - - def watch_label(self, value): - try: - light: RadioButton = self.query_exactly_one("#light") - light.label = value - except query.NoMatches: - if self.is_mounted: - raise - - def watch_on_label(self, value): - try: - on: Button = self.query_exactly_one("#on") - on.label = value - except query.NoMatches: - if self.is_mounted: - raise - - def watch_off_label(self, value): - try: - off: Button = self.query_exactly_one("#off") - off.label = value - except query.NoMatches: - if self.is_mounted: - raise - - def watch_swap_red(self, value): - try: - on: Button = self.query_exactly_one("#on") - off: Button = self.query_exactly_one("#off") - off.variant = "success" if value else "error" - on.variant = "error" if value else "success" - except query.NoMatches: - if self.is_mounted: - raise \ No newline at end of file -- 2.47.3