Skip to content

Commit 3e2c3fb

Browse files
committed
Merge branch 'main' into cli-keys
2 parents 0987229 + df6e4df commit 3e2c3fb

File tree

17 files changed

+506
-170
lines changed

17 files changed

+506
-170
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [0.8.0] - Unreleased
9+
10+
### Fixed
11+
12+
- Fixed issues with nested auto dimensions https://github.com/Textualize/textual/issues/1402
13+
14+
### Added
15+
16+
- Added `textual.actions.SkipAction` exception which can be raised from an action to allow parents to process bindings.
17+
18+
### Fixed
19+
20+
- Fixed watch method incorrectly running on first set when value hasnt changed and init=False https://github.com/Textualize/textual/pull/1367
21+
822
## [0.7.0] - 2022-12-17
923

1024
### Added
@@ -24,6 +38,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2438
- Fixed validator not running on first reactive set https://github.com/Textualize/textual/pull/1359
2539
- Ensure only printable characters are used as key_display https://github.com/Textualize/textual/pull/1361
2640

41+
2742
## [0.6.0] - 2022-12-11
2843

2944
### 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
@@ -1393,11 +1394,10 @@ async def invoke_ready_callback() -> None:
13931394
raise
13941395

13951396
finally:
1397+
self._running = True
13961398
await self._ready()
13971399
await invoke_ready_callback()
13981400

1399-
self._running = True
1400-
14011401
try:
14021402
await self._process_messages_loop()
14031403
except asyncio.CancelledError:
@@ -1760,8 +1760,8 @@ async def check_bindings(self, key: str, priority: bool = False) -> bool:
17601760
):
17611761
binding = bindings.keys.get(key)
17621762
if binding is not None and binding.priority == priority:
1763-
await self.action(binding.action, default_namespace=namespace)
1764-
return True
1763+
if await self.action(binding.action, namespace):
1764+
return True
17651765
return False
17661766

17671767
async def on_event(self, event: events.Event) -> None:
@@ -1830,32 +1830,41 @@ async def action(
18301830
async def _dispatch_action(
18311831
self, namespace: object, action_name: str, params: Any
18321832
) -> bool:
1833+
"""Dispatch an action to an action method.
1834+
1835+
Args:
1836+
namespace (object): Namespace (object) of action.
1837+
action_name (str): Name of the action.
1838+
params (Any): Action parameters.
1839+
1840+
Returns:
1841+
bool: True if handled, otherwise False.
1842+
"""
1843+
_rich_traceback_guard = True
1844+
18331845
log(
18341846
"<action>",
18351847
namespace=namespace,
18361848
action_name=action_name,
18371849
params=params,
18381850
)
1839-
_rich_traceback_guard = True
1840-
1841-
public_method_name = f"action_{action_name}"
1842-
private_method_name = f"_{public_method_name}"
18431851

1844-
private_method = getattr(namespace, private_method_name, None)
1845-
public_method = getattr(namespace, public_method_name, None)
1846-
1847-
if private_method is None and public_method is None:
1852+
try:
1853+
private_method = getattr(namespace, f"_action_{action_name}", None)
1854+
if callable(private_method):
1855+
await invoke(private_method, *params)
1856+
return True
1857+
public_method = getattr(namespace, f"action_{action_name}", None)
1858+
if callable(public_method):
1859+
await invoke(public_method, *params)
1860+
return True
18481861
log(
1849-
f"<action> {action_name!r} has no target. Couldn't find methods {public_method_name!r} or {private_method_name!r}"
1862+
f"<action> {action_name!r} has no target."
1863+
f" Could not find methods '_action_{action_name}' or 'action_{action_name}'"
18501864
)
1851-
1852-
if callable(private_method):
1853-
await invoke(private_method, *params)
1854-
return True
1855-
elif callable(public_method):
1856-
await invoke(public_method, *params)
1857-
return True
1858-
1865+
except SkipAction:
1866+
# The action method raised this to explicitly not handle the action
1867+
log("<action> {action_name!r} skipped.")
18591868
return False
18601869

18611870
async def _broker_event(
@@ -1866,7 +1875,7 @@ async def _broker_event(
18661875
Args:
18671876
event_name (str): _description_
18681877
event (events.Event): An event object.
1869-
default_namespace (object | None): TODO: _description_
1878+
default_namespace (object | None): The default namespace, where one isn't supplied.
18701879
18711880
Returns:
18721881
bool: True if an action was processed.

src/textual/css/_style_properties.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -794,9 +794,8 @@ def __set__(self, obj: StylesBase, names: str | tuple[str] | None = None):
794794
class ColorProperty:
795795
"""Descriptor for getting and setting color properties."""
796796

797-
def __init__(self, default_color: Color | str, background: bool = False) -> None:
797+
def __init__(self, default_color: Color | str) -> None:
798798
self._default_color = Color.parse(default_color)
799-
self._is_background = background
800799

801800
def __set_name__(self, owner: StylesBase, name: str) -> None:
802801
self.name = name
@@ -830,11 +829,10 @@ def __set__(self, obj: StylesBase, color: Color | str | None):
830829
_rich_traceback_omit = True
831830
if color is None:
832831
if obj.clear_rule(self.name):
833-
obj.refresh(children=self._is_background)
832+
obj.refresh(children=True)
834833
elif isinstance(color, Color):
835834
if obj.set_rule(self.name, color):
836-
obj.refresh(children=self._is_background)
837-
835+
obj.refresh(children=True)
838836
elif isinstance(color, str):
839837
alpha = 1.0
840838
parsed_color = Color(255, 255, 255)
@@ -855,8 +853,9 @@ def __set__(self, obj: StylesBase, color: Color | str | None):
855853
),
856854
)
857855
parsed_color = parsed_color.with_alpha(alpha)
856+
858857
if obj.set_rule(self.name, parsed_color):
859-
obj.refresh(children=self._is_background)
858+
obj.refresh(children=True)
860859
else:
861860
raise StyleValueError(f"Invalid color value {color}")
862861

src/textual/css/styles.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ class StylesBase(ABC):
214214

215215
auto_color = BooleanProperty(default=False)
216216
color = ColorProperty(Color(255, 255, 255))
217-
background = ColorProperty(Color(0, 0, 0, 0), background=True)
217+
background = ColorProperty(Color(0, 0, 0, 0))
218218
text_style = StyleFlagsProperty()
219219

220220
opacity = FractionalProperty()
@@ -421,7 +421,7 @@ def refresh(self, *, layout: bool = False, children: bool = False) -> None:
421421
422422
Args:
423423
layout (bool, optional): Also require a layout. Defaults to False.
424-
children (bool, opional): Also refresh children. Defaults to False.
424+
children (bool, optional): Also refresh children. Defaults to False.
425425
"""
426426

427427
@abstractmethod

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/reactive.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,11 @@ def __set__(self, obj: Reactable, value: ReactiveType) -> None:
175175
current_value = getattr(obj, name)
176176
# Check for validate function
177177
validate_function = getattr(obj, f"validate_{name}", None)
178-
# Check if this is the first time setting the value
179-
first_set = getattr(obj, f"__first_set_{self.internal_name}", True)
180178
# Call validate
181179
if callable(validate_function):
182180
value = validate_function(value)
183181
# If the value has changed, or this is the first time setting the value
184-
if current_value != value or first_set or self._always_update:
185-
# Set the first set flag to False
186-
setattr(obj, f"__first_set_{self.internal_name}", False)
182+
if current_value != value or self._always_update:
187183
# Store the internal value
188184
setattr(obj, self.internal_name, value)
189185
# Check all watchers
@@ -200,7 +196,6 @@ def _check_watchers(cls, obj: Reactable, name: str, old_value: Any):
200196
obj (Reactable): The reactable object.
201197
name (str): Attribute name.
202198
old_value (Any): The old (previous) value of the attribute.
203-
first_set (bool, optional): True if this is the first time setting the value. Defaults to False.
204199
"""
205200
_rich_traceback_omit = True
206201
# Get the current value.

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: 43 additions & 23 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
@@ -2175,8 +2176,14 @@ def refresh(
21752176

21762177
if layout:
21772178
self._layout_required = True
2178-
if isinstance(self._parent, Widget):
2179-
self._parent._clear_arrangement_cache()
2179+
for ancestor in self.ancestors:
2180+
if not isinstance(ancestor, Widget):
2181+
break
2182+
if ancestor.styles.auto_dimensions:
2183+
for ancestor in self.ancestors_with_self:
2184+
if isinstance(ancestor, Widget):
2185+
ancestor._clear_arrangement_cache()
2186+
break
21802187

21812188
if repaint:
21822189
self._set_dirty(*regions)
@@ -2344,17 +2351,22 @@ def _on_enter(self, event: events.Enter) -> None:
23442351
self.mouse_over = True
23452352

23462353
def _on_focus(self, event: events.Focus) -> None:
2347-
for node in self.ancestors_with_self:
2348-
if node._has_focus_within:
2349-
self.app.update_styles(node)
23502354
self.has_focus = True
23512355
self.refresh()
2356+
self.emit_no_wait(events.DescendantFocus(self))
23522357

23532358
def _on_blur(self, event: events.Blur) -> None:
2354-
if any(node._has_focus_within for node in self.ancestors_with_self):
2355-
self.app.update_styles(self)
23562359
self.has_focus = False
23572360
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)
23582370

23592371
def _on_mouse_scroll_down(self, event) -> None:
23602372
if self.allow_vertical_scroll:
@@ -2399,33 +2411,41 @@ def _on_scroll_to_region(self, message: messages.ScrollToRegion) -> None:
23992411
self.scroll_to_region(message.region, animate=True)
24002412

24012413
def action_scroll_home(self) -> None:
2402-
if self._allow_scroll:
2403-
self.scroll_home()
2414+
if not self._allow_scroll:
2415+
raise SkipAction()
2416+
self.scroll_home()
24042417

24052418
def action_scroll_end(self) -> None:
2406-
if self._allow_scroll:
2407-
self.scroll_end()
2419+
if not self._allow_scroll:
2420+
raise SkipAction()
2421+
self.scroll_end()
24082422

24092423
def action_scroll_left(self) -> None:
2410-
if self.allow_horizontal_scroll:
2411-
self.scroll_left()
2424+
if not self.allow_horizontal_scroll:
2425+
raise SkipAction()
2426+
self.scroll_left()
24122427

24132428
def action_scroll_right(self) -> None:
2414-
if self.allow_horizontal_scroll:
2415-
self.scroll_right()
2429+
if not self.allow_horizontal_scroll:
2430+
raise SkipAction()
2431+
self.scroll_right()
24162432

24172433
def action_scroll_up(self) -> None:
2418-
if self.allow_vertical_scroll:
2419-
self.scroll_up()
2434+
if not self.allow_vertical_scroll:
2435+
raise SkipAction()
2436+
self.scroll_up()
24202437

24212438
def action_scroll_down(self) -> None:
2422-
if self.allow_vertical_scroll:
2423-
self.scroll_down()
2439+
if not self.allow_vertical_scroll:
2440+
raise SkipAction()
2441+
self.scroll_down()
24242442

24252443
def action_page_down(self) -> None:
2426-
if self.allow_vertical_scroll:
2427-
self.scroll_page_down()
2444+
if not self.allow_vertical_scroll:
2445+
raise SkipAction()
2446+
self.scroll_page_down()
24282447

24292448
def action_page_up(self) -> None:
2430-
if self.allow_vertical_scroll:
2431-
self.scroll_page_up()
2449+
if not self.allow_vertical_scroll:
2450+
raise SkipAction()
2451+
self.scroll_page_up()

0 commit comments

Comments
 (0)