From f495870b08fe90ca345dd4be61c57cdca2ec77d9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 10:12:49 +0000 Subject: [PATCH 01/54] Remove the disabled styling from `Input` This seems to be a hangover from the early days of the development of `Input`, and the styles do nothing as there's nothing else in the `Input` code that makes use of the class that's involved. Removed in anticipation of #1748 taking care of this. --- src/textual/widgets/_input.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 8528e225ab..f11dd7df8f 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -110,9 +110,6 @@ class Input(Widget, can_focus=True): height: 1; min-height: 1; } - Input.-disabled { - opacity: 0.6; - } Input:focus { border: tall $accent; } @@ -121,9 +118,6 @@ class Input(Widget, can_focus=True): color: $text; text-style: reverse; } - Input>.input--placeholder { - color: $text-disabled; - } """ cursor_blink = reactive(True) From 04c207627cb520e6a1887afa832e7b1ed80e53a1 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 10:49:31 +0000 Subject: [PATCH 02/54] Put the placeholder styling background f495870b08fe90ca345dd4be61c57cdca2ec77d9 got a little too carried away. --- src/textual/widgets/_input.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index f11dd7df8f..c652da9a5f 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -118,6 +118,9 @@ class Input(Widget, can_focus=True): color: $text; text-style: reverse; } + Input>.input--placeholder { + color: $text-disabled; + } """ cursor_blink = reactive(True) From 7aaf8842823b98472618e57cf7fedb9087a850fa Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 12:40:37 +0000 Subject: [PATCH 03/54] Move disabled out of `Button` and into `Widget` This doesn't go close to what #1748 is intending to do, but moves `disabled` to where I want it and keeps `Button` working as before. --- src/textual/widget.py | 2 ++ src/textual/widgets/_button.py | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 197eb6147b..53fe29983d 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -225,6 +225,8 @@ class Widget(DOMNode): """Rich renderable may shrink.""" auto_links = Reactive(True) """Widget will highlight links automatically.""" + disabled = Reactive(False) + """The disabled state of the widget. `True` if disabled, `False if not.""" hover_style: Reactive[Style] = Reactive(Style, repaint=False) highlight_link_id: Reactive[str] = Reactive("") diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index fbeb60645a..a06248b53d 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -156,9 +156,6 @@ class Button(Static, can_focus=True): variant = reactive("default") """The variant name for the button.""" - disabled = reactive(False) - """The disabled state of the button; `True` if disabled, `False` if not.""" - class Pressed(Message, bubble=True): """Event sent when a `Button` is pressed. From 257fe7b30ae62d661924a7bda9a5500700f2a031 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 12:56:52 +0000 Subject: [PATCH 04/54] Add enabled and disabled pseudo-classes Note that this doesn't touch the application of stylesheets yet, in terms of things like specificity; this just makes sure that the classes exist and can be seen. --- docs/guide/CSS.md | 2 ++ src/textual/widget.py | 1 + 2 files changed, 3 insertions(+) diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md index 80753ddbd0..985c4b41f4 100644 --- a/docs/guide/CSS.md +++ b/docs/guide/CSS.md @@ -315,6 +315,8 @@ The `background: green` is only applied to the Button underneath the mouse curso Here are some other pseudo classes: +- `:disabled` Matches widgets which are in a disabled state. +- `:enabled` Matches widgets which are in an enabled state. - `:focus` Matches widgets which have input focus. - `:focus-within` Matches widgets with a focused a child widget. diff --git a/src/textual/widget.py b/src/textual/widget.py index 53fe29983d..5cb2783896 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2084,6 +2084,7 @@ def get_pseudo_classes(self) -> Iterable[str]: Names of the pseudo classes. """ + yield "disabled" if self.disabled else "enabled" if self.mouse_over: yield "hover" if self.has_focus: From a0a83e96adad1b5437829a36c53ae4b3242308f9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 13:04:01 +0000 Subject: [PATCH 05/54] Correct docstring typo --- src/textual/widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 5cb2783896..759da55a36 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -226,7 +226,7 @@ class Widget(DOMNode): auto_links = Reactive(True) """Widget will highlight links automatically.""" disabled = Reactive(False) - """The disabled state of the widget. `True` if disabled, `False if not.""" + """The disabled state of the widget. `True` if disabled, `False` if not.""" hover_style: Reactive[Style] = Reactive(Style, repaint=False) highlight_link_id: Reactive[str] = Reactive("") From d3104a92c9e16eb6ebf2c5b498b211df35e06387 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 13:04:59 +0000 Subject: [PATCH 06/54] Add `disabled` as a widget construction keyword argument --- src/textual/widget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/textual/widget.py b/src/textual/widget.py index 759da55a36..1913d6e841 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -237,6 +237,7 @@ def __init__( name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool | None = None, ) -> None: self._size = Size(0, 0) self._container_size = Size(0, 0) @@ -245,6 +246,7 @@ def __init__( self._default_layout = VerticalLayout() self._animate: BoundAnimator | None = None self.highlight_style: Style | None = None + self.disabled = bool(disabled) self._vertical_scrollbar: ScrollBar | None = None self._horizontal_scrollbar: ScrollBar | None = None From 821a6ff7c1d2e127d2c3d3bd6786cd80c2f8c0a6 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 13:09:12 +0000 Subject: [PATCH 07/54] Simplify the default for disabled on a Widget There was no need to default to None and then convert to a bool, defaulting to False is just fine. --- src/textual/widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 1913d6e841..ee9492fba6 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -237,7 +237,7 @@ def __init__( name: str | None = None, id: str | None = None, classes: str | None = None, - disabled: bool | None = None, + disabled: bool = False, ) -> None: self._size = Size(0, 0) self._container_size = Size(0, 0) @@ -246,7 +246,7 @@ def __init__( self._default_layout = VerticalLayout() self._animate: BoundAnimator | None = None self.highlight_style: Style | None = None - self.disabled = bool(disabled) + self.disabled = disabled self._vertical_scrollbar: ScrollBar | None = None self._horizontal_scrollbar: ScrollBar | None = None From 3707f80aa4b8f6cb56e7624d2f23f8716fca7a5a Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 14:19:36 +0000 Subject: [PATCH 08/54] Move duplicate safe calls to update_styles into a single method It's done as an internal, but can be called from child classes of course. This is intended to be a single central method of asking the app to update styles while also not caring if there is no active app available just yet. --- src/textual/dom.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 1ed204aa9e..447ab3cf2b 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -874,6 +874,16 @@ def set_class(self, add: bool, *class_names: str) -> None: else: self.remove_class(*class_names) + def _update_styles(self) -> None: + """Request an update of this node's styles. + + Should be called whenever CSS classes / pseudo classes change. + """ + try: + self.app.update_styles(self) + except NoActiveAppError: + pass + def add_class(self, *class_names: str) -> None: """Add class names to this Node. @@ -886,10 +896,7 @@ def add_class(self, *class_names: str) -> None: self._classes.update(class_names) if old_classes == self._classes: return - try: - self.app.update_styles(self) - except NoActiveAppError: - pass + self._update_styles() def remove_class(self, *class_names: str) -> None: """Remove class names from this Node. @@ -903,10 +910,7 @@ def remove_class(self, *class_names: str) -> None: self._classes.difference_update(class_names) if old_classes == self._classes: return - try: - self.app.update_styles(self) - except NoActiveAppError: - pass + self._update_styles() def toggle_class(self, *class_names: str) -> None: """Toggle class names on this Node. @@ -920,10 +924,7 @@ def toggle_class(self, *class_names: str) -> None: self._classes.symmetric_difference_update(class_names) if old_classes == self._classes: return - try: - self.app.update_styles(self) - except NoActiveAppError: - pass + self._update_styles() def has_pseudo_class(self, *class_names: str) -> bool: """Check for pseudo class (such as hover, focus etc)""" From d37895dfdd855a71ed6c7718f41ee0562f47e58d Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 14:23:20 +0000 Subject: [PATCH 09/54] Convert some app.update_styles calls into _update_styles calls --- src/textual/widget.py | 8 ++++---- src/textual/widgets/_button.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index ee9492fba6..7f34862647 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2134,11 +2134,11 @@ def post_render(self, renderable: RenderableType) -> ConsoleRenderable: def watch_mouse_over(self, value: bool) -> None: """Update from CSS if mouse over state changes.""" if self._has_hover_style: - self.app.update_styles(self) + self._update_styles() def watch_has_focus(self, value: bool) -> None: """Update from CSS if has focus state changes.""" - self.app.update_styles(self) + self._update_styles() def _size_updated( self, size: Size, virtual_size: Size, container_size: Size @@ -2486,11 +2486,11 @@ def _on_blur(self, event: events.Blur) -> None: def _on_descendant_blur(self, event: events.DescendantBlur) -> None: if self._has_focus_within: - self.app.update_styles(self) + self._update_styles() def _on_descendant_focus(self, event: events.DescendantBlur) -> None: if self._has_focus_within: - self.app.update_styles(self) + self._update_styles() def _on_mouse_scroll_down(self, event: events.MouseScrollDown) -> None: if event.ctrl or event.shift: diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index a06248b53d..ceccb8fc7b 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -211,7 +211,7 @@ def __rich_repr__(self) -> rich.repr.Result: def watch_mouse_over(self, value: bool) -> None: """Update from CSS if mouse over state changes.""" if self._has_hover_style and not self.disabled: - self.app.update_styles(self) + self._update_styles() def validate_variant(self, variant: str) -> str: if variant not in _VALID_BUTTON_VARIANTS: From 1097fb267d7882ba3812126efd0bbf82f04daac7 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 14:25:40 +0000 Subject: [PATCH 10/54] Set the initial disabled state later in __init__ I was going too early with setting this; it needs to happen after pretty much everything else is set up *and* after the super's __init__ has been called. --- src/textual/widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 7f34862647..6efc732b88 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -246,7 +246,6 @@ def __init__( self._default_layout = VerticalLayout() self._animate: BoundAnimator | None = None self.highlight_style: Style | None = None - self.disabled = disabled self._vertical_scrollbar: ScrollBar | None = None self._horizontal_scrollbar: ScrollBar | None = None @@ -281,6 +280,7 @@ def __init__( raise WidgetError("A widget can't be its own parent") self._add_children(*children) + self.disabled = disabled virtual_size = Reactive(Size(0, 0), layout=True) auto_width = Reactive(True) From bbdc70a620a615c1018adb25afae3d515cdcba2a Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 14:28:10 +0000 Subject: [PATCH 11/54] Move the main handling of disabled up to `Widget` There's still a bit to do here, but this migrates the main work up to the `Widget`. At this point `Button` is pretty much built expressed as a function of what `Widget` provides in terms of things being disabled. Focus can still move into disabled controls (or in this case right now, into a disabled `Button`). The next step is to add something that works alongside `can_focus` to say if a control is currently capable of receiving focus (in other words, it's `not disabled and can_focus`). --- src/textual/widget.py | 4 ++++ src/textual/widgets/_button.py | 30 +++++++++++++----------------- src/textual/widgets/_static.py | 3 ++- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 6efc732b88..bd32de442d 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2140,6 +2140,10 @@ def watch_has_focus(self, value: bool) -> None: """Update from CSS if has focus state changes.""" self._update_styles() + def watch_disabled(self) -> None: + # self.can_focus = not self.disabled + self._update_styles() + def _size_updated( self, size: Size, virtual_size: Size, container_size: Size ) -> None: diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index ceccb8fc7b..01877054a2 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -39,7 +39,7 @@ class Button(Static, can_focus=True): text-style: bold; } - Button.-disabled { + Button:disabled { opacity: 0.4; text-opacity: 0.7; } @@ -173,34 +173,30 @@ def button(self) -> Button: def __init__( self, label: TextType | None = None, - disabled: bool = False, variant: ButtonVariant = "default", *, name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ): """Create a Button widget. Args: label: The text that appears within the button. - disabled: Whether the button is disabled or not. variant: The variant of the button. name: The name of the button. id: The ID of the button in the DOM. classes: The CSS classes of the button. + disabled: Whether the button is disabled or not. """ - super().__init__(name=name, id=id, classes=classes) + super().__init__(name=name, id=id, classes=classes, disabled=disabled) if label is None: label = self.css_identifier_styled self.label = self.validate_label(label) - self.disabled = disabled - if disabled: - self.add_class("-disabled") - self.variant = self.validate_variant(variant) def __rich_repr__(self) -> rich.repr.Result: @@ -224,9 +220,9 @@ def watch_variant(self, old_variant: str, variant: str): self.remove_class(f"-{old_variant}") self.add_class(f"-{variant}") - def watch_disabled(self, disabled: bool) -> None: - self.set_class(disabled, "-disabled") - self.can_focus = not disabled + # def watch_disabled(self, disabled: bool) -> None: + # self.set_class(disabled, "-disabled") + # self.can_focus = not disabled def validate_label(self, label: RenderableType) -> RenderableType: """Parse markup for self.label""" @@ -269,11 +265,11 @@ async def _on_key(self, event: events.Key) -> None: def success( cls, label: TextType | None = None, - disabled: bool = False, *, name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ) -> Button: """Utility constructor for creating a success Button variant. @@ -289,22 +285,22 @@ def success( """ return Button( label=label, - disabled=disabled, variant="success", name=name, id=id, classes=classes, + disabled=disabled, ) @classmethod def warning( cls, label: TextType | None = None, - disabled: bool = False, *, name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ) -> Button: """Utility constructor for creating a warning Button variant. @@ -320,22 +316,22 @@ def warning( """ return Button( label=label, - disabled=disabled, variant="warning", name=name, id=id, classes=classes, + disabled=disabled, ) @classmethod def error( cls, label: TextType | None = None, - disabled: bool = False, *, name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ) -> Button: """Utility constructor for creating an error Button variant. @@ -351,9 +347,9 @@ def error( """ return Button( label=label, - disabled=disabled, variant="error", name=name, id=id, classes=classes, + disabled=disabled, ) diff --git a/src/textual/widgets/_static.py b/src/textual/widgets/_static.py index a9698a9544..d2c2edab26 100644 --- a/src/textual/widgets/_static.py +++ b/src/textual/widgets/_static.py @@ -56,8 +56,9 @@ def __init__( name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ) -> None: - super().__init__(name=name, id=id, classes=classes) + super().__init__(name=name, id=id, classes=classes, disabled=disabled) self.expand = expand self.shrink = shrink self.markup = markup From 682c4de06d0369efff367eb77d10e627ee8d41ff Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 15:11:33 +0000 Subject: [PATCH 12/54] Remove unnecessary comment Don't use comments as version control! Or, really, I don't need this note to self any more about the code as it's being handled elsewhere. --- src/textual/widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index bd32de442d..188d80ec4b 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2141,7 +2141,6 @@ def watch_has_focus(self, value: bool) -> None: self._update_styles() def watch_disabled(self) -> None: - # self.can_focus = not self.disabled self._update_styles() def _size_updated( From 14f83a0a4abc03ffbd4a3edcc974b9eebc0d7f04 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 15:39:19 +0000 Subject: [PATCH 13/54] Remove commented out code from Button Some hangover from the work to migrate `disabled` out of `Button` and into `Widget`, that I forgot to remove. --- src/textual/widget.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/textual/widget.py b/src/textual/widget.py index 188d80ec4b..9849bc924b 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1177,6 +1177,11 @@ def virtual_region_with_margin(self) -> Region: """ return self.virtual_region.grow(self.styles.margin) + @property + def focusable(self) -> bool: + """Can this widget currently receive focus?""" + return self.can_focus and not self.disabled + @property def focusable_children(self) -> list[Widget]: """Get the children which may be focused. From 500458e5f386923f05061cea6b71a25cd9f10a3a Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 15:39:19 +0000 Subject: [PATCH 14/54] Add a focusable property We want to maintain `can_focus` as "this is a thing that can, at some point, receive focus, assuming it isn't disabled". So `can_focus` is now very much about the ability to receive focus at all. The new `focusable` property becomes about "can this widget receive focus right now?". [This is a rewording of a previous commit -- I get the wrong thing] --- src/textual/widget.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/textual/widget.py b/src/textual/widget.py index 188d80ec4b..9849bc924b 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1177,6 +1177,11 @@ def virtual_region_with_margin(self) -> Region: """ return self.virtual_region.grow(self.styles.margin) + @property + def focusable(self) -> bool: + """Can this widget currently receive focus?""" + return self.can_focus and not self.disabled + @property def focusable_children(self) -> list[Widget]: """Get the children which may be focused. From 8379945b60a89be47ebe29630966cc1dbf69864f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 15:43:40 +0000 Subject: [PATCH 15/54] Remove commented out code from Button Some hangover from the work to migrate `disabled` out of `Button` and into `Widget`, that I forgot to remove. --- src/textual/widgets/_button.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index 01877054a2..fbe49279c4 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -220,10 +220,6 @@ def watch_variant(self, old_variant: str, variant: str): self.remove_class(f"-{old_variant}") self.add_class(f"-{variant}") - # def watch_disabled(self, disabled: bool) -> None: - # self.set_class(disabled, "-disabled") - # self.can_focus = not disabled - def validate_label(self, label: RenderableType) -> RenderableType: """Parse markup for self.label""" if isinstance(label, str): From 0171ad7c85d8b81bd148503e486ed64b603af83f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 15:44:02 +0000 Subject: [PATCH 16/54] Build the focus chain from focusable widgets Rather than it being about widgets where `can_focus` is `True`, have it be about widgets that are currently able to receive focus. Before this change widgets with a positive `can_focus` but which were disabled would still end up in the chain. --- src/textual/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index d1354a106f..33848f5295 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -177,7 +177,7 @@ def focus_chain(self) -> list[Widget]: else: if node.is_container and node.can_focus_children: push(iter(node.focusable_children)) - if node.can_focus: + if node.focusable: add_widget(node) return widgets From 850c140a05d5174fc4dceb719d7f6ec87008eb7f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 16:10:28 +0000 Subject: [PATCH 17/54] Swap `set_focus` over to checking focusable --- src/textual/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index 33848f5295..c8ccb884ec 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -351,7 +351,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: self.focused.post_message_no_wait(events.Blur(self)) self.focused = None self.log.debug("focus was removed") - elif widget.can_focus: + elif widget.focusable: if self.focused != widget: if self.focused is not None: # Blur currently focused widget From 813c89c1ef3453f98947cfbe8a6234ae99077ec2 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 16:12:28 +0000 Subject: [PATCH 18/54] Swap _forward_event over to focsuable (from can_focus) --- src/textual/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index c8ccb884ec..34c3db79cf 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -547,7 +547,7 @@ async def _forward_event(self, event: events.Event) -> None: except errors.NoWidget: self.set_focus(None) else: - if isinstance(event, events.MouseUp) and widget.can_focus: + if isinstance(event, events.MouseUp) and widget.focusable: if self.focused is not widget: self.set_focus(widget) event.stop() From d148dd6237ed728cfef5220210f6b6415e0e53b8 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 16:14:13 +0000 Subject: [PATCH 19/54] Swap _reset_focus over to focsuable (from can_focus) --- src/textual/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index 34c3db79cf..34fdad3d0f 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -314,7 +314,7 @@ def _reset_focus( # It may have been made invisible # Move to a sibling if possible for sibling in widget.visible_siblings: - if sibling not in avoiding and sibling.can_focus: + if sibling not in avoiding and sibling.focusable: self.set_focus(sibling) break else: From 525455bf76b4685738a6f51097e93a15217f14ec Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 16:19:38 +0000 Subject: [PATCH 20/54] Bring the focus_chain docstring in line with out newer guidelines --- src/textual/screen.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index 34fdad3d0f..ef73c8a63e 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -159,11 +159,7 @@ def find_widget(self, widget: Widget) -> MapGeometry: @property def focus_chain(self) -> list[Widget]: - """Get widgets that may receive focus, in focus order. - - Returns: - List of Widgets in focus order. - """ + """A list of widgets that may receive focus, in focus order.""" widgets: list[Widget] = [] add_widget = widgets.append stack: list[Iterator[Widget]] = [iter(self.focusable_children)] From ea8470ee7a8600d3f2f1e4777f0dd0385c3ac0d5 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 13 Feb 2023 16:55:08 +0000 Subject: [PATCH 21/54] Stop mouse events going to a widget that can't receive focus right now --- src/textual/widget.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/textual/widget.py b/src/textual/widget.py index 9849bc924b..80f32c5812 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2436,6 +2436,14 @@ def release_mouse(self) -> None: """ self.app.capture_mouse(None) + def check_message_enabled(self, message: Message) -> bool: + # Do the normal checking and get out if that fails. + if not super().check_message_enabled(message): + return False + # Otherwise, if this is a mouse event the widget receiving the event + # must be capable of being focused right at this moment. + return self.focusable if isinstance(message, events.MouseEvent) else True + async def broker_event(self, event_name: str, event: events.Event) -> bool: return await self.app._broker_event(event_name, event, default_namespace=self) From c7990d990d3eb176e353e470e5b08e4c511ebb0a Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 09:56:09 +0000 Subject: [PATCH 22/54] Add support for disabling a widget and all of its children --- src/textual/widget.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 80f32c5812..83f853bb8f 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1177,10 +1177,19 @@ def virtual_region_with_margin(self) -> Region: """ return self.virtual_region.grow(self.styles.margin) + @property + def _self_or_ancestors_disabled(self) -> bool: + """Is this widget or any of its ancestors disabled?""" + return any( + node.disabled + for node in self.ancestors_with_self + if isinstance(node, Widget) + ) + @property def focusable(self) -> bool: """Can this widget currently receive focus?""" - return self.can_focus and not self.disabled + return self.can_focus and not self._self_or_ancestors_disabled @property def focusable_children(self) -> list[Widget]: @@ -2091,7 +2100,7 @@ def get_pseudo_classes(self) -> Iterable[str]: Names of the pseudo classes. """ - yield "disabled" if self.disabled else "enabled" + yield "disabled" if self._self_or_ancestors_disabled else "enabled" if self.mouse_over: yield "hover" if self.has_focus: @@ -2146,7 +2155,8 @@ def watch_has_focus(self, value: bool) -> None: self._update_styles() def watch_disabled(self) -> None: - self._update_styles() + for node in self.walk_children(with_self=True): + node._update_styles() def _size_updated( self, size: Size, virtual_size: Size, container_size: Size From 674ee26b88f5fcbbf16f84f9e0496fbfc76c9670 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 10:40:17 +0000 Subject: [PATCH 23/54] Add a docstring to watch_disabled --- src/textual/widget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widget.py b/src/textual/widget.py index 5c817fd6ad..cf1c4f0a9c 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2153,6 +2153,7 @@ def watch_has_focus(self, value: bool) -> None: self._update_styles() def watch_disabled(self) -> None: + """Update the styles of the widget and its children when disabled is toggled.""" for node in self.walk_children(with_self=True): node._update_styles() From d27d9f81affc1e9a185cf4be13f000a401dac314 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 10:51:32 +0000 Subject: [PATCH 24/54] Only block mouse events if disabled, not if not focusable --- src/textual/widget.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index cf1c4f0a9c..53c1585f9c 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2449,9 +2449,13 @@ def check_message_enabled(self, message: Message) -> bool: # Do the normal checking and get out if that fails. if not super().check_message_enabled(message): return False - # Otherwise, if this is a mouse event the widget receiving the event - # must be capable of being focused right at this moment. - return self.focusable if isinstance(message, events.MouseEvent) else True + # Otherwise, if this is a mouse event, the widget receiving the + # event must not be disabled at this moment. + return ( + not self._self_or_ancestors_disabled + if isinstance(message, events.MouseEvent) + else True + ) async def broker_event(self, event_name: str, event: events.Event) -> bool: return await self.app._broker_event(event_name, event, default_namespace=self) From 7c81247a95538160aa3b638da617c1969c8c2fcd Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 11:22:25 +0000 Subject: [PATCH 25/54] Remove Button.watch_mouse_over This isn't needed any more. --- src/textual/widgets/_button.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index fbe49279c4..fdae0128b5 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -204,11 +204,6 @@ def __rich_repr__(self) -> rich.repr.Result: yield "variant", self.variant, "default" yield "disabled", self.disabled, False - def watch_mouse_over(self, value: bool) -> None: - """Update from CSS if mouse over state changes.""" - if self._has_hover_style and not self.disabled: - self._update_styles() - def validate_variant(self, variant: str) -> str: if variant not in _VALID_BUTTON_VARIANTS: raise InvalidButtonVariant( From 068aa07a5e8cc04eeeaea7cff0fb541eeda4ba20 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 11:28:31 +0000 Subject: [PATCH 26/54] Only enable Enter and Leave if not disabled Until now I was just removing all events that inherited from MouseEvent when checking if an event was enabled for a given widget, in relation to the widget itself being disabled. Enter and Leave also need to be taken into account; they don't inherit from MouseEvent (they're more reactions to mouse events) but should be handled too. --- src/textual/widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 53c1585f9c..eaf537ade2 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2453,7 +2453,7 @@ def check_message_enabled(self, message: Message) -> bool: # event must not be disabled at this moment. return ( not self._self_or_ancestors_disabled - if isinstance(message, events.MouseEvent) + if isinstance(message, (events.MouseEvent, events.Enter, events.Leave)) else True ) From cccf7afccbc91ad091cf549c32f87f1083acf6b6 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 11:30:55 +0000 Subject: [PATCH 27/54] Remove :disabled styling from the `Button` This will move up to the App's default styles. --- src/textual/widgets/_button.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index fdae0128b5..d9da388f50 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -39,11 +39,6 @@ class Button(Static, can_focus=True): text-style: bold; } - Button:disabled { - opacity: 0.4; - text-opacity: 0.7; - } - Button:focus { text-style: bold reverse; } From c65278d0388a1e4682aadb8512edd8c945199a6d Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 11:31:31 +0000 Subject: [PATCH 28/54] Add default disabled styling for the whole application --- src/textual/app.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/textual/app.py b/src/textual/app.py index 46a11d9bc0..0a425c8398 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -241,6 +241,11 @@ class App(Generic[ReturnType], DOMNode): background: $background; color: $text; } + + *:disabled { + opacity: 0.6; + text-opacity: 0.8; + } """ SCREENS: dict[str, Screen | Callable[[], Screen]] = {} From 3c8f4648183af5e742d7d1e2b3e785d334ff38df Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 11:44:36 +0000 Subject: [PATCH 29/54] Update the snapshots The disabled styling has changed slightly, and there is a snapshot test for buttons which is now thrown off. This updates that. --- .../__snapshots__/test_snapshots.ambr | 172 +++++++++--------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index a9f31e2aa9..c1f1dfb467 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -181,162 +181,162 @@ font-weight: 700; } - .terminal-3615181303-matrix { + .terminal-2059425018-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3615181303-title { + .terminal-2059425018-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3615181303-r1 { fill: #e1e1e1 } - .terminal-3615181303-r2 { fill: #c5c8c6 } - .terminal-3615181303-r3 { fill: #e1e1e1;font-weight: bold } - .terminal-3615181303-r4 { fill: #454a50 } - .terminal-3615181303-r5 { fill: #292b2e } - .terminal-3615181303-r6 { fill: #24292f;font-weight: bold } - .terminal-3615181303-r7 { fill: #555657;font-weight: bold } - .terminal-3615181303-r8 { fill: #000000 } - .terminal-3615181303-r9 { fill: #161617 } - .terminal-3615181303-r10 { fill: #507bb3 } - .terminal-3615181303-r11 { fill: #283c52 } - .terminal-3615181303-r12 { fill: #dde6ed;font-weight: bold } - .terminal-3615181303-r13 { fill: #4f5a62;font-weight: bold } - .terminal-3615181303-r14 { fill: #001541 } - .terminal-3615181303-r15 { fill: #122032 } - .terminal-3615181303-r16 { fill: #7ae998 } - .terminal-3615181303-r17 { fill: #3d6a4a } - .terminal-3615181303-r18 { fill: #0a180e;font-weight: bold } - .terminal-3615181303-r19 { fill: #1e2f23;font-weight: bold } - .terminal-3615181303-r20 { fill: #008139 } - .terminal-3615181303-r21 { fill: #1b4c2f } - .terminal-3615181303-r22 { fill: #ffcf56 } - .terminal-3615181303-r23 { fill: #775f2f } - .terminal-3615181303-r24 { fill: #211505;font-weight: bold } - .terminal-3615181303-r25 { fill: #392b18;font-weight: bold } - .terminal-3615181303-r26 { fill: #b86b00 } - .terminal-3615181303-r27 { fill: #644316 } - .terminal-3615181303-r28 { fill: #e76580 } - .terminal-3615181303-r29 { fill: #683540 } - .terminal-3615181303-r30 { fill: #f5e5e9;font-weight: bold } - .terminal-3615181303-r31 { fill: #6c595e;font-weight: bold } - .terminal-3615181303-r32 { fill: #780028 } - .terminal-3615181303-r33 { fill: #491928 } + .terminal-2059425018-r1 { fill: #e1e1e1 } + .terminal-2059425018-r2 { fill: #c5c8c6 } + .terminal-2059425018-r3 { fill: #e1e1e1;font-weight: bold } + .terminal-2059425018-r4 { fill: #454a50 } + .terminal-2059425018-r5 { fill: #313437 } + .terminal-2059425018-r6 { fill: #24292f;font-weight: bold } + .terminal-2059425018-r7 { fill: #7c7d7e;font-weight: bold } + .terminal-2059425018-r8 { fill: #000000 } + .terminal-2059425018-r9 { fill: #101011 } + .terminal-2059425018-r10 { fill: #507bb3 } + .terminal-2059425018-r11 { fill: #324f70 } + .terminal-2059425018-r12 { fill: #dde6ed;font-weight: bold } + .terminal-2059425018-r13 { fill: #75828b;font-weight: bold } + .terminal-2059425018-r14 { fill: #001541 } + .terminal-2059425018-r15 { fill: #0c1e39 } + .terminal-2059425018-r16 { fill: #7ae998 } + .terminal-2059425018-r17 { fill: #4f9262 } + .terminal-2059425018-r18 { fill: #0a180e;font-weight: bold } + .terminal-2059425018-r19 { fill: #192e1f;font-weight: bold } + .terminal-2059425018-r20 { fill: #008139 } + .terminal-2059425018-r21 { fill: #156034 } + .terminal-2059425018-r22 { fill: #ffcf56 } + .terminal-2059425018-r23 { fill: #a4823a } + .terminal-2059425018-r24 { fill: #211505;font-weight: bold } + .terminal-2059425018-r25 { fill: #3a2a13;font-weight: bold } + .terminal-2059425018-r26 { fill: #b86b00 } + .terminal-2059425018-r27 { fill: #825210 } + .terminal-2059425018-r28 { fill: #e76580 } + .terminal-2059425018-r29 { fill: #904354 } + .terminal-2059425018-r30 { fill: #f5e5e9;font-weight: bold } + .terminal-2059425018-r31 { fill: #978186;font-weight: bold } + .terminal-2059425018-r32 { fill: #780028 } + .terminal-2059425018-r33 { fill: #5b132a } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ButtonsApp + ButtonsApp - - - - - Standard ButtonsDisabled Buttons - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - DefaultDefault - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Primary!Primary! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Success!Success! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Warning!Warning! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Error!Error! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - + + + + + Standard ButtonsDisabled Buttons + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + DefaultDefault + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Primary!Primary! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Success!Success! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Warning!Warning! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Error!Error! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + From 9e4d4aae5e7bb7d98bf41b7acceb6af4f72005d4 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 12:50:58 +0000 Subject: [PATCH 30/54] Add a disabled keyword argument to the Switch constructor --- src/textual/widgets/_switch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_switch.py b/src/textual/widgets/_switch.py index 45e856dd17..97e3e22233 100644 --- a/src/textual/widgets/_switch.py +++ b/src/textual/widgets/_switch.py @@ -100,6 +100,7 @@ def __init__( name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ): """Initialise the switch. @@ -109,8 +110,9 @@ def __init__( name: The name of the switch. id: The ID of the switch in the DOM. classes: The CSS classes of the switch. + disabled: Whether the switch is disabled or not. """ - super().__init__(name=name, id=id, classes=classes) + super().__init__(name=name, id=id, classes=classes, disabled=disabled) if value: self.slider_pos = 1.0 self._reactive_value = value From e1a60d9225b6eec52f2ec3ec62c342939c8ab418 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 12:53:56 +0000 Subject: [PATCH 31/54] Add a disabled keyword argument to the Input constructor --- src/textual/widgets/_input.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index c652da9a5f..eb94718892 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -176,6 +176,7 @@ def __init__( name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ) -> None: """Initialise the `Input` widget. @@ -187,8 +188,9 @@ def __init__( name: Optional name for the input widget. id: Optional ID for the widget. classes: Optional initial classes for the widget. + disabled: Whether the input is disabled or not. """ - super().__init__(name=name, id=id, classes=classes) + super().__init__(name=name, id=id, classes=classes, disabled=disabled) if value is not None: self.value = value self.placeholder = placeholder From 20aaf0f20507bd67acbfa9d65e328234e3d65de8 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 13:04:44 +0000 Subject: [PATCH 32/54] Add a disabled keyword argument to the ListView constructor --- src/textual/widgets/_list_view.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index 76c0fdf059..295934feb5 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -71,6 +71,7 @@ def __init__( name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ) -> None: """ Args: @@ -79,8 +80,11 @@ def __init__( name: The name of the widget. id: The unique ID of the widget used in CSS/query selection. classes: The CSS classes of the widget. + disabled: Whether the ListView is disabled or not. """ - super().__init__(*children, name=name, id=id, classes=classes) + super().__init__( + *children, name=name, id=id, classes=classes, disabled=disabled + ) self._index = initial_index def on_mount(self) -> None: From 69af2bfd5783ef81fee66ccf7090fca353412632 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 13:11:44 +0000 Subject: [PATCH 33/54] Add a disabled keyword argument to the Tree constructor --- src/textual/widgets/_tree.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 5117d10dc1..1a99fd9dba 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -467,8 +467,9 @@ def __init__( name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ) -> None: - super().__init__(name=name, id=id, classes=classes) + super().__init__(name=name, id=id, classes=classes, disabled=disabled) text_label = self.process_label(label) From 67b57d8a286346ee8d716d9322e1426441f85251 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 13:14:52 +0000 Subject: [PATCH 34/54] Add a disabled keyword argument to the DirectoryTree constructor --- src/textual/widgets/_directory_tree.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 9b3d33fc51..8b481a5522 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -29,6 +29,7 @@ class DirectoryTree(Tree[DirEntry]): name: The name of the widget, or None for no name. Defaults to None. id: The ID of the widget in the DOM, or None for no ID. Defaults to None. classes: A space-separated list of classes, or None for no classes. Defaults to None. + disabled: Whether the directory tree is disabled or not. """ COMPONENT_CLASSES: ClassVar[set[str]] = { @@ -87,6 +88,7 @@ def __init__( name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ) -> None: self.path = path super().__init__( @@ -95,6 +97,7 @@ def __init__( name=name, id=id, classes=classes, + disabled=disabled, ) def process_label(self, label: TextType): From 7c5020ddd3e580417091fe187d7c20475f839c5c Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 13:27:44 +0000 Subject: [PATCH 35/54] Add a disabled keyword argument to the TextLog constructor --- src/textual/widgets/_text_log.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_log.py b/src/textual/widgets/_text_log.py index f78934e70a..c7efee07a2 100644 --- a/src/textual/widgets/_text_log.py +++ b/src/textual/widgets/_text_log.py @@ -43,8 +43,9 @@ def __init__( name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ) -> None: - super().__init__(name=name, id=id, classes=classes) + super().__init__(name=name, id=id, classes=classes, disabled=disabled) self.max_lines = max_lines self._start_line: int = 0 self.lines: list[Strip] = [] From 5e0190c43fec6b896df76c3406310482c77da8d2 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 14:47:57 +0000 Subject: [PATCH 36/54] Add unit tests for disabled property and pseudoclass --- tests/test_disabled.py | 80 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/test_disabled.py diff --git a/tests/test_disabled.py b/tests/test_disabled.py new file mode 100644 index 0000000000..2f59fee715 --- /dev/null +++ b/tests/test_disabled.py @@ -0,0 +1,80 @@ +"""Test Widget.disabled.""" + +from textual.app import App, ComposeResult +from textual.containers import Vertical +from textual.widgets import ( + Button, + DataTable, + DirectoryTree, + Input, + ListView, + Switch, + TextLog, + Tree, +) + + +class DisableApp(App[None]): + """Application for testing Widget.disable.""" + + def compose(self) -> ComposeResult: + """Compose the child widgets.""" + yield Vertical( + Button(), + DataTable(), + DirectoryTree("."), + Input(), + ListView(), + Switch(), + TextLog(), + Tree("Test"), + id="test-container", + ) + + +async def test_all_initially_enabled() -> None: + """All widgets should start out enabled.""" + async with DisableApp().run_test() as pilot: + assert all( + not node.disabled for node in pilot.app.screen.query("#test-container > *") + ) + + +async def test_enabled_widgets_have_enabled_pseudo_class() -> None: + """All enabled widgets should have the :enabled pseudoclass.""" + async with DisableApp().run_test() as pilot: + assert all( + node.has_pseudo_class("enabled") and not node.has_pseudo_class("disabled") + for node in pilot.app.screen.query("#test-container > *") + ) + + +async def test_all_individually_disabled() -> None: + """Post-disable all widgets should report being disabled.""" + async with DisableApp().run_test() as pilot: + for node in pilot.app.screen.query("Vertical > *"): + node.disabled = True + assert all( + node.disabled for node in pilot.app.screen.query("#test-container > *") + ) + + +async def test_disabled_widgets_have_disabled_pseudo_class() -> None: + """All disabled widgets should have the :disabled pseudoclass.""" + async with DisableApp().run_test() as pilot: + for node in pilot.app.screen.query("#test-container > *"): + node.disabled = True + assert all( + node.has_pseudo_class("disabled") and not node.has_pseudo_class("enabled") + for node in pilot.app.screen.query("#test-container > *") + ) + + +async def test_disable_via_container() -> None: + """All child widgets should appear (to CSS) as disabled by a container being disabled.""" + async with DisableApp().run_test() as pilot: + pilot.app.screen.query_one("#test-container", Vertical).disabled = True + assert all( + node.has_pseudo_class("disabled") and not node.has_pseudo_class("enabled") + for node in pilot.app.screen.query("#test-container > *") + ) From e26e75a9d1ec1cb2be335d0f0a5c82f0730a5dc8 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 14:59:53 +0000 Subject: [PATCH 37/54] Swap `game_playable` to use the simplified disable ability --- examples/five_by_five.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/five_by_five.py b/examples/five_by_five.py index f3aada19f3..b79deef8ab 100644 --- a/examples/five_by_five.py +++ b/examples/five_by_five.py @@ -194,8 +194,7 @@ def game_playable(self, playable: bool) -> None: Args: playable (bool): Should the game currently be playable? """ - for cell in self.query(GameCell): - cell.disabled = not playable + self.query_one(GameGrid).disabled = not playable def cell(self, row: int, col: int) -> GameCell: """Get the cell at a given location. From 7763020f53b3fa906620529cce749cda87395d5f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 15:00:57 +0000 Subject: [PATCH 38/54] Move future import to after the module docstring Looks like someone added this but placed it before the module docstring. Fixing it as I notice it. --- examples/five_by_five.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/five_by_five.py b/examples/five_by_five.py index b79deef8ab..d986175a57 100644 --- a/examples/five_by_five.py +++ b/examples/five_by_five.py @@ -1,7 +1,7 @@ -from __future__ import annotations - """Simple version of 5x5, developed for/with Textual.""" +from __future__ import annotations + from pathlib import Path from typing import TYPE_CHECKING, cast From fbd871c3d7baaafac4c519da7c4dfc59182472fc Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 15:52:52 +0000 Subject: [PATCH 39/54] Start a snapshot test for disabled widgets Eventually this should likely have every user-interactive widget within it. Perhaps every widget. --- .../snapshot_apps/disable_widgets.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/snapshot_tests/snapshot_apps/disable_widgets.py diff --git a/tests/snapshot_tests/snapshot_apps/disable_widgets.py b/tests/snapshot_tests/snapshot_apps/disable_widgets.py new file mode 100644 index 0000000000..8a95154273 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/disable_widgets.py @@ -0,0 +1,58 @@ +from textual.app import App, ComposeResult +from textual.containers import Vertical +from textual.widgets import ( + Header, Footer, Button, DataTable, Input, ListView, ListItem, Label, Tree, TextLog +) + +class WidgetDisableTestApp( App[ None ] ): + + CSS = """ + DataTable, ListView, Tree { + height: 2; + } + """ + + @property + def data_table(self) -> DataTable: + data_table = DataTable() + data_table.add_columns( "Column 1", "Column 2", "Column 3", "Column 4" ) + data_table.add_rows( + [ ( str( n ), str( n * 10 ), str( n * 100 ), str( n * 1000 ) ) for n in range( 100 ) ] + ) + return data_table + + @property + def list_view( self ) -> ListView: + return ListView( + *[ ListItem( Label( f"This is list item {n}" ) ) for n in range( 20 ) ] + ) + + @property + def test_tree( self ) -> Tree: + tree = Tree[None]( label="This is a test tree" ) + for n in range( 10 ): + tree.root.add_leaf( f"Leaf {n}" ) + tree.root.expand() + return tree + + def compose( self ) -> ComposeResult: + yield Header() + yield Vertical( + Button(), + self.data_table, + self.list_view, + self.test_tree, + TextLog(), + Input(), + Input(placeholder="This is an empty input with a placeholder"), + Input("This is some text in an input"), + id="test-container", + ) + yield Footer() + + def on_mount(self) -> None: + self.query_one(TextLog).write("Hello, World!") + self.query_one("#test-container", Vertical).disabled = True + +if __name__ == "__main__": + WidgetDisableTestApp().run() From b4f8a6b7788ead9328f0e8149865f7a71b856fb3 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 15:53:45 +0000 Subject: [PATCH 40/54] Add disabled snapshot test to the general snapshot tests --- .../__snapshots__/test_snapshots.ambr | 171 ++++++++++++++++++ tests/snapshot_tests/test_snapshots.py | 4 + 2 files changed, 175 insertions(+) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index c1f1dfb467..7448ad637d 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -10656,6 +10656,177 @@ ''' # --- +# name: test_disabled_widgets + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WidgetDisableTestApp + + + + + + + + + + WidgetDisableTestApp + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Button + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +  Column 1  Column 2  Column 3  Column 4  +  0         0         0         0         + This is list item 0 + This is list item 1 + ▼ This is a test tree + ├── Leaf 0 + Hello, World! + + + ▇▇ + + + + + + + + + + + + + + + ''' +# --- # name: test_dock_layout_sidebar ''' diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index c77025d4d1..1eed7ab772 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -218,3 +218,7 @@ def test_auto_width_input(snap_compare): def test_screen_switch(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "screen_switch.py", press=["a", "b"]) + + +def test_disabled_widgets(snap_compare): + assert snap_compare(SNAPSHOT_APPS_DIR / "disable_widgets.py") From 26df6aeb008936c79e8200cc86e951993860cd30 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 16:20:52 +0000 Subject: [PATCH 41/54] Tidy up the disabled snapshot test --- .../snapshot_apps/disable_widgets.py | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/tests/snapshot_tests/snapshot_apps/disable_widgets.py b/tests/snapshot_tests/snapshot_apps/disable_widgets.py index 8a95154273..809f0c7c5b 100644 --- a/tests/snapshot_tests/snapshot_apps/disable_widgets.py +++ b/tests/snapshot_tests/snapshot_apps/disable_widgets.py @@ -1,13 +1,26 @@ from textual.app import App, ComposeResult -from textual.containers import Vertical +from textual.containers import Vertical, Horizontal from textual.widgets import ( - Header, Footer, Button, DataTable, Input, ListView, ListItem, Label, Tree, TextLog + Header, + Footer, + Button, + DataTable, + Input, + ListView, + ListItem, + Label, + Tree, + TextLog, ) -class WidgetDisableTestApp( App[ None ] ): + +class WidgetDisableTestApp(App[None]): CSS = """ - DataTable, ListView, Tree { + Horizontal { + height: auto; + } + DataTable, ListView, Tree, TextLog { height: 2; } """ @@ -15,30 +28,34 @@ class WidgetDisableTestApp( App[ None ] ): @property def data_table(self) -> DataTable: data_table = DataTable() - data_table.add_columns( "Column 1", "Column 2", "Column 3", "Column 4" ) + data_table.add_columns("Column 1", "Column 2", "Column 3", "Column 4") data_table.add_rows( - [ ( str( n ), str( n * 10 ), str( n * 100 ), str( n * 1000 ) ) for n in range( 100 ) ] + [(str(n), str(n * 10), str(n * 100), str(n * 1000)) for n in range(100)] ) return data_table @property - def list_view( self ) -> ListView: - return ListView( - *[ ListItem( Label( f"This is list item {n}" ) ) for n in range( 20 ) ] - ) + def list_view(self) -> ListView: + return ListView(*[ListItem(Label(f"This is list item {n}")) for n in range(20)]) @property - def test_tree( self ) -> Tree: - tree = Tree[None]( label="This is a test tree" ) - for n in range( 10 ): - tree.root.add_leaf( f"Leaf {n}" ) + def test_tree(self) -> Tree: + tree = Tree[None](label="This is a test tree") + for n in range(10): + tree.root.add_leaf(f"Leaf {n}") tree.root.expand() return tree - def compose( self ) -> ComposeResult: + def compose(self) -> ComposeResult: yield Header() yield Vertical( - Button(), + Horizontal( + Button(), + Button(variant="primary"), + Button(variant="success"), + Button(variant="warning"), + Button(variant="error"), + ), self.data_table, self.list_view, self.test_tree, @@ -54,5 +71,6 @@ def on_mount(self) -> None: self.query_one(TextLog).write("Hello, World!") self.query_one("#test-container", Vertical).disabled = True + if __name__ == "__main__": WidgetDisableTestApp().run() From 8ebb70efac6419aa9d4520bf04cceae90120d9f0 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 14 Feb 2023 16:22:15 +0000 Subject: [PATCH 42/54] Refresh the snapshots --- .../__snapshots__/test_snapshots.ambr | 157 ++++++++++-------- 1 file changed, 86 insertions(+), 71 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 7448ad637d..3d9516f101 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -10679,147 +10679,162 @@ font-weight: 700; } - .terminal-751021020-matrix { + .terminal-2139170380-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-751021020-title { + .terminal-2139170380-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-751021020-r1 { fill: #c5c8c6 } - .terminal-751021020-r2 { fill: #e3e3e3 } - .terminal-751021020-r3 { fill: #313437 } - .terminal-751021020-r4 { fill: #e1e1e1 } - .terminal-751021020-r5 { fill: #7c7d7e;font-weight: bold } - .terminal-751021020-r6 { fill: #101011 } - .terminal-751021020-r7 { fill: #75828b;font-weight: bold } - .terminal-751021020-r8 { fill: #7b7b7b } - .terminal-751021020-r9 { fill: #3a2a13 } - .terminal-751021020-r10 { fill: #78838b } - .terminal-751021020-r11 { fill: #7f8081 } - .terminal-751021020-r12 { fill: #7c7d7e } - .terminal-751021020-r13 { fill: #31220c;font-weight: bold } - .terminal-751021020-r14 { fill: #e2e3e3 } - .terminal-751021020-r15 { fill: #104e2d } - .terminal-751021020-r16 { fill: #7a7b7b } - .terminal-751021020-r17 { fill: #14191f } - .terminal-751021020-r18 { fill: #ddedf9 } + .terminal-2139170380-r1 { fill: #c5c8c6 } + .terminal-2139170380-r2 { fill: #e3e3e3 } + .terminal-2139170380-r3 { fill: #313437 } + .terminal-2139170380-r4 { fill: #324f70 } + .terminal-2139170380-r5 { fill: #4f9262 } + .terminal-2139170380-r6 { fill: #a4823a } + .terminal-2139170380-r7 { fill: #904354 } + .terminal-2139170380-r8 { fill: #7c7d7e;font-weight: bold } + .terminal-2139170380-r9 { fill: #75828b;font-weight: bold } + .terminal-2139170380-r10 { fill: #192e1f;font-weight: bold } + .terminal-2139170380-r11 { fill: #3a2a13;font-weight: bold } + .terminal-2139170380-r12 { fill: #978186;font-weight: bold } + .terminal-2139170380-r13 { fill: #101011 } + .terminal-2139170380-r14 { fill: #0c1e39 } + .terminal-2139170380-r15 { fill: #156034 } + .terminal-2139170380-r16 { fill: #825210 } + .terminal-2139170380-r17 { fill: #5b132a } + .terminal-2139170380-r18 { fill: #7b7b7b } + .terminal-2139170380-r19 { fill: #e1e1e1 } + .terminal-2139170380-r20 { fill: #3a2a13 } + .terminal-2139170380-r21 { fill: #78838b } + .terminal-2139170380-r22 { fill: #7f8081 } + .terminal-2139170380-r23 { fill: #7c7d7e } + .terminal-2139170380-r24 { fill: #31220c;font-weight: bold } + .terminal-2139170380-r25 { fill: #e2e3e3 } + .terminal-2139170380-r26 { fill: #104e2d } + .terminal-2139170380-r27 { fill: #7a7b7b } + .terminal-2139170380-r28 { fill: #1c1c1c } + .terminal-2139170380-r29 { fill: #191919 } + .terminal-2139170380-r30 { fill: #181818 } + .terminal-2139170380-r31 { fill: #7c7c7c } + .terminal-2139170380-r32 { fill: #494949 } + .terminal-2139170380-r33 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WidgetDisableTestApp + WidgetDisableTestApp - - - - WidgetDisableTestApp - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Button - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -  Column 1  Column 2  Column 3  Column 4  -  0         0         0         0         - This is list item 0 - This is list item 1 - ▼ This is a test tree - ├── Leaf 0 - Hello, World! - - - ▇▇ - - - - - - - - - + + + + WidgetDisableTestApp + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +  Column 1  Column 2  Column 3  Column 4  +  0         0         0         0         + This is list item 0 + This is list item 1 + ▼ This is a test tree + ├── Leaf 0 + Hello, World! + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This is an empty input with a placeholder + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This is some text in an input + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + From 3ccfb5370f74ac6376897afbf9b86e3b1e5baaa1 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 15 Feb 2023 11:16:23 +0000 Subject: [PATCH 43/54] Add Markdown and MarkdownViewer to the disabled property test --- tests/test_disabled.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_disabled.py b/tests/test_disabled.py index 2f59fee715..8ad3d0e09c 100644 --- a/tests/test_disabled.py +++ b/tests/test_disabled.py @@ -8,6 +8,8 @@ DirectoryTree, Input, ListView, + Markdown, + MarkdownViewer, Switch, TextLog, Tree, @@ -28,6 +30,8 @@ def compose(self) -> ComposeResult: Switch(), TextLog(), Tree("Test"), + Markdown(), + MarkdownViewer(), id="test-container", ) From 2379bb46e06fa38780f21b56822d2f932b2585c1 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 15 Feb 2023 11:22:58 +0000 Subject: [PATCH 44/54] Add type annotation to the DataTable Now that the new DataTable is merged in. --- tests/snapshot_tests/snapshot_apps/disable_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshot_tests/snapshot_apps/disable_widgets.py b/tests/snapshot_tests/snapshot_apps/disable_widgets.py index 809f0c7c5b..321e94f7b1 100644 --- a/tests/snapshot_tests/snapshot_apps/disable_widgets.py +++ b/tests/snapshot_tests/snapshot_apps/disable_widgets.py @@ -27,7 +27,7 @@ class WidgetDisableTestApp(App[None]): @property def data_table(self) -> DataTable: - data_table = DataTable() + data_table = DataTable[str]() data_table.add_columns("Column 1", "Column 2", "Column 3", "Column 4") data_table.add_rows( [(str(n), str(n * 10), str(n * 100), str(n * 1000)) for n in range(100)] From d90cae3a92965daa4a13dcbf3f701d5e3b38096f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 15 Feb 2023 11:26:58 +0000 Subject: [PATCH 45/54] Add Markdown and MarkdownViewer to the disabled snapshot test --- .../__snapshots__/test_snapshots.ambr | 173 +++++++++--------- .../snapshot_apps/disable_widgets.py | 8 + 2 files changed, 95 insertions(+), 86 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 01de5b3e83..088f6aba71 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -10836,162 +10836,163 @@ font-weight: 700; } - .terminal-2139170380-matrix { + .terminal-1429392340-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2139170380-title { + .terminal-1429392340-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2139170380-r1 { fill: #c5c8c6 } - .terminal-2139170380-r2 { fill: #e3e3e3 } - .terminal-2139170380-r3 { fill: #313437 } - .terminal-2139170380-r4 { fill: #324f70 } - .terminal-2139170380-r5 { fill: #4f9262 } - .terminal-2139170380-r6 { fill: #a4823a } - .terminal-2139170380-r7 { fill: #904354 } - .terminal-2139170380-r8 { fill: #7c7d7e;font-weight: bold } - .terminal-2139170380-r9 { fill: #75828b;font-weight: bold } - .terminal-2139170380-r10 { fill: #192e1f;font-weight: bold } - .terminal-2139170380-r11 { fill: #3a2a13;font-weight: bold } - .terminal-2139170380-r12 { fill: #978186;font-weight: bold } - .terminal-2139170380-r13 { fill: #101011 } - .terminal-2139170380-r14 { fill: #0c1e39 } - .terminal-2139170380-r15 { fill: #156034 } - .terminal-2139170380-r16 { fill: #825210 } - .terminal-2139170380-r17 { fill: #5b132a } - .terminal-2139170380-r18 { fill: #7b7b7b } - .terminal-2139170380-r19 { fill: #e1e1e1 } - .terminal-2139170380-r20 { fill: #3a2a13 } - .terminal-2139170380-r21 { fill: #78838b } - .terminal-2139170380-r22 { fill: #7f8081 } - .terminal-2139170380-r23 { fill: #7c7d7e } - .terminal-2139170380-r24 { fill: #31220c;font-weight: bold } - .terminal-2139170380-r25 { fill: #e2e3e3 } - .terminal-2139170380-r26 { fill: #104e2d } - .terminal-2139170380-r27 { fill: #7a7b7b } - .terminal-2139170380-r28 { fill: #1c1c1c } - .terminal-2139170380-r29 { fill: #191919 } - .terminal-2139170380-r30 { fill: #181818 } - .terminal-2139170380-r31 { fill: #7c7c7c } - .terminal-2139170380-r32 { fill: #494949 } - .terminal-2139170380-r33 { fill: #ddedf9 } + .terminal-1429392340-r1 { fill: #c5c8c6 } + .terminal-1429392340-r2 { fill: #e3e3e3 } + .terminal-1429392340-r3 { fill: #313437 } + .terminal-1429392340-r4 { fill: #324f70 } + .terminal-1429392340-r5 { fill: #4f9262 } + .terminal-1429392340-r6 { fill: #a4823a } + .terminal-1429392340-r7 { fill: #904354 } + .terminal-1429392340-r8 { fill: #e1e1e1 } + .terminal-1429392340-r9 { fill: #7c7d7e;font-weight: bold } + .terminal-1429392340-r10 { fill: #75828b;font-weight: bold } + .terminal-1429392340-r11 { fill: #192e1f;font-weight: bold } + .terminal-1429392340-r12 { fill: #3a2a13;font-weight: bold } + .terminal-1429392340-r13 { fill: #978186;font-weight: bold } + .terminal-1429392340-r14 { fill: #101011 } + .terminal-1429392340-r15 { fill: #0c1e39 } + .terminal-1429392340-r16 { fill: #156034 } + .terminal-1429392340-r17 { fill: #825210 } + .terminal-1429392340-r18 { fill: #5b132a } + .terminal-1429392340-r19 { fill: #7b7b7b } + .terminal-1429392340-r20 { fill: #3a2a13 } + .terminal-1429392340-r21 { fill: #78838b } + .terminal-1429392340-r22 { fill: #7f8081 } + .terminal-1429392340-r23 { fill: #7c7d7e } + .terminal-1429392340-r24 { fill: #31220c;font-weight: bold } + .terminal-1429392340-r25 { fill: #e2e3e3 } + .terminal-1429392340-r26 { fill: #104e2d } + .terminal-1429392340-r27 { fill: #7a7b7b } + .terminal-1429392340-r28 { fill: #1c1c1c } + .terminal-1429392340-r29 { fill: #191919 } + .terminal-1429392340-r30 { fill: #181818 } + .terminal-1429392340-r31 { fill: #7c7c7c } + .terminal-1429392340-r32 { fill: #494949 } + .terminal-1429392340-r33 { fill: #14191f } + .terminal-1429392340-r34 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WidgetDisableTestApp + WidgetDisableTestApp - - - - WidgetDisableTestApp - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -  Column 1  Column 2  Column 3  Column 4  -  0         0         0         0         - This is list item 0 - This is list item 1 - ▼ This is a test tree - ├── Leaf 0 - Hello, World! - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - This is an empty input with a placeholder - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - This is some text in an input - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - + + + + WidgetDisableTestApp + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +  Column 1  Column 2  Column 3  Column 4  +  0         0         0         0         + This is list item 0 + This is list item 1 + ▼ This is a test tree + ├── Leaf 0 + Hello, World! + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This is an empty input with a placeholder + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This is some text in an input + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▇▇ diff --git a/tests/snapshot_tests/snapshot_apps/disable_widgets.py b/tests/snapshot_tests/snapshot_apps/disable_widgets.py index 321e94f7b1..7a241e9146 100644 --- a/tests/snapshot_tests/snapshot_apps/disable_widgets.py +++ b/tests/snapshot_tests/snapshot_apps/disable_widgets.py @@ -9,6 +9,8 @@ ListView, ListItem, Label, + Markdown, + MarkdownViewer, Tree, TextLog, ) @@ -23,6 +25,10 @@ class WidgetDisableTestApp(App[None]): DataTable, ListView, Tree, TextLog { height: 2; } + + Markdown, MarkdownViewer { + height: 1fr; + } """ @property @@ -63,6 +69,8 @@ def compose(self) -> ComposeResult: Input(), Input(placeholder="This is an empty input with a placeholder"), Input("This is some text in an input"), + Markdown("# Hello, World!"), + MarkdownViewer("# Hello, World!"), id="test-container", ) yield Footer() From 148f9c15439822afee3831cee8556f423a46bf4b Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 15 Feb 2023 14:05:15 +0000 Subject: [PATCH 46/54] Update the ChangeLog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b83ccf894..d945ff4d98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.12.0] - Unreleased + +### Added + +- Added `Widget.disabled` https://github.com/Textualize/textual/pull/1785 + ## [0.11.0] - 2023-02-15 ### Added From 6f0a7541a415e9ac44cd0136a2a2dc7beeed697e Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 15 Feb 2023 14:13:50 +0000 Subject: [PATCH 47/54] Add a disabled keyword to DataTable --- src/textual/widgets/_data_table.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 3a3d5e1004..096913578b 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -473,8 +473,9 @@ def __init__( name: str | None = None, id: str | None = None, classes: str | None = None, + disabled: bool = False, ) -> None: - super().__init__(name=name, id=id, classes=classes) + super().__init__(name=name, id=id, classes=classes, disabled=disabled) self._data: dict[RowKey, dict[ColumnKey, CellType]] = {} """Contains the cells of the table, indexed by row key and column key. The final positioning of a cell on screen cannot be determined solely by this From 8dbc97553dbd240b44ae5780a5e2f4fb09b73434 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 16 Feb 2023 09:49:33 +0000 Subject: [PATCH 48/54] Shave a function call off get_pseudo_classes See https://github.com/Textualize/textual/pull/1785#discussion_r1108208063 for the background. --- src/textual/widget.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index eaf537ade2..da25905606 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1180,6 +1180,8 @@ def virtual_region_with_margin(self) -> Region: @property def _self_or_ancestors_disabled(self) -> bool: """Is this widget or any of its ancestors disabled?""" + # NOTE: Please see the copy of this code in get_pseudo_classes. I + # you change this, change that too. return any( node.disabled for node in self.ancestors_with_self @@ -2098,7 +2100,16 @@ def get_pseudo_classes(self) -> Iterable[str]: Names of the pseudo classes. """ - yield "disabled" if self._self_or_ancestors_disabled else "enabled" + # NOTE: The heart of this yield is a direct copy of + # _self_or_ancestors_disabled. Because this method is called so + # much, here we save one function call as a very small but long-term + # useful optimisation. If _self_or_ancestors_disabled ever changes, + # be sure to reflect that change here! + yield "disabled" if any( + node.disabled + for node in self.ancestors_with_self + if isinstance(node, Widget) + ) else "enabled" if self.mouse_over: yield "hover" if self.has_focus: From c35277aab798450157c4259cd963b59094435944 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 16 Feb 2023 10:07:18 +0000 Subject: [PATCH 49/54] Simplify how the styles are updated See https://github.com/Textualize/textual/pull/1785#discussion_r1108210336 --- src/textual/widget.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index da25905606..3a6af140f7 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2165,8 +2165,7 @@ def watch_has_focus(self, value: bool) -> None: def watch_disabled(self) -> None: """Update the styles of the widget and its children when disabled is toggled.""" - for node in self.walk_children(with_self=True): - node._update_styles() + self._update_styles() def _size_updated( self, size: Size, virtual_size: Size, container_size: Size From 3d43d18486b6a73cdc52f3d8a77b497b80066fee Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 16 Feb 2023 10:12:46 +0000 Subject: [PATCH 50/54] Remove disabled from the rich repr of Button It's not specific to Button now. --- src/textual/widgets/_button.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index d9da388f50..ec143323ca 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -197,7 +197,6 @@ def __init__( def __rich_repr__(self) -> rich.repr.Result: yield from super().__rich_repr__() yield "variant", self.variant, "default" - yield "disabled", self.disabled, False def validate_variant(self, variant: str) -> str: if variant not in _VALID_BUTTON_VARIANTS: From 8762866b27e0b0889403cb6837f067ca849c5072 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 16 Feb 2023 10:16:56 +0000 Subject: [PATCH 51/54] Fix comment typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I typo. Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- src/textual/widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 3a6af140f7..d62ef7cc45 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1180,7 +1180,7 @@ def virtual_region_with_margin(self) -> Region: @property def _self_or_ancestors_disabled(self) -> bool: """Is this widget or any of its ancestors disabled?""" - # NOTE: Please see the copy of this code in get_pseudo_classes. I + # NOTE: Please see the copy of this code in get_pseudo_classes. If # you change this, change that too. return any( node.disabled From 3964a108ca5dd9d3fda620c67bf4713e2969d3e3 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 16 Feb 2023 11:34:11 +0000 Subject: [PATCH 52/54] Add missing parameter to the Static docstring --- src/textual/widgets/_static.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_static.py b/src/textual/widgets/_static.py index d2c2edab26..5007f1b1d3 100644 --- a/src/textual/widgets/_static.py +++ b/src/textual/widgets/_static.py @@ -36,6 +36,7 @@ class Static(Widget, inherit_bindings=False): name: Name of widget. Defaults to None. id: ID of Widget. Defaults to None. classes: Space separated list of class names. Defaults to None. + disabled: Whether the static is disabled or not. """ DEFAULT_CSS = """ From 7ba64c71999c148bc1a5ea1c508199b96781ad7c Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 16 Feb 2023 11:40:52 +0000 Subject: [PATCH 53/54] Fix docstring typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- tests/test_disabled.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_disabled.py b/tests/test_disabled.py index 8ad3d0e09c..850fcf7c7b 100644 --- a/tests/test_disabled.py +++ b/tests/test_disabled.py @@ -17,7 +17,7 @@ class DisableApp(App[None]): - """Application for testing Widget.disable.""" + """Application for testing Widget.disabled.""" def compose(self) -> ComposeResult: """Compose the child widgets.""" From 53fa0920afe8a262ee2d0af7d53c658446847322 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 16 Feb 2023 13:13:43 +0000 Subject: [PATCH 54/54] Further simplify the en/disabled pseudo-class calculation --- src/textual/widget.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 3a6af140f7..a16a6bf60d 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1180,8 +1180,6 @@ def virtual_region_with_margin(self) -> Region: @property def _self_or_ancestors_disabled(self) -> bool: """Is this widget or any of its ancestors disabled?""" - # NOTE: Please see the copy of this code in get_pseudo_classes. I - # you change this, change that too. return any( node.disabled for node in self.ancestors_with_self @@ -2100,16 +2098,14 @@ def get_pseudo_classes(self) -> Iterable[str]: Names of the pseudo classes. """ - # NOTE: The heart of this yield is a direct copy of - # _self_or_ancestors_disabled. Because this method is called so - # much, here we save one function call as a very small but long-term - # useful optimisation. If _self_or_ancestors_disabled ever changes, - # be sure to reflect that change here! - yield "disabled" if any( - node.disabled - for node in self.ancestors_with_self - if isinstance(node, Widget) - ) else "enabled" + node = self + while isinstance(node, Widget): + if node.disabled: + yield "disabled" + break + node = node._parent + else: + yield "enabled" if self.mouse_over: yield "hover" if self.has_focus: