from textual.css.query import NoMatches
from . import log, data
-from .widget import OBSSettingsPane
+from .interface import OBSSettingsPane
-from .widget.element.bool_control import BoolControl
-from .widget.element.text_readout import TextReadout
+from .interface.element.bool_control import BoolControl
+from .interface.element.text_readout import TextReadout
+from .interface import RecordControls
logger = log.getLogger(__name__)
obs_req_client = obs.ReqClient()
with ScrollableContainer(id="content_container"):
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 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")
+ yield RecordControls(id="record_controls")
+ # with Grid(id="controls_container", classes="blocks gridblocks"):
+ # 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")
yield TextReadout("Encdng frames dropped", "-/- (-.-%)", id="encoding_lag", classes="stats")
- @on(BoolControl.SetFalse, "#stream_control")
+ @on(RecordControls.StreamStopped, "#record_controls",)
def stream_off(self, message):
- logger.debug(f"Stop message rcv'd from {message.control}")
try:
obs_req_client.stop_stream()
except obs_error.OBSSDKRequestError as err:
else:
raise
- @on(BoolControl.SetTrue, "#stream_control")
+
+ @on(RecordControls.StreamStarted, "#record_controls")
def stream_on(self, message):
- logger.debug("Stream Started message rcv'd")
try:
obs_req_client.start_stream()
except obs_error.OBSSDKRequestError as err:
else:
raise
- @on(BoolControl.SetFalse, "#record_control")
- def record_off(self, message):
- logger.debug("Record Stopped message rcv'd")
- try:
- obs_req_client.stop_record()
- except obs_error.OBSSDKRequestError as err:
- if err.code == 501:
- logger.debug("Record already stopped.")
- else:
- raise
- @on(BoolControl.SetTrue, "#record_control")
+ @on(RecordControls.RecordStarted, "#record_controls")
def record_on(self, message):
- logger.debug("Record Started message rcv'd")
try:
obs_req_client.start_record()
except obs_error.OBSSDKRequestError as err:
raise
- @on(BoolControl.SetTrue, "#pause_control")
+ @on(RecordControls.RecordStopped, "#record_controls")
+ def record_off(self, message):
+ logger.debug("Record Stopped message rcv'd")
+ try:
+ obs_req_client.stop_record()
+ except obs_error.OBSSDKRequestError as err:
+ if err.code == 501:
+ logger.debug("Record already stopped.")
+ else:
+ raise
+
+
+ @on(RecordControls.RecordPaused, "#record_controls")
def pause_on(self, message):
logger.debug("Record Paused message rcv'd")
try:
raise
- @on(BoolControl.SetFalse, "#pause_control")
+ @on(RecordControls.RecordUnpaused, "#pause_control")
def pause_off(self, message):
logger.debug("Record Unpaused message rcv'd")
try:
min-height:8;
}
+RecordControls {
+}
+
BoolControl {
background:$surface;
border:blank;
--- /dev/null
+from textual.app import App, ComposeResult
+from textual.containers import Horizontal, VerticalScroll, Grid, Container
+from textual.message import Message
+from textual.reactive import reactive
+from textual.widgets import Label, RadioButton, Button, Input
+from textual.widget import Widget
+from textual.css import query
+
+from .message import MSGBase
+from .molecule import RecordControls
+from .. import log
+
+
+
+logger = log.getLogger(__name__)
+
+class OBSSettingsPane(VerticalScroll):
+ DEFAULT_CSS = """
+ .obssettingspane--input {
+ width:1fr;
+ }
+ Grid {
+ grid-size:2 1;
+ grid-columns:21 1fr;
+ Label {
+ width:1fr;
+ text-align:right;
+ padding-right:1;
+ }
+ }
+ """
+ COMPONENT_CLASSES = {
+ "obssettingspane--input",
+ }
+ record_path: reactive[str] = reactive("")
+ class RecordPathChanged(Message):
+ control: "OBSSettingsPane" = None
+ record_path: str = None
+ def __init__(self, ctrl, path):
+ super().__init__()
+ self.control = ctrl
+ self.record_path = path
+
+ def compose(self):
+ with Grid():
+ yield Label("Record path:")
+ yield Input("path", id="input_record_path", classes="obssettingspane--input", compact=True)
+
+ def on_input_submitted(self, event:Input.Submitted):
+ self.set_reactive(OBSSettingsPane.record_path, event.value)
+ if event.control.id == "input_record_path":
+ self.post_message(OBSSettingsPane.RecordPathChanged(self, event.value))
+
+ def watch_record_path(self, value):
+ input: Input = self.query_exactly_one("#input_record_path")
+ input.value = value
--- /dev/null
+from .bool_control import BoolControl
+from .text_readout import TextReadout
\ No newline at end of file
--- /dev/null
+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[bool] = reactive(False)
+ label: reactive[str] = reactive("Status")
+ true_label: reactive[str] = reactive("Start")
+ false_label: reactive[str] = reactive("Stop")
+ swap_red: reactive[bool] = reactive(False)
+ compact: reactive[bool] = reactive(True)
+ control_layout: reactive[str] = reactive("vertical")
+
+ class MsgBase(Message):
+ control: "BoolControl" = None
+ def __init__(self, control: "BoolControl"):
+ super().__init__()
+ self.control = control
+ class SetTrue(MsgBase): ...
+ class SetFalse(MsgBase): ...
+
+
+ def __init__(
+ self,
+ value: bool,
+ label: str = "Boolean",
+ true_label: str = "Start",
+ false_label: str = "Stop",
+ swap_red: bool = False,
+ compact: bool = 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
--- /dev/null
+from textual.containers import Container
+from textual.reactive import reactive
+from textual.widgets import Label
+
+class TextReadout(Container):
+ COMPONENT_CLASSES = {
+ "text_readout--label",
+ "text_readout--text",
+ }
+ text = reactive("Text")
+ label = reactive("Label")
+ readout_layout = reactive("vertical")
+
+ def __init__(self, label, text, *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(TextReadout.label, label)
+ self.set_reactive(TextReadout.text, text)
+ self.set_reactive(TextReadout.readout_layout, layout)
+
+
+ def compose(self):
+ yield Container(Label(self.label, id="label"), classes="text_readout--label")
+ yield Container(Label(self.text, id="text"), classes="text_readout--text")
+
+ def watch_text(self, value: str):
+ text: Label = self.query_exactly_one("#text")
+ text.update(value)
+
+
+ def watch_label(self, value):
+ label: Label = self.query_exactly_one("#label")
+ label.update(value)
+
+
+ def watch_readout_layout(self, value):
+ self.set_styles(layout=value)
\ No newline at end of file
--- /dev/null
+from textual.message import Message
+from textual.widget import Widget
+
+class MSGBase(Message):
+ control: Widget = None
+ def __init__(self, control):
+ self.control = control
+ super().__init__()
\ No newline at end of file
--- /dev/null
+from .record_controls import RecordControls
\ No newline at end of file
--- /dev/null
+from textual.containers import ScrollableContainer, Grid
+from textual.message import Message
+from textual.reactive import reactive
+
+from .. import MSGBase
+from ..element import BoolControl
+from ...log import getLogger
+
+logger=getLogger(__name__)
+class RecordControls(ScrollableContainer):
+ DEFAULT_CSS = """
+ Grid {
+ grid-size:2;
+ grid-rows:5;
+ height:10;
+ }
+ """
+
+ recording: reactive[bool] = False
+ paused: reactive[bool] = False
+ streaming: reactive[bool] = False
+
+ class RecordStarted(MSGBase): ...
+ class RecordStopped(MSGBase): ...
+ class RecordPaused(MSGBase): ...
+ class RecordUnpaused(MSGBase): ...
+ class StreamStarted(MSGBase): ...
+ class StreamStopped(MSGBase): ...
+
+ def compose(self):
+ with Grid():
+ yield BoolControl(False, label="Record", id="record_control")
+ yield BoolControl(
+ False,
+ label="Pause",
+ true_label="Pause",
+ false_label="Unpause",
+ swap_red=True,
+ id="pause_control"
+ )
+ yield BoolControl(False, label="Stream", id="stream_control")
+
+ def on_bool_control_set_true(self, message: BoolControl.SetTrue):
+ match message.control.id:
+ case "record_control":
+ self.post_message(RecordControls.RecordStarted(self))
+ case "pause_control":
+ self.post_message(RecordControls.RecordPaused(self))
+ case "stream_control":
+ self.post_message(RecordControls.StreamStarted(self))
+
+
+ def on_bool_control_set_false(self, message: BoolControl.SetFalse):
+ match message.control.id:
+ case "record_control":
+ self.post_message(RecordControls.RecordStopped(self))
+ case "pause_control":
+ self.post_message(RecordControls.RecordUnpaused(self))
+ case "stream_control":
+ self.post_message(RecordControls.StreamStopped(self))
+
+ def watch_recording(self, value):
+ control: BoolControl = self.query_exactly_one("#record_control")
+ control.value = value
+
+ def watch_paused(self, value):
+ control: BoolControl = self.query_exactly_one("#pause_control")
+ control.value = value
+
+ def watch_streaming(self, value):
+ control: BoolControl = self.query_exactly_one("#stream_control")
+ control.value = value
\ No newline at end of file
+++ /dev/null
-from textual.app import App, ComposeResult
-from textual.containers import Horizontal, VerticalScroll, Grid, Container
-from textual.message import Message
-from textual.reactive import reactive
-from textual.widgets import Label, RadioButton, Button, Input
-from textual.widget import Widget
-from textual.css import query
-
-
-from .. import log
-
-logger = log.getLogger(__name__)
-
-class OBSSettingsPane(VerticalScroll):
- DEFAULT_CSS = """
- .obssettingspane--input {
- width:1fr;
- }
- Grid {
- grid-size:2 1;
- grid-columns:21 1fr;
- Label {
- width:1fr;
- text-align:right;
- padding-right:1;
- }
- }
- """
- COMPONENT_CLASSES = {
- "obssettingspane--input",
- }
- record_path: reactive[str] = reactive("")
- class RecordPathChanged(Message):
- control: "OBSSettingsPane" = None
- record_path: str = None
- def __init__(self, ctrl, path):
- super().__init__()
- self.control = ctrl
- self.record_path = path
-
- def compose(self):
- with Grid():
- yield Label("Record path:")
- yield Input("path", id="input_record_path", classes="obssettingspane--input", compact=True)
-
- def on_input_submitted(self, event:Input.Submitted):
- self.set_reactive(OBSSettingsPane.record_path, event.value)
- if event.control.id == "input_record_path":
- self.post_message(OBSSettingsPane.RecordPathChanged(self, event.value))
-
- def watch_record_path(self, value):
- input: Input = self.query_exactly_one("#input_record_path")
- input.value = value
+++ /dev/null
-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[bool] = reactive(False)
- label: reactive[str] = reactive("Status")
- true_label: reactive[str] = reactive("Start")
- false_label: reactive[str] = reactive("Stop")
- swap_red: reactive[bool] = reactive(False)
- compact: reactive[bool] = reactive(True)
- control_layout: reactive[str] = reactive("vertical")
-
- class MsgBase(Message):
- control: "BoolControl" = None
- def __init__(self, control: "BoolControl"):
- super().__init__()
- self.control = control
- class SetTrue(MsgBase): ...
- class SetFalse(MsgBase): ...
-
-
- def __init__(
- self,
- value: bool,
- label: str = "Boolean",
- true_label: str = "Start",
- false_label: str = "Stop",
- swap_red: bool = False,
- compact: bool = 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
+++ /dev/null
-from textual.containers import Container
-from textual.reactive import reactive
-from textual.widgets import Label
-
-class TextReadout(Container):
- COMPONENT_CLASSES = {
- "text_readout--label",
- "text_readout--text",
- }
- text = reactive("Text")
- label = reactive("Label")
- readout_layout = reactive("vertical")
-
- def __init__(self, label, text, *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(TextReadout.label, label)
- self.set_reactive(TextReadout.text, text)
- self.set_reactive(TextReadout.readout_layout, layout)
-
-
- def compose(self):
- yield Container(Label(self.label, id="label"), classes="text_readout--label")
- yield Container(Label(self.text, id="text"), classes="text_readout--text")
-
- def watch_text(self, value: str):
- text: Label = self.query_exactly_one("#text")
- text.update(value)
-
-
- def watch_label(self, value):
- label: Label = self.query_exactly_one("#label")
- label.update(value)
-
-
- def watch_readout_layout(self, value):
- self.set_styles(layout=value)
\ No newline at end of file