Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promote disabled to Widget level #1785

Merged
merged 64 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
f495870
Remove the disabled styling from `Input`
davep Feb 13, 2023
04c2076
Put the placeholder styling background
davep Feb 13, 2023
7aaf884
Move disabled out of `Button` and into `Widget`
davep Feb 13, 2023
257fe7b
Add enabled and disabled pseudo-classes
davep Feb 13, 2023
a0a83e9
Correct docstring typo
davep Feb 13, 2023
d3104a9
Add `disabled` as a widget construction keyword argument
davep Feb 13, 2023
821a6ff
Simplify the default for disabled on a Widget
davep Feb 13, 2023
3707f80
Move duplicate safe calls to update_styles into a single method
davep Feb 13, 2023
d37895d
Convert some app.update_styles calls into _update_styles calls
davep Feb 13, 2023
1097fb2
Set the initial disabled state later in __init__
davep Feb 13, 2023
bbdc70a
Move the main handling of disabled up to `Widget`
davep Feb 13, 2023
682c4de
Remove unnecessary comment
davep Feb 13, 2023
14f83a0
Remove commented out code from Button
davep Feb 13, 2023
500458e
Add a focusable property
davep Feb 13, 2023
bf1188c
Merge branch 'promote-disabled' of github.com:davep/textual into prom…
davep Feb 13, 2023
8379945
Remove commented out code from Button
davep Feb 13, 2023
0171ad7
Build the focus chain from focusable widgets
davep Feb 13, 2023
850c140
Swap `set_focus` over to checking focusable
davep Feb 13, 2023
813c89c
Swap _forward_event over to focsuable (from can_focus)
davep Feb 13, 2023
d148dd6
Swap _reset_focus over to focsuable (from can_focus)
davep Feb 13, 2023
525455b
Bring the focus_chain docstring in line with out newer guidelines
davep Feb 13, 2023
ea8470e
Stop mouse events going to a widget that can't receive focus right now
davep Feb 13, 2023
c7990d9
Add support for disabling a widget and all of its children
davep Feb 14, 2023
bff4334
Merge branch 'main' into promote-disabled
davep Feb 14, 2023
674ee26
Add a docstring to watch_disabled
davep Feb 14, 2023
d27d9f8
Only block mouse events if disabled, not if not focusable
davep Feb 14, 2023
7c81247
Remove Button.watch_mouse_over
davep Feb 14, 2023
068aa07
Only enable Enter and Leave if not disabled
davep Feb 14, 2023
cccf7af
Remove :disabled styling from the `Button`
davep Feb 14, 2023
c65278d
Add default disabled styling for the whole application
davep Feb 14, 2023
3c8f464
Update the snapshots
davep Feb 14, 2023
9e4d4aa
Add a disabled keyword argument to the Switch constructor
davep Feb 14, 2023
e1a60d9
Add a disabled keyword argument to the Input constructor
davep Feb 14, 2023
20aaf0f
Add a disabled keyword argument to the ListView constructor
davep Feb 14, 2023
69af2bf
Add a disabled keyword argument to the Tree constructor
davep Feb 14, 2023
67b57d8
Add a disabled keyword argument to the DirectoryTree constructor
davep Feb 14, 2023
7c5020d
Add a disabled keyword argument to the TextLog constructor
davep Feb 14, 2023
5e0190c
Add unit tests for disabled property and pseudoclass
davep Feb 14, 2023
e26e75a
Swap `game_playable` to use the simplified disable ability
davep Feb 14, 2023
7763020
Move future import to after the module docstring
davep Feb 14, 2023
fbd871c
Start a snapshot test for disabled widgets
davep Feb 14, 2023
b4f8a6b
Add disabled snapshot test to the general snapshot tests
davep Feb 14, 2023
26df6ae
Tidy up the disabled snapshot test
davep Feb 14, 2023
8ebb70e
Refresh the snapshots
davep Feb 14, 2023
d0a9656
Merge branch 'main' into promote-disabled
davep Feb 14, 2023
ba9b2e0
Merge branch 'main' into promote-disabled
davep Feb 15, 2023
6ab9a42
Merge branch 'main' into promote-disabled
davep Feb 15, 2023
4fe1e0f
Merge branch 'main' into promote-disabled
davep Feb 15, 2023
3ccfb53
Add Markdown and MarkdownViewer to the disabled property test
davep Feb 15, 2023
2379bb4
Add type annotation to the DataTable
davep Feb 15, 2023
d90cae3
Add Markdown and MarkdownViewer to the disabled snapshot test
davep Feb 15, 2023
911582d
Merge branch 'main' into promote-disabled
davep Feb 15, 2023
ca1eede
Merge branch 'main' into promote-disabled
davep Feb 15, 2023
148f9c1
Update the ChangeLog
davep Feb 15, 2023
6f0a754
Add a disabled keyword to DataTable
davep Feb 15, 2023
8dbc975
Shave a function call off get_pseudo_classes
davep Feb 16, 2023
cd3e018
Merge branch 'main' into promote-disabled
davep Feb 16, 2023
c35277a
Simplify how the styles are updated
davep Feb 16, 2023
3d43d18
Remove disabled from the rich repr of Button
davep Feb 16, 2023
8762866
Fix comment typo
davep Feb 16, 2023
3964a10
Add missing parameter to the Static docstring
davep Feb 16, 2023
7ba64c7
Fix docstring typo
davep Feb 16, 2023
53fa092
Further simplify the en/disabled pseudo-class calculation
davep Feb 16, 2023
121b0b8
Merge branch 'promote-disabled' of github.com:davep/textual into prom…
davep Feb 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added

- Added `Widget.disabled` https://github.com/Textualize/textual/pull/1785
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved

### Fixed

- DataTable scrollbars resize correctly when header is toggled https://github.com/Textualize/textual/pull/1803
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/CSS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 3 additions & 4 deletions examples/five_by_five.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]] = {}
Expand Down
25 changes: 13 additions & 12 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,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.

Expand All @@ -884,10 +894,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.
Expand All @@ -900,10 +907,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.
Expand All @@ -916,10 +920,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 classes (such as hover, focus etc)
Expand Down
14 changes: 5 additions & 9 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -177,7 +173,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
Expand Down Expand Up @@ -314,7 +310,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:
Expand Down Expand Up @@ -351,7 +347,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
Expand Down Expand Up @@ -547,7 +543,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()
Expand Down
50 changes: 46 additions & 4 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand All @@ -235,6 +237,7 @@ def __init__(
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
) -> None:
self._size = Size(0, 0)
self._container_size = Size(0, 0)
Expand Down Expand Up @@ -277,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)
Expand Down Expand Up @@ -1173,6 +1177,20 @@ 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._self_or_ancestors_disabled

@property
def focusable_children(self) -> list[Widget]:
"""Get the children which may be focused.
Expand Down Expand Up @@ -2080,6 +2098,14 @@ def get_pseudo_classes(self) -> Iterable[str]:
Names of the pseudo classes.

"""
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:
Expand Down Expand Up @@ -2127,11 +2153,15 @@ 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 watch_disabled(self) -> None:
"""Update the styles of the widget and its children when disabled is toggled."""
self._update_styles()

def _size_updated(
self, size: Size, virtual_size: Size, container_size: Size
Expand Down Expand Up @@ -2421,6 +2451,18 @@ def release_mouse(self) -> None:
"""
self.app.capture_mouse(None)

def check_message_enabled(self, message: Message) -> bool:
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved
# 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 not be disabled at this moment.
return (
not self._self_or_ancestors_disabled
if isinstance(message, (events.MouseEvent, events.Enter, events.Leave))
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)

Expand Down Expand Up @@ -2479,11 +2521,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:
Expand Down
Loading