Skip to content

Commit 93716e7

Browse files
authored
Merge branch 'main' into nested-height-fix
2 parents b265b85 + 552b0cc commit 93716e7

File tree

11 files changed

+244
-150
lines changed

11 files changed

+244
-150
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1111

1212
- Fixed issues with nested auto dimensions https://github.com/Textualize/textual/issues/1402
1313

14+
### Added
15+
16+
- Added `textual.actions.SkipAction` exception which can be raised from an action to allow parents to process bindings.
17+
1418
## [0.7.0] - 2022-12-17
1519

1620
### Added

docs/guide/actions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ When you click any of the links, Textual runs the `"set_background"` action to c
7575

7676
## Bindings
7777

78-
Textual will also run actions bound to keys. The following example adds key [bindings](./input.md#bindings) for the ++r++, ++g++, and ++b++ keys which call the `"set_background"` action.
78+
Textual will run actions bound to keys. The following example adds key [bindings](./input.md#bindings) for the ++r++, ++g++, and ++b++ keys which call the `"set_background"` action.
7979

8080
=== "actions04.py"
8181

@@ -92,7 +92,7 @@ If you run this example, you can change the background by pressing keys in addit
9292

9393
## Namespaces
9494

95-
Textual will look for action methods on the widget or app where they are used. If we were to create a [custom widget](./widgets.md#custom-widgets) it can have its own set of actions.
95+
Textual will look for action methods in the class where they are defined (App, Screen, or Widget). If we were to create a [custom widget](./widgets.md#custom-widgets) it can have its own set of actions.
9696

9797
The following example defines a custom widget with its own `set_background` action.
9898

src/textual/actions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
import re
55

66

7+
class SkipAction(Exception):
8+
"""Raise in an action to skip the action (and allow any parent bindings to run)."""
9+
10+
711
class ActionError(Exception):
812
pass
913

src/textual/app.py

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from rich.segment import Segment, Segments
3939
from rich.traceback import Traceback
4040

41-
from . import Logger, LogGroup, LogVerbosity, actions, events, log, messages
41+
from . import actions, Logger, LogGroup, LogVerbosity, events, log, messages
4242
from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction
4343
from ._ansi_sequences import SYNC_END, SYNC_START
4444
from ._callback import invoke
@@ -47,6 +47,7 @@
4747
from ._filter import LineFilter, Monochrome
4848
from ._path import _make_path_object_relative
4949
from ._typing import Final, TypeAlias
50+
from .actions import SkipAction
5051
from .await_remove import AwaitRemove
5152
from .binding import Binding, Bindings
5253
from .css.query import NoMatches
@@ -1386,11 +1387,10 @@ async def invoke_ready_callback() -> None:
13861387
raise
13871388

13881389
finally:
1390+
self._running = True
13891391
await self._ready()
13901392
await invoke_ready_callback()
13911393

1392-
self._running = True
1393-
13941394
try:
13951395
await self._process_messages_loop()
13961396
except asyncio.CancelledError:
@@ -1753,8 +1753,8 @@ async def check_bindings(self, key: str, priority: bool = False) -> bool:
17531753
):
17541754
binding = bindings.keys.get(key)
17551755
if binding is not None and binding.priority == priority:
1756-
await self.action(binding.action, default_namespace=namespace)
1757-
return True
1756+
if await self.action(binding.action, namespace):
1757+
return True
17581758
return False
17591759

17601760
async def on_event(self, event: events.Event) -> None:
@@ -1823,32 +1823,41 @@ async def action(
18231823
async def _dispatch_action(
18241824
self, namespace: object, action_name: str, params: Any
18251825
) -> bool:
1826+
"""Dispatch an action to an action method.
1827+
1828+
Args:
1829+
namespace (object): Namespace (object) of action.
1830+
action_name (str): Name of the action.
1831+
params (Any): Action parameters.
1832+
1833+
Returns:
1834+
bool: True if handled, otherwise False.
1835+
"""
1836+
_rich_traceback_guard = True
1837+
18261838
log(
18271839
"<action>",
18281840
namespace=namespace,
18291841
action_name=action_name,
18301842
params=params,
18311843
)
1832-
_rich_traceback_guard = True
1833-
1834-
public_method_name = f"action_{action_name}"
1835-
private_method_name = f"_{public_method_name}"
18361844

1837-
private_method = getattr(namespace, private_method_name, None)
1838-
public_method = getattr(namespace, public_method_name, None)
1839-
1840-
if private_method is None and public_method is None:
1845+
try:
1846+
private_method = getattr(namespace, f"_action_{action_name}", None)
1847+
if callable(private_method):
1848+
await invoke(private_method, *params)
1849+
return True
1850+
public_method = getattr(namespace, f"action_{action_name}", None)
1851+
if callable(public_method):
1852+
await invoke(public_method, *params)
1853+
return True
18411854
log(
1842-
f"<action> {action_name!r} has no target. Couldn't find methods {public_method_name!r} or {private_method_name!r}"
1855+
f"<action> {action_name!r} has no target."
1856+
f" Could not find methods '_action_{action_name}' or 'action_{action_name}'"
18431857
)
1844-
1845-
if callable(private_method):
1846-
await invoke(private_method, *params)
1847-
return True
1848-
elif callable(public_method):
1849-
await invoke(public_method, *params)
1850-
return True
1851-
1858+
except SkipAction:
1859+
# The action method raised this to explicitly not handle the action
1860+
log("<action> {action_name!r} skipped.")
18521861
return False
18531862

18541863
async def _broker_event(
@@ -1859,7 +1868,7 @@ async def _broker_event(
18591868
Args:
18601869
event_name (str): _description_
18611870
event (events.Event): An event object.
1862-
default_namespace (object | None): TODO: _description_
1871+
default_namespace (object | None): The default namespace, where one isn't supplied.
18631872
18641873
Returns:
18651874
bool: True if an action was processed.

src/textual/demo.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Sidebar {
2929
}
3030

3131
Sidebar:focus-within {
32-
offset: 0 0 !important;
32+
offset: 0 0 !important;
3333
}
3434

3535
Sidebar.-hidden {

src/textual/screen.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,22 +295,19 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None:
295295
# No focus, so blur currently focused widget if it exists
296296
if self.focused is not None:
297297
self.focused.post_message_no_wait(events.Blur(self))
298-
self.focused.emit_no_wait(events.DescendantBlur(self))
299298
self.focused = None
300299
self.log.debug("focus was removed")
301300
elif widget.can_focus:
302301
if self.focused != widget:
303302
if self.focused is not None:
304303
# Blur currently focused widget
305304
self.focused.post_message_no_wait(events.Blur(self))
306-
self.focused.emit_no_wait(events.DescendantBlur(self))
307305
# Change focus
308306
self.focused = widget
309307
# Send focus event
310308
if scroll_visible:
311309
self.screen.scroll_to_widget(widget)
312310
widget.post_message_no_wait(events.Focus(self))
313-
widget.emit_no_wait(events.DescendantFocus(self))
314311
self.log.debug(widget, "was focused")
315312

316313
async def _on_idle(self, event: events.Idle) -> None:

src/textual/widget.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from ._segment_tools import align_lines
4545
from ._styles_cache import StylesCache
4646
from ._types import Lines
47+
from .actions import SkipAction
4748
from .await_remove import AwaitRemove
4849
from .binding import Binding
4950
from .box_model import BoxModel, get_box_model
@@ -2350,17 +2351,22 @@ def _on_enter(self, event: events.Enter) -> None:
23502351
self.mouse_over = True
23512352

23522353
def _on_focus(self, event: events.Focus) -> None:
2353-
for node in self.ancestors_with_self:
2354-
if node._has_focus_within:
2355-
self.app.update_styles(node)
23562354
self.has_focus = True
23572355
self.refresh()
2356+
self.emit_no_wait(events.DescendantFocus(self))
23582357

23592358
def _on_blur(self, event: events.Blur) -> None:
2360-
if any(node._has_focus_within for node in self.ancestors_with_self):
2361-
self.app.update_styles(self)
23622359
self.has_focus = False
23632360
self.refresh()
2361+
self.emit_no_wait(events.DescendantBlur(self))
2362+
2363+
def _on_descendant_blur(self, event: events.DescendantBlur) -> None:
2364+
if self._has_focus_within:
2365+
self.app.update_styles(self)
2366+
2367+
def _on_descendant_focus(self, event: events.DescendantBlur) -> None:
2368+
if self._has_focus_within:
2369+
self.app.update_styles(self)
23642370

23652371
def _on_mouse_scroll_down(self, event) -> None:
23662372
if self.allow_vertical_scroll:
@@ -2405,33 +2411,41 @@ def _on_scroll_to_region(self, message: messages.ScrollToRegion) -> None:
24052411
self.scroll_to_region(message.region, animate=True)
24062412

24072413
def action_scroll_home(self) -> None:
2408-
if self._allow_scroll:
2409-
self.scroll_home()
2414+
if not self._allow_scroll:
2415+
raise SkipAction()
2416+
self.scroll_home()
24102417

24112418
def action_scroll_end(self) -> None:
2412-
if self._allow_scroll:
2413-
self.scroll_end()
2419+
if not self._allow_scroll:
2420+
raise SkipAction()
2421+
self.scroll_end()
24142422

24152423
def action_scroll_left(self) -> None:
2416-
if self.allow_horizontal_scroll:
2417-
self.scroll_left()
2424+
if not self.allow_horizontal_scroll:
2425+
raise SkipAction()
2426+
self.scroll_left()
24182427

24192428
def action_scroll_right(self) -> None:
2420-
if self.allow_horizontal_scroll:
2421-
self.scroll_right()
2429+
if not self.allow_horizontal_scroll:
2430+
raise SkipAction()
2431+
self.scroll_right()
24222432

24232433
def action_scroll_up(self) -> None:
2424-
if self.allow_vertical_scroll:
2425-
self.scroll_up()
2434+
if not self.allow_vertical_scroll:
2435+
raise SkipAction()
2436+
self.scroll_up()
24262437

24272438
def action_scroll_down(self) -> None:
2428-
if self.allow_vertical_scroll:
2429-
self.scroll_down()
2439+
if not self.allow_vertical_scroll:
2440+
raise SkipAction()
2441+
self.scroll_down()
24302442

24312443
def action_page_down(self) -> None:
2432-
if self.allow_vertical_scroll:
2433-
self.scroll_page_down()
2444+
if not self.allow_vertical_scroll:
2445+
raise SkipAction()
2446+
self.scroll_page_down()
24342447

24352448
def action_page_up(self) -> None:
2436-
if self.allow_vertical_scroll:
2437-
self.scroll_page_up()
2449+
if not self.allow_vertical_scroll:
2450+
raise SkipAction()
2451+
self.scroll_page_up()

src/textual/widgets/_static.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def _check_renderable(renderable: object):
2424
)
2525

2626

27-
class Static(Widget):
27+
class Static(Widget, inherit_bindings=False):
2828
"""A widget to display simple static content, or use as a base class for more complex widgets.
2929
3030
Args:

0 commit comments

Comments
 (0)