This commit is contained in:
Will McGugan
2022-09-17 10:31:21 +01:00
parent 76404b70f5
commit 1013f84ffc
7 changed files with 122 additions and 30 deletions

View File

@@ -51,14 +51,40 @@ Let's explore how Textual decides what method to call for a given event.
### Default behaviors
You may be familiar with using Python's [super](https://docs.python.org/3/library/functions.html#super) function to call a function defined in a base class. You will not have to do this for Textual event handlers as Textual will automatically call any handler methods defined in the base class *after* the current handler has run. This allows textual to run any default behavior for the given event.
You may be familiar with Python's [super](https://docs.python.org/3/library/functions.html#super) function to call a function defined in a base class. You will not have to do this for Textual event handlers as Textual will automatically call any handler methods defined in the base class.
For instance if a widget defines an `on_key` handler it will run when the user hits a key. Textual will also run `Widget.on_key`, which allows Textual to respond to any key bindings. This is generally desirable, but you can prevent Textual from running the base class handler by calling [prevent_default()][textual.message.Message.prevent_default] on the event object.
For instance if you define a custom widget, Textual will call its `on_key` handler when you hit a key. Textual will also run any `on_key` methods found in the widget's base classes, including `Widget.on_key` where key bindings are processed. Without this behavior, you would have to remember to call `super().on_key(event)` or key bindings would break.
If you don't want this behavior you can call [prevent_default()][textual.message.Message.prevent_default] on the event object. This tells Textual not to call any handlers on base classes.
For the case of key events, you may want to prevent the default behavior for keys that you handle by calling `event.prevent_default()`, but allow the base class to handle all other keys.
### Bubbling
Messages have a `bubble` attribute. If this is set to `True` then events will be sent to their parent widget. Input events typically bubble so that a widget will have the opportunity to process events after its children.
The following diagram shows an (abbreviated) DOM for a UI with a container and two buttons. With the "No" button [focused](#) it will receive the key event first.
<div class="excalidraw">
--8<-- "docs/images/events/bubble1.excalidraw.svg"
</div>
After Textual calls `Button.on_key` it _bubbles_ the event to its parent and call `Container.on_key` (if it exists).
<div class="excalidraw">
--8<-- "docs/images/events/bubble2.excalidraw.svg"
</div>
Then it will bubble to the container's parent (the App class).
<div class="excalidraw">
--8<-- "docs/images/events/bubble3.excalidraw.svg"
</div>
The App class is always the root of the DOM, so there is no where for the event to bubble to.
#### Stopping bubbling
Event handlers may stop this bubble behavior by calling the [stop()][textual.message.Message.stop] method on the event or message. You might want to do this if a widget has responded to the event in an authoritative way. For instance if a text input widget as responded to a key event you probably do not want it to also invoke a key binding.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -71,10 +71,11 @@ class Message:
@property
def handler_name(self) -> str:
"""The name of the handler associated with this message."""
# Property to make it read only
return self._handler_name
def set_forwarded(self) -> None:
def _set_forwarded(self) -> None:
"""Mark this event as being forwarded."""
self._forwarded = True
@@ -90,7 +91,8 @@ class Message:
return False
def prevent_default(self, prevent: bool = True) -> Message:
"""Suppress the default action.
"""Suppress the default action(s). This will prevent handlers in any base classes
from being called.
Args:
prevent (bool, optional): True if the default action should be suppressed,

View File

@@ -71,6 +71,22 @@ class ScrollView(Widget):
def watch_virtual_size(self, virtual_size: Size) -> None:
self._scroll_update(virtual_size)
def watch_show_horizontal_scrollbar(self, value: bool) -> None:
"""Watch function for show_horizontal_scrollbar attribute.
Args:
value (bool): Show horizontal scrollbar flag.
"""
self.refresh(layout=True)
def watch_show_vertical_scrollbar(self, value: bool) -> None:
"""Watch function for show_vertical_scrollbar attribute.
Args:
value (bool): Show vertical scrollbar flag.
"""
self.refresh(layout=True)
def _size_updated(
self, size: Size, virtual_size: Size, container_size: Size
) -> None:

View File

@@ -234,10 +234,10 @@ class Widget(DOMNode):
Args:
value (bool): Show horizontal scrollbar flag.
"""
self.refresh(layout=True)
# if not value:
# # reset the scroll position if the scrollbar is hidden.
# self.scroll_to(0, 0, animate=False)
# self.refresh(layout=True)
if not value:
# reset the scroll position if the scrollbar is hidden.
self.scroll_to(0, 0, animate=False)
def watch_show_vertical_scrollbar(self, value: bool) -> None:
"""Watch function for show_vertical_scrollbar attribute.
@@ -245,10 +245,10 @@ class Widget(DOMNode):
Args:
value (bool): Show vertical scrollbar flag.
"""
self.refresh(layout=True)
# if not value:
# # reset the scroll position if the scrollbar is hidden.
# self.scroll_to(0, 0, animate=False)
# self.refresh(layout=True)
if not value:
# reset the scroll position if the scrollbar is hidden.
self.scroll_to(0, 0, animate=False)
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
"""Mount child widgets (making this widget a container).
@@ -507,7 +507,7 @@ class Widget(DOMNode):
elif overflow_y == "auto":
show_vertical = self.virtual_size.height > height
if show_vertical and not show_horizontal:
if show_vertical and not show_horizontal and overflow_x == "auto":
show_horizontal = (
self.virtual_size.width + styles.scrollbar_size_vertical > width
)
@@ -853,7 +853,7 @@ class Widget(DOMNode):
y (int | None, optional): Y coordinate (row) to scroll to, or None for no change. Defaults to None.
animate (bool, optional): Animate to new scroll position. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if the scroll position changed, otherwise False.
@@ -895,8 +895,8 @@ class Widget(DOMNode):
scroll_y = self.scroll_y
self.scroll_target_y = self.scroll_y = y
scrolled_y = scroll_y != self.scroll_y
if scrolled_x or scrolled_y:
self.refresh(repaint=False, layout=True)
# if scrolled_x or scrolled_y:
# self.refresh(repaint=False, layout=False)
return scrolled_x or scrolled_y
@@ -916,7 +916,7 @@ class Widget(DOMNode):
y (int | None, optional): Y distance (rows) to scroll, or ``None`` for no change. Defaults to None.
animate (bool, optional): Animate to new scroll position. Defaults to False.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if the scroll position changed, otherwise False.
@@ -941,7 +941,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -962,7 +962,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -986,7 +986,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1008,7 +1008,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1030,7 +1030,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1052,7 +1052,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1074,7 +1074,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1099,7 +1099,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1124,7 +1124,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1151,7 +1151,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1408,8 +1408,8 @@ class Widget(DOMNode):
self._container_size = container_size
if self.is_scrollable:
self._scroll_update(virtual_size)
self.refresh(layout=True)
self.scroll_to(self.scroll_x, self.scroll_y)
# self.refresh(layout=True)
# self.scroll_to(self.scroll_x, self.scroll_y)
else:
self.refresh()