From 38c43da92ad4a007212c5fc622c6cf4a5a3da08d Mon Sep 17 00:00:00 2001 From: salt-die <53280662+salt-die@users.noreply.github.com> Date: Mon, 4 Mar 2024 00:07:50 -0600 Subject: [PATCH] ButtonState and ToggleState enums are now literal types. Added "disallowed" state to ButtonState. Added theming for disallowed buttons and menu items. --- examples/advanced/figfonts.py | 18 ++- examples/basic/buttons.py | 20 +-- examples/basic/menu.py | 2 +- src/batgrl/colors/color_types.py | 3 +- src/batgrl/colors/colors.py | 3 +- .../gadgets/behaviors/button_behavior.py | 100 ++++++++------- .../behaviors/toggle_button_behavior.py | 115 +++++++---------- src/batgrl/gadgets/button.py | 25 ++-- src/batgrl/gadgets/flat_toggle.py | 120 +++++++++++------- src/batgrl/gadgets/menu.py | 81 +++++------- src/batgrl/gadgets/tabs.py | 16 +-- src/batgrl/gadgets/toggle_button.py | 52 ++++---- src/batgrl/gadgets/tree_view.py | 2 +- 13 files changed, 269 insertions(+), 288 deletions(-) diff --git a/examples/advanced/figfonts.py b/examples/advanced/figfonts.py index 65f5420d..6c03aa97 100644 --- a/examples/advanced/figfonts.py +++ b/examples/advanced/figfonts.py @@ -6,7 +6,7 @@ from batgrl.app import App from batgrl.colors import BLACK, RED, WHITE, gradient, lerp_colors from batgrl.figfont import FIGFont -from batgrl.gadgets.behaviors.button_behavior import ButtonBehavior, ButtonState +from batgrl.gadgets.behaviors.button_behavior import ButtonBehavior from batgrl.gadgets.text import Text ASSETS = Path(__file__).parent.parent / "assets" @@ -73,24 +73,26 @@ async def _drip(self): class TextButton(ButtonBehavior, Text): def __init__(self, **kwargs): - super().__init__(**kwargs) - self._dark_red = lerp_colors(RED, BLACK, 0.35) + self._color_task = None self._grad = gradient(WHITE, RED, 20) + self._dark_red = lerp_colors(RED, BLACK, 0.35) self._i = 0 + super().__init__(**kwargs) def on_add(self): self._color_task = asyncio.create_task(self._to_white()) super().on_add() def on_remove(self): - self._color_task.cancel() + if self._color_task is not None: + self._color_task.cancel() super().on_remove() async def _to_red(self): self.canvas["fg_color"] = self._grad[self._i] while self._i < len(self._grad) - 1: self._i += 1 - if self.state is not ButtonState.DOWN: + if self.button_state != "down": self.canvas["fg_color"] = self._grad[self._i] await asyncio.sleep(0.01) @@ -102,12 +104,14 @@ async def _to_white(self): await asyncio.sleep(0.01) def update_hover(self): - self._color_task.cancel() + if self._color_task is not None: + self._color_task.cancel() self._color_task = asyncio.create_task(self._to_red()) self.canvas["fg_color"] = self._grad[self._i] def update_normal(self): - self._color_task.cancel() + if self._color_task is not None: + self._color_task.cancel() self._color_task = asyncio.create_task(self._to_white()) self.canvas["fg_color"] = self._grad[self._i] diff --git a/examples/basic/buttons.py b/examples/basic/buttons.py index 49f872fa..490bb77f 100644 --- a/examples/basic/buttons.py +++ b/examples/basic/buttons.py @@ -4,7 +4,7 @@ from batgrl.gadgets.flat_toggle import FlatToggle from batgrl.gadgets.grid_layout import GridLayout from batgrl.gadgets.text import Text -from batgrl.gadgets.toggle_button import ToggleButton, ToggleState +from batgrl.gadgets.toggle_button import ToggleButton class ButtonApp(App): @@ -19,7 +19,7 @@ def callback(): def toggle_button_callback(i): def callback(state): - prefix = "un" if state is ToggleState.OFF else "" + prefix = "un" if state == "off" else "" display.add_str(f"Button {i + 1} {prefix}toggled!".ljust(20)) return callback @@ -36,13 +36,10 @@ def callback(state): horizontal_spacing=1, ) - # Buttons grid_layout.add_gadgets( Button(size=(1, 10), label=f"Button {i + 1}", callback=button_callback(i)) for i in range(5) ) - - # Independent toggle buttons grid_layout.add_gadgets( ToggleButton( size=(1, 12), @@ -51,18 +48,15 @@ def callback(state): ) for i in range(5, 10) ) - - # Grouped radio buttons grid_layout.add_gadgets( ToggleButton( size=(1, 12), - group="a", + group=0, label=f"Button {i + 1}", callback=toggle_button_callback(i), ) for i in range(10, 15) ) - grid_layout.size = grid_layout.minimum_grid_size flat_grid = GridLayout( @@ -72,20 +66,14 @@ def callback(state): orientation="lr-tb", horizontal_spacing=1, ) - - # Independent flat toggles flat_grid.add_gadgets( FlatToggle(callback=toggle_button_callback(i)) for i in range(15, 20) ) - - # Grouped flat toggles flat_grid.add_gadgets( - FlatToggle(group="b", callback=toggle_button_callback(i)) + FlatToggle(group=1, callback=toggle_button_callback(i)) for i in range(20, 25) ) - flat_grid.size = flat_grid.minimum_grid_size - self.add_gadgets(display, grid_layout, flat_grid) diff --git a/examples/basic/menu.py b/examples/basic/menu.py index d232fc33..e27f3e9d 100644 --- a/examples/basic/menu.py +++ b/examples/basic/menu.py @@ -51,7 +51,7 @@ def inner(toggle_state): ) ) - self.children[-2].children[1].item_disabled = True + self.children[-2].children[1].button_state = "disallowed" if __name__ == "__main__": diff --git a/src/batgrl/colors/color_types.py b/src/batgrl/colors/color_types.py index 08e51407..ea0bbbb0 100644 --- a/src/batgrl/colors/color_types.py +++ b/src/batgrl/colors/color_types.py @@ -266,9 +266,10 @@ class ColorTheme(TypedDict, total=False): button_normal: ColorPair button_hover: ColorPair button_press: ColorPair + button_disallowed: ColorPair menu_item_hover: ColorPair menu_item_selected: ColorPair - menu_item_disabled: ColorPair + menu_item_disallowed: ColorPair titlebar_normal: ColorPair titlebar_inactive: ColorPair data_table_sort_indicator: ColorPair diff --git a/src/batgrl/colors/colors.py b/src/batgrl/colors/colors.py index c04ba214..5d99c441 100644 --- a/src/batgrl/colors/colors.py +++ b/src/batgrl/colors/colors.py @@ -101,9 +101,10 @@ "button_normal": {"fg": "dde4ed", "bg": "2a3ca0"}, "button_hover": {"fg": "fff0f6", "bg": "3248c0"}, "button_press": {"fg": "fff0f6", "bg": "c4a219"}, + "button_disallowed": {"fg": "272b40", "bg": "070c25"}, "menu_item_hover": {"fg": "f2babc", "bg": "111834"}, "menu_item_selected": {"fg": "ecf3ff", "bg": "1b244b"}, - "menu_item_disabled": {"fg": "272b40", "bg": "070c25"}, + "menu_item_disallowed": {"fg": "272b40", "bg": "070c25"}, "titlebar_normal": {"fg": "ffe0df", "bg": "070c25"}, "titlebar_inactive": {"fg": "7d6b71", "bg": "070c25"}, "data_table_sort_indicator": {"fg": "ecf3ff", "bg": "070c25"}, diff --git a/src/batgrl/gadgets/behaviors/button_behavior.py b/src/batgrl/gadgets/behaviors/button_behavior.py index 8bf3f955..1de8c47a 100644 --- a/src/batgrl/gadgets/behaviors/button_behavior.py +++ b/src/batgrl/gadgets/behaviors/button_behavior.py @@ -1,31 +1,25 @@ """Button behavior for a gadget.""" -from enum import Enum +from typing import Literal from ...io import MouseEventType __all__ = ["ButtonState", "ButtonBehavior"] - -class ButtonState(str, Enum): - """ - State of a button gadget. - - :class:`ButtonState` is one of "normal", "hover", "down". - """ - - NORMAL = "normal" - HOVER = "hover" - DOWN = "down" +ButtonState = Literal["normal", "hover", "down", "disallowed"] +"""Button behavior states.""" class ButtonBehavior: """ Button behavior for a gadget. - A button has three states: 'normal', 'hover', and 'down'. + A button has four states: "normal", "hover", "down", and "disallowed". When a button's state changes one of the following methods are called: - :meth:`update_normal`, :meth:`update_hover`, and :meth:`update_down`. + - :meth:`update_normal` + - :meth:`update_hover` + - :meth:`update_down` + - :meth:`update_disallowed` When a button is released, the :meth:`on_release` method is called. @@ -38,45 +32,52 @@ class ButtonBehavior: ---------- always_release : bool Whether a mouse up event outside the button will trigger it. - state : ButtonState - Current button state. One of `NORMAL`, `HOVER`, `DOWN`. + button_state : ButtonState + Current button state. Methods ------- + on_release() + Triggered when a button is released. update_normal() Paint the normal state. update_hover() Paint the hover state. update_down() Paint the down state. - on_release() - Triggered when a button is released. + update_disallowed() + Paint the disallowed state. """ def __init__(self, *, always_release: bool = False, **kwargs): super().__init__(**kwargs) self.always_release = always_release - self.state = ButtonState.NORMAL - - def on_add(self): - """Paint normal state.""" - super().on_add() - self._normal() - - def _normal(self): - self.state = ButtonState.NORMAL - self.update_normal() - - def _hover(self): - self.state = ButtonState.HOVER - self.update_hover() - - def _down(self): - self.state = ButtonState.DOWN - self.update_down() - - def on_mouse(self, mouse_event): + self.button_state: ButtonState = "normal" + + @property + def button_state(self) -> ButtonState: + """Current button state.""" + return self._button_state + + @button_state.setter + def button_state(self, button_state: ButtonState): + dispatch = { + "normal": self.update_normal, + "hover": self.update_hover, + "down": self.update_down, + "disallowed": self.update_disallowed, + } + if button_state not in dispatch: + button_state = "normal" + + self._button_state = button_state + dispatch[button_state]() + + def on_mouse(self, mouse_event) -> bool | None: """Determine button state from mouse event.""" + if self.button_state == "disallowed": + return False + if super().on_mouse(mouse_event): return True @@ -84,28 +85,31 @@ def on_mouse(self, mouse_event): if mouse_event.event_type is MouseEventType.MOUSE_DOWN: if collides: - self._down() + self.button_state = "down" return True elif ( mouse_event.event_type is MouseEventType.MOUSE_UP - and self.state is ButtonState.DOWN + and self.button_state == "down" ): if collides: - self._hover() self.on_release() + self.button_state = "hover" return True - self._normal() + self.button_state = "normal" if self.always_release: self.on_release() return True - if not collides and self.state is ButtonState.HOVER: - self._normal() - elif collides and self.state is ButtonState.NORMAL: - self._hover() + if not collides and self.button_state == "hover": + self.button_state = "normal" + elif collides and self.button_state == "normal": + self.button_state = "hover" + + def on_release(self): + """Triggered when button is released.""" def update_normal(self): """Paint the normal state.""" @@ -116,5 +120,5 @@ def update_hover(self): def update_down(self): """Paint the down state.""" - def on_release(self): - """Triggered when button is released.""" + def update_disallowed(self): + """Paint the disallowed state.""" diff --git a/src/batgrl/gadgets/behaviors/toggle_button_behavior.py b/src/batgrl/gadgets/behaviors/toggle_button_behavior.py index 0a4a1746..66e696da 100644 --- a/src/batgrl/gadgets/behaviors/toggle_button_behavior.py +++ b/src/batgrl/gadgets/behaviors/toggle_button_behavior.py @@ -1,29 +1,21 @@ """Toggle button behavior for a gadget.""" from collections.abc import Hashable -from enum import Enum +from typing import Literal from weakref import WeakValueDictionary from .button_behavior import ButtonBehavior, ButtonState __all__ = ["ButtonState", "ToggleState", "ToggleButtonBehavior"] - -class ToggleState(str, Enum): - """ - Toggle button states. - - :class:`ToggleState` is one of "on", "off". - """ - - ON = "on" - OFF = "off" +ToggleState = Literal["on", "off"] +"""Toggle button behavior states.""" class ToggleButtonBehavior(ButtonBehavior): """ Toggle button behavior for gadgets. - Without a group, toggle button's states switch between on and off when pressed. + Without a group, toggle button's states switch between "on" and "off" when pressed. With a group, only a single button in the group can be in the "on" state at a time. Parameters @@ -32,9 +24,7 @@ class ToggleButtonBehavior(ButtonBehavior): If a group is provided, only one button in a group can be in the on state. allow_no_selection : bool, default: False If a group is provided, setting this to true allows no selection, i.e., - every button can be in the "off" state. - toggle_state : ToggleState, default: ToggleState.OFF - Initial toggle state of button. + every button can be in the off state. always_release : bool, default: False Whether a mouse up event outside the button will trigger it. @@ -48,25 +38,27 @@ class ToggleButtonBehavior(ButtonBehavior): Toggle state of button. always_release : bool Whether a mouse up event outside the button will trigger it. - state : ButtonState - Current button state. One of normal, hover or down. + button_state : ButtonState + Current button state. Methods ------- + on_toggle() + Triggled on toggle state change. update_off() Paint the off state. update_on() Paint the on state. - on_toggle() - Update gadget on toggle state change. + on_release() + Triggered when a button is released. update_normal() Paint the normal state. update_hover() Paint the hover state. update_down() Paint the down state. - on_release() - Triggered when a button is released. + update_disallowed() + Paint the disallowed state. """ _toggle_groups = WeakValueDictionary() @@ -75,86 +67,69 @@ def __init__( self, group: Hashable | None = None, allow_no_selection: bool = False, - toggle_state: ToggleState = ToggleState.OFF, always_release: bool = False, **kwargs, ): + self._toggle_state = "off" + super().__init__(always_release=always_release, **kwargs) self.group = group self.allow_no_selection = allow_no_selection - self._toggle_state = ToggleState.OFF if ( group is not None - and toggle_state is ToggleState.OFF - and ToggleButtonBehavior._toggle_groups.get(group) is None and not allow_no_selection + and ToggleButtonBehavior._toggle_groups.get(group) is None ): - # If a group requires a selection, the first member of the group - # will be forced on and initial toggle state will be ignored. - toggle_state = ToggleState.ON ToggleButtonBehavior._toggle_groups[group] = self - - super().__init__(always_release=always_release, **kwargs) - - self.toggle_state = toggle_state + self._toggle_state = "on" + self.update_on() + else: + self.update_off() @property def toggle_state(self) -> ToggleState: - """ - Initial toggle state of button. - - If button is in a group and :attr:`allow_no_selection` is false this value will - be ignored if all buttons would be off. - """ + """Toggle state of button.""" return self._toggle_state @toggle_state.setter def toggle_state(self, toggle_state: ToggleState): - toggle_state = ToggleState(toggle_state) + if toggle_state not in {"on", "off"}: + toggle_state = "off" - if self._toggle_state is toggle_state or ( - self.group is not None - and toggle_state is ToggleState.OFF - and not self.allow_no_selection - ): + if self._toggle_state == toggle_state: return - self._toggle_state = toggle_state + groups = ToggleButtonBehavior._toggle_groups + grouped_on = groups.get(self.group) - if toggle_state is ToggleState.ON: - if ( - self.group is not None - and (last_on := ToggleButtonBehavior._toggle_groups.get(self.group)) - is not None - and last_on - is not self # last condition is false if initialized in the "on" state - ): - last_on._toggle_state = ToggleState.OFF - last_on.update_off() - last_on.on_toggle() - ToggleButtonBehavior._toggle_groups[self.group] = self + if toggle_state == "on": + if grouped_on is not None: + grouped_on._toggle_state = "off" + grouped_on.update_off() + grouped_on.on_toggle() + if self.group is not None: + groups[self.group] = self self.update_on() else: - if ( - self.group is not None - and ToggleButtonBehavior._toggle_groups.get(self.group) is self - ): - del ToggleButtonBehavior._toggle_groups[self.group] + if grouped_on is self: + if self.allow_no_selection: + del groups[self.group] + else: + return self.update_off() + self._toggle_state = toggle_state self.on_toggle() - def _down(self): - self.toggle_state = ( - ToggleState.OFF if self.toggle_state is ToggleState.ON else ToggleState.ON - ) - super()._down() + def on_toggle(self): + """Update gadget on toggle state change.""" + + def on_release(self): + """Triggered when button is released.""" + self.toggle_state = "off" if self.toggle_state == "on" else "on" def update_off(self): """Paint the off state.""" def update_on(self): """Paint the on state.""" - - def on_toggle(self): - """Update gadget on toggle state change.""" diff --git a/src/batgrl/gadgets/button.py b/src/batgrl/gadgets/button.py index 5cc80435..08a889ec 100644 --- a/src/batgrl/gadgets/button.py +++ b/src/batgrl/gadgets/button.py @@ -68,8 +68,8 @@ class Button(Themable, ButtonBehavior, Gadget): Transparency of gadget. always_release : bool Whether a mouse up event outside the button will trigger it. - state : ButtonState - Current button state. One of `NORMAL`, `HOVER`, `DOWN`. + button_state : ButtonState + Current button state. size : Size Size of gadget. height : int @@ -121,14 +121,16 @@ class Button(Themable, ButtonBehavior, Gadget): ------- update_theme() Paint the gadget with current theme. + on_release() + Triggered when a button is released. update_normal() Paint the normal state. update_hover() Paint the hover state. update_down() Paint the down state. - on_release() - Triggered when a button is released. + update_disallowed() + Paint the disallowed state. on_size() Update gadget after a resize. apply_hints() @@ -239,13 +241,7 @@ def label(self, label: str): def update_theme(self): """Paint the gadget with current theme.""" - match self.state: - case ButtonState.NORMAL: - self.update_normal() - case ButtonState.HOVER: - self.update_hover() - case ButtonState.DOWN: - self.update_down() + getattr(self, f"update_{self.button_state}")() def update_normal(self): """Paint the normal state.""" @@ -262,7 +258,12 @@ def update_down(self): self._pane.bg_color = self.color_theme.button_press.bg self._label.canvas["fg_color"] = self.color_theme.button_press.fg + def update_disallowed(self): + """Paint the disallowd state.""" + self._pane.bg_color = self.color_theme.button_disallowed.bg + self._label.canvas["fg_color"] = self.color_theme.button_disallowed.fg + def on_release(self): """Triggered when button is released.""" - if self.callback is not None: + if self.root is not None and self.callback is not None: self.callback() diff --git a/src/batgrl/gadgets/flat_toggle.py b/src/batgrl/gadgets/flat_toggle.py index 5ca7626b..9d63494e 100644 --- a/src/batgrl/gadgets/flat_toggle.py +++ b/src/batgrl/gadgets/flat_toggle.py @@ -17,7 +17,7 @@ SizeHint, SizeHintDict, ) -from .text import Text, add_text +from .text import Text __all__ = [ "ButtonState", @@ -32,66 +32,84 @@ ] TOGGLE_BLOCK = "▊▋▌▍▎▏" -DARK_GREY = Color.from_hex("333333") +DARK_GREY = Color.from_hex("222222") LIGHT_GREY = Color.from_hex("666666") +DARK_RED = Color.from_hex("4f0908") +DARK_GREEN = Color.from_hex("0e4f08") class _AnimatedToggle(ToggleButtonBehavior, Text): - def __init__( - self, group, allow_no_selection, toggle_state, always_release, bg_color - ): + def __init__(self, group, allow_no_selection, always_release, bg_color): super().__init__( - size=(3, 4), pos_hint={"y_hint": 0.5, "x_hint": 0.5}, group=group, allow_no_selection=allow_no_selection, - toggle_state=toggle_state, always_release=always_release, ) - self._animation_task = asyncio.create_task(asyncio.sleep(0)) # dummy task + self._animation_task = None + + if self.toggle_state == "on": + self.set_text("▄▄▄▄\n█▊▊█\n▀▀▀▀") + self._animation_progess = 0 + else: + self.set_text("▄▄▄▄\n█▏▏█\n▀▀▀▀") + self._animation_progess = 5 self.canvas["bg_color"] = bg_color self.canvas["fg_color"] = DARK_GREY self.canvas["bg_color"][1, 1] = DARK_GREY + if self.toggle_state == "on": + self.update_on() + else: + self.update_off() - if self.toggle_state is ToggleState.ON: - add_text(self.canvas, "▄▄▄▄\n█▊▊█\n▀▀▀▀") - self.canvas["fg_color"][1, 1] = GREEN - self.canvas["bg_color"][1, 2] = GREEN - self._animation_progess = 0 + def update_off(self): + self.canvas["fg_color"][1, 1] = LIGHT_GREY + self.canvas["bg_color"][1, 2] = LIGHT_GREY + + def update_on(self): + self.canvas["fg_color"][1, 1] = GREEN + self.canvas["bg_color"][1, 2] = GREEN + + def update_normal(self): + if self.toggle_state == "off": + self.update_off() else: - add_text(self.canvas, "▄▄▄▄\n█▏▏█\n▀▀▀▀") - self.canvas["fg_color"][1, 1] = LIGHT_GREY - self.canvas["bg_color"][1, 2] = LIGHT_GREY - self._animation_progess = 5 + self.update_on() - def on_remove(self): - self._animation_task.cancel() + def update_hover(self): + self.update_normal() + + def update_down(self): + self.update_normal() + + def update_disallowed(self): + color = DARK_RED if self.toggle_state == "off" else DARK_GREEN + self.canvas["fg_color"][1, 1] = color + self.canvas["bg_color"][1, 2] = color async def _animate_toggle(self): - if self.toggle_state is ToggleState.ON: - self.canvas["fg_color"][1, 1] = GREEN - self.canvas["bg_color"][1, 2] = GREEN - r = range(self._animation_progess - 1, -1, -1) + if self.toggle_state == "on": + it = range(self._animation_progess - 1, -1, -1) else: - self.canvas["fg_color"][1, 1] = LIGHT_GREY - self.canvas["bg_color"][1, 2] = LIGHT_GREY - r = range(self._animation_progess + 1, 6) + it = range(self._animation_progess + 1, 6) - for i in r: + for i in it: self._animation_progess = i self.canvas["char"][1, 1:3] = TOGGLE_BLOCK[i] await asyncio.sleep(0.05) - self.parent.callback(self.toggle_state) - def on_toggle(self): - if not hasattr(self, "_animation_task"): - # Initializing... - return - self._animation_task.cancel() + if self._animation_task is not None: + self._animation_task.cancel() + if self.parent.callback is not None: + self.parent.callback(self.toggle_state) self._animation_task = asyncio.create_task(self._animate_toggle()) + def on_remove(self): + if self._animation_task is not None: + self._animation_task.cancel() + class _ToggleButtonProperty: def __set_name__(self, owner, name): @@ -113,21 +131,17 @@ class FlatToggle(Gadget): Parameters ---------- - callback : Callable[[ToggleState], None], default: lambda state: None - Called when toggle state changes. The new state is provided as first argument. + callback : Callable[[ToggleState], None] | None, default: None + Called when button is toggled. The toggle state is provided as first argument. toggle_bg_color: Color, default: BLACK Background color of toggle. - group : None | Hashable, default: None - If a group is provided, only one button in a group can be in the "on" state. + group : Hashable | None, default: None + If a group is provided, only one button in a group can be in the on state. allow_no_selection : bool, default: False If a group is provided, setting this to true allows no selection, i.e., - every button can be in the "off" state. - toggle_state : ToggleState, default: ToggleState.OFF - Initial toggle state of button. + every button can be in the off state. always_release : bool, default: False Whether a mouse up event outside the button will trigger it. - size : Size, default: Size(10, 10) - Size of gadget. size : Size, default: Size(10, 10) Size of gadget. pos : Point, default: Point(0, 0) @@ -147,10 +161,20 @@ class FlatToggle(Gadget): Attributes ---------- - callback : Callable[[ToggleState], None] - Toggle button callback. + callback : Callable[[ToggleState], None] | None + Called when button is toggled. toggle_background: Color Background color of toggle. + group : Hashable | None + If a group is provided, only one button in a group can be in the on state. + allow_no_selection : bool + If true and button is in a group, every button can be in the off state. + toggle_state : ToggleState + Toggle state of button. + always_release : bool + Whether a mouse up event outside the button will trigger it. + button_state : ButtonState + Current button state. size : Size Size of gadget. height : int @@ -256,16 +280,17 @@ class FlatToggle(Gadget): """Toggle state of button.""" always_release = _ToggleButtonProperty() """Whether a mouse up event outside the button will trigger it.""" + button_state = _ToggleButtonProperty() + """Current button state.""" def __init__( self, *, size: Size = Size(3, 4), - callback: Callable[[ToggleState], None] = lambda state: None, + callback: Callable[[ToggleState], None] = None, toggle_bg_color: Color = BLACK, group: None | Hashable = None, allow_no_selection: bool = False, - toggle_state: ToggleState = ToggleState.OFF, always_release: bool = False, pos=Point(0, 0), size_hint: SizeHint | SizeHintDict | None = None, @@ -283,13 +308,10 @@ def __init__( is_visible=is_visible, is_enabled=is_enabled, ) - self.callback = callback - self._toggle = _AnimatedToggle( group=group, allow_no_selection=allow_no_selection, - toggle_state=toggle_state, always_release=always_release, bg_color=toggle_bg_color, ) diff --git a/src/batgrl/gadgets/menu.py b/src/batgrl/gadgets/menu.py index acde14f9..0bd68a4a 100644 --- a/src/batgrl/gadgets/menu.py +++ b/src/batgrl/gadgets/menu.py @@ -8,11 +8,7 @@ from ..geometry import clamp from ..io import MouseEventType from .behaviors.themable import Themable -from .behaviors.toggle_button_behavior import ( - ButtonState, - ToggleButtonBehavior, - ToggleState, -) +from .behaviors.toggle_button_behavior import ToggleButtonBehavior, ToggleState from .grid_layout import GridLayout from .pane import ( Pane, @@ -55,7 +51,6 @@ def __init__( *, left_label: str = "", right_label: str = "", - item_disabled: bool = False, item_callback: ItemCallback = lambda: None, submenu: Optional["Menu"] = None, **kwargs, @@ -66,7 +61,6 @@ def __init__( pos_hint={"x_hint": 1.0, "anchor": "right"}, alpha=0.0, ) - self._item_disabled = item_disabled self.item_callback = item_callback self.submenu = submenu super().__init__(**kwargs) @@ -77,11 +71,11 @@ def __init__( self.update_off() def _repaint(self): - if self.item_disabled: - color_pair = self.color_theme.menu_item_disabled - elif self.state is ButtonState.NORMAL: + if self.button_state == "disallowed": + color_pair = self.color_theme.menu_item_disallowed + elif self.button_state == "normal": color_pair = self.color_theme.primary - elif self.state is ButtonState.HOVER or self.state is ButtonState.DOWN: + elif self.button_state == "hover" or self.button_state == "down": color_pair = self.color_theme.menu_item_hover self.bg_color = color_pair.bg self.left_label.canvas[["fg_color", "bg_color"]] = color_pair @@ -109,16 +103,6 @@ def is_transparent(self, is_transparent: bool): if self.submenu is not None: self.submenu.is_transparent = is_transparent - @property - def item_disabled(self) -> bool: - """If true, item will not be selectable in menu.""" - return self._item_disabled - - @item_disabled.setter - def item_disabled(self, item_disabled: bool): - self._item_disabled = item_disabled - self._repaint() - def update_theme(self): """Paint the gadget with current theme.""" self._repaint() @@ -131,7 +115,8 @@ def update_hover(self): if self.parent._current_selection not in {-1, index}: self.parent.close_submenus() if self.parent._current_selection != -1: - self.parent.children[self.parent._current_selection]._normal() + last_hovered = self.parent.children[self.parent._current_selection] + last_hovered.button_state = "normal" self.parent._current_selection = index if self.submenu is not None: @@ -149,6 +134,21 @@ def update_normal(self): elif not self.submenu.collides_point(self._last_mouse_pos): self.submenu.close_menu() + def update_disallowed(self): + self._repaint() + if self.submenu is not None: + self.submenu.close_menu() + + def update_off(self): + """Paint the off state.""" + if self.item_callback is not None and nargs(self.item_callback) == 1: + self.left_label.canvas["char"][0, 1] = CHECK_OFF + + def update_on(self): + """Paint the on state.""" + if self.item_callback is not None and nargs(self.item_callback) == 1: + self.left_label.canvas["char"][0, 1] = CHECK_ON + def on_mouse(self, mouse_event): """Save last mouse position.""" self._last_mouse_pos = mouse_event.position @@ -156,9 +156,6 @@ def on_mouse(self, mouse_event): def on_release(self): """Open submenu or call item callback on release.""" - if self.item_disabled: - return - if self.submenu is not None: self.submenu.open_menu() elif nargs(self.item_callback) == 0: @@ -166,16 +163,8 @@ def on_release(self): if self.parent.close_on_release: self.parent.close_parents() - - def update_off(self): - """Paint the off state.""" - if self.item_callback is not None and nargs(self.item_callback) == 1: - self.left_label.canvas["char"][0, 1] = CHECK_OFF - - def update_on(self): - """Paint the on state.""" - if self.item_callback is not None and nargs(self.item_callback) == 1: - self.left_label.canvas["char"][0, 1] = CHECK_ON + else: + super().on_release() def on_toggle(self): """Call item callback on toggle state change.""" @@ -450,18 +439,18 @@ def open_menu(self): def close_menu(self): """Close the menu.""" - if ( - self._menu_button is not None - and self._menu_button.toggle_state is ToggleState.ON - ): - self._menu_button.toggle_state = ToggleState.OFF + if self._menu_button is not None and self._menu_button.toggle_state == "on": + self._menu_button.toggle_state = "off" else: + if self._menu_button is not None: + self._menu_button.update_off() + self.is_enabled = False self._current_selection = -1 self.close_submenus() for child in self.children: - child._normal() + child.button_state = "normal" def close_submenus(self): """Close all submenus.""" @@ -510,7 +499,7 @@ def on_key(self, key_event): i = len(self.children) - 1 else: i = self._current_selection - self.children[i]._normal() + self.children[i].button_state = "normal" i = (i - 1) % len(self.children) for _ in self.children: @@ -530,7 +519,7 @@ def on_key(self, key_event): if i == -1: i = 0 else: - self.children[i]._normal() + self.children[i].button_state = "normal" i = (i + 1) % len(self.children) for _ in self.children: @@ -677,7 +666,7 @@ def __init__(self, label, menu, group): self._menu = menu def _repaint(self): - if self.state is not ButtonState.NORMAL or self.toggle_state is ToggleState.ON: + if self.button_state != "normal" or self.toggle_state == "on": color_pair = self.color_theme.menu_item_hover else: color_pair = self.color_theme.primary @@ -697,7 +686,7 @@ def update_normal(self): def update_hover(self): self._repaint() if self._toggle_groups.get(self.group): - self.toggle_state = ToggleState.ON + self.toggle_state = "on" def update_on(self): self._repaint() @@ -706,7 +695,7 @@ def update_off(self): self._repaint() def on_toggle(self): - if self.toggle_state is ToggleState.ON: + if self.toggle_state == "on": self._menu.open_menu() else: self._menu.close_menu() diff --git a/src/batgrl/gadgets/tabs.py b/src/batgrl/gadgets/tabs.py index 8922b8a3..6d84cd1e 100644 --- a/src/batgrl/gadgets/tabs.py +++ b/src/batgrl/gadgets/tabs.py @@ -3,11 +3,7 @@ from ..colors import lerp_colors from .behaviors.themable import Themable -from .behaviors.toggle_button_behavior import ( - ButtonState, - ToggleButtonBehavior, - ToggleState, -) +from .behaviors.toggle_button_behavior import ToggleButtonBehavior from .gadget import ( Gadget, Point, @@ -45,7 +41,7 @@ def __init__(self, title, content, **kwargs): def on_toggle(self): if self.parent is None: return - if self.toggle_state is ToggleState.ON: + if self.toggle_state == "on": self.content.is_enabled = True tabbed: Tabs = self.parent.parent @@ -69,10 +65,10 @@ def on_toggle(self): self.content.is_enabled = False def _update(self): - if self.toggle_state is ToggleState.ON: + if self.toggle_state == "on": color_pair = self.color_theme.titlebar_normal bold = True - elif self.state is ButtonState.HOVER: + elif self.button_state == "hover": color_pair = self.hover_color_pair bold = False else: @@ -329,7 +325,7 @@ def add_tab(self, title: str, content: Gadget): self.tab_window.add_gadget(content) self._history.append(title) - self.tabs[title].toggle_state = ToggleState.ON + self.tabs[title].toggle_state = "on" self._tab_underline.is_enabled = True def remove_tab(self, title: str): @@ -353,7 +349,7 @@ def remove_tab(self, title: str): if self._active_tab is title: if self._history: self._active_tab = self._history[-1] - self.tabs[self._active_tab].toggle_state = ToggleState.ON + self.tabs[self._active_tab].toggle_state = "on" else: self._active_tab = None self._tab_underline.is_enabled = False diff --git a/src/batgrl/gadgets/toggle_button.py b/src/batgrl/gadgets/toggle_button.py index 68000203..3a5eb782 100644 --- a/src/batgrl/gadgets/toggle_button.py +++ b/src/batgrl/gadgets/toggle_button.py @@ -52,13 +52,11 @@ class ToggleButton(Themable, ToggleButtonBehavior, Gadget): Called when toggle state changes. The new state is provided as first argument. alpha : float, default: 1.0 Transparency of gadget. - group : None | Hashable, default: None - If a group is provided, only one button in a group can be in the "on" state. + group : Hashable | None, default: None + If a group is provided, only one button in a group can be in the on state. allow_no_selection : bool, default: False If a group is provided, setting this to true allows no selection, i.e., - every button can be in the "off" state. - toggle_state : ToggleState, default: ToggleState.OFF - Initial toggle state of button. + every button can be in the off state. always_release : bool, default: False Whether a mouse up event outside the button will trigger it. size : Size, default: Size(10, 10) @@ -86,16 +84,16 @@ class ToggleButton(Themable, ToggleButtonBehavior, Gadget): Called when toggle state changes. alpha : float Transparency of gadget. - group : None | Hashable - If a group is provided, only one button in a group can be in the "on" state. + group : Hashable | None + If a group is provided, only one button in a group can be in the on state. allow_no_selection : bool - If true and button is in a group, every button can be in the "off" state. + If true and button is in a group, every button can be in the off state. toggle_state : ToggleState Toggle state of button. always_release : bool Whether a mouse up event outside the button will trigger it. - state : ButtonState - Current button state. One of `NORMAL`, `HOVER`, `DOWN`. + button_state : ButtonState + Current button state. size : Size Size of gadget. height : int @@ -147,20 +145,22 @@ class ToggleButton(Themable, ToggleButtonBehavior, Gadget): ------- update_theme() Paint the gadget with current theme. + on_toggle() + Triggled on toggle state change. update_off() - Paint the "off" state. + Paint the off state. update_on() - Paint the "on" state. - on_toggle() - Update gadget on toggle state change. + Paint the on state. + on_release() + Triggered when a button is released. update_normal() Paint the normal state. update_hover() Paint the hover state. update_down() Paint the down state. - on_release() - Triggered when a button is released. + update_disallowed() + Paint the disallowed state. on_size() Update gadget after a resize. apply_hints() @@ -217,7 +217,6 @@ def __init__( alpha: float = 1.0, group: None | Hashable = None, allow_no_selection: bool = False, - toggle_state: ToggleState = ToggleState.OFF, always_release: bool = False, size=Size(10, 10), pos=Point(0, 0), @@ -234,7 +233,6 @@ def __init__( super().__init__( group=group, allow_no_selection=allow_no_selection, - toggle_state=toggle_state, always_release=always_release, size=size, pos=pos, @@ -281,7 +279,7 @@ def label(self, label: str): else: on, off = TOGGLE_ON, TOGGLE_OFF - if self.toggle_state is ToggleState.ON: + if self.toggle_state == "on": prefix = on else: prefix = off @@ -290,25 +288,27 @@ def label(self, label: str): def update_theme(self): """Paint the gadget with current theme.""" - match self.state: - case ButtonState.NORMAL: - self.update_normal() - case ButtonState.HOVER: - self.update_hover() - case ButtonState.DOWN: - self.update_down() + getattr(self, f"update_{self.button_state}")() def update_normal(self): """Paint the normal state.""" self._pane.bg_color = self.color_theme.button_normal.bg + self._label.canvas["fg_color"] = self.color_theme.button_normal.fg def update_hover(self): """Paint the hover state.""" self._pane.bg_color = self.color_theme.button_hover.bg + self._label.canvas["fg_color"] = self.color_theme.button_hover.fg def update_down(self): """Paint the down state.""" self._pane.bg_color = self.color_theme.button_press.bg + self._label.canvas["fg_color"] = self.color_theme.button_press.fg + + def update_disallowed(self): + """Paint the disallowed state.""" + self._pane.bg_color = self.color_theme.button_disallowed.bg + self._label.canvas["fg_color"] = self.color_theme.button_disallowed.fg def on_toggle(self): """Call callback on toggle state change.""" diff --git a/src/batgrl/gadgets/tree_view.py b/src/batgrl/gadgets/tree_view.py index e4b7cc93..f4ce8fae 100644 --- a/src/batgrl/gadgets/tree_view.py +++ b/src/batgrl/gadgets/tree_view.py @@ -249,7 +249,7 @@ def __init__( def _repaint(self): if self.is_selected: color_pair = self.color_theme.menu_item_selected - elif self.state is ButtonState.NORMAL: + elif self.button_state == "normal": color_pair = self.color_theme.primary else: color_pair = self.color_theme.menu_item_hover