mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
lots of docstrings
This commit is contained in:
@@ -20,7 +20,7 @@ Textual requires Python 3.7 or later. Textual runs on Linux, MacOS, Windows and
|
||||
|
||||
## Installation
|
||||
|
||||
You can install Textual via PyPi.
|
||||
You can install Textual via PyPI.
|
||||
|
||||
If you plan on developing Textual apps, then you should install `textual[dev]`. The `[dev]` part installs a few extra dependencies for development.
|
||||
|
||||
@@ -34,7 +34,7 @@ If you only plan on _running_ Textual apps, then you can drop the `[dev]` part:
|
||||
pip install textual
|
||||
```
|
||||
|
||||
## Textual CLI app
|
||||
## Textual CLI
|
||||
|
||||
If you installed the dev dependencies you have have access to the `textual` CLI command. There are a number of sub-commands which will aid you in building Textual apps.
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ If you want to try the finished Stopwatch app and following along with the code
|
||||
gh repo clone Textualize/textual
|
||||
```
|
||||
|
||||
With the repository cloned, navigate to `/docs/examples/introduction` and run `stopwatch.py`.
|
||||
With the repository cloned, navigate to `docs/examples/introduction` and run `stopwatch.py`.
|
||||
|
||||
```bash
|
||||
cd textual/docs/examples/introduction
|
||||
@@ -69,7 +69,7 @@ def repeat(text: str, count: int) -> str:
|
||||
```
|
||||
|
||||
- Parameter types follow a colon. So `text: str` indicates that `text` requires a string and `count: int` means that `count` requires an integer.
|
||||
- Return types follow `->`. So `-> str:` indicates that this method returns a string.
|
||||
- Return types follow `->`. So `-> str:` indicates this method returns a string.
|
||||
|
||||
|
||||
## The App class
|
||||
@@ -335,7 +335,7 @@ If you run "stopwatch04.py" now you will be able to toggle between the two state
|
||||
|
||||
## Reactive attributes
|
||||
|
||||
A reoccurring theme in Textual is that you rarely need to explicitly update a widget. It is possible: you can call `refresh()` to display new data. However, Textual prefers to do this automatically via _reactive_ attributes.
|
||||
A reoccurring theme in Textual is that you rarely need to explicitly update a widget. It is possible: you can call [`refresh()`][textual.widget.Widget.refresh] to display new data. However, Textual prefers to do this automatically via _reactive_ attributes.
|
||||
|
||||
You can declare a reactive attribute with `textual.reactive.Reactive`. Let's use this feature to create a timer that displays elapsed time and keeps it updated.
|
||||
|
||||
|
||||
5
docs/reference/message_pump.md
Normal file
5
docs/reference/message_pump.md
Normal file
@@ -0,0 +1,5 @@
|
||||
A message pump is a class that processes messages.
|
||||
|
||||
It is a base class for the App, Screen, and Widgets.
|
||||
|
||||
::: textual.message_pump.MessagePump
|
||||
1
docs/reference/timer.md
Normal file
1
docs/reference/timer.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.timer
|
||||
@@ -67,6 +67,8 @@ nav:
|
||||
- "reference/dom_node.md"
|
||||
- "reference/events.md"
|
||||
- "reference/geometry.md"
|
||||
- "reference/message_pump.md"
|
||||
- "reference/timer.md"
|
||||
- "reference/widget.md"
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from dataclasses import dataclass
|
||||
from . import _clock
|
||||
from ._callback import invoke
|
||||
from ._easing import DEFAULT_EASING, EASING
|
||||
from ._timer import Timer
|
||||
from .timer import Timer
|
||||
from ._types import MessageTarget, CallbackType
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
|
||||
@@ -64,7 +64,7 @@ def arrange(
|
||||
fraction_unit = Fraction(
|
||||
size.height if edge in ("top", "bottom") else size.width
|
||||
)
|
||||
box_model = dock_widget.get_box_model(size, viewport, fraction_unit)
|
||||
box_model = dock_widget._get_box_model(size, viewport, fraction_unit)
|
||||
widget_width_fraction, widget_height_fraction, margin = box_model
|
||||
|
||||
widget_width = int(widget_width_fraction) + margin.width
|
||||
@@ -98,7 +98,7 @@ def arrange(
|
||||
|
||||
dock_spacing = Spacing(top, right, bottom, left)
|
||||
region = size.region.shrink(dock_spacing)
|
||||
layout_placements, arranged_layout_widgets = widget.layout.arrange(
|
||||
layout_placements, arranged_layout_widgets = widget._layout.arrange(
|
||||
widget, layout_widgets, region.size
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterator, overload, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Iterator, Sequence, overload
|
||||
|
||||
import rich.repr
|
||||
|
||||
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
@rich.repr.auto(angular=True)
|
||||
class NodeList:
|
||||
class NodeList(Sequence):
|
||||
"""
|
||||
A container for widgets that forms one level of hierarchy.
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
result (ReturnType | None, optional): Return value. Defaults to None.
|
||||
"""
|
||||
self._return_value = result
|
||||
self.close_messages_no_wait()
|
||||
self._close_messages_no_wait()
|
||||
|
||||
@property
|
||||
def focus_chain(self) -> list[Widget]:
|
||||
@@ -458,6 +458,14 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
@property
|
||||
def screen(self) -> Screen:
|
||||
"""Get the current screen.
|
||||
|
||||
Raises:
|
||||
ScreenStackError: If there are no screens on the stack.
|
||||
|
||||
Returns:
|
||||
Screen: The currently active screen.
|
||||
"""
|
||||
try:
|
||||
return self._screen_stack[-1]
|
||||
except IndexError:
|
||||
@@ -465,6 +473,11 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
@property
|
||||
def size(self) -> Size:
|
||||
"""Get the size of the terminal.
|
||||
|
||||
Returns:
|
||||
Size: SIze of the terminal
|
||||
"""
|
||||
return Size(*self.console.size)
|
||||
|
||||
def log(
|
||||
@@ -640,9 +653,9 @@ class App(Generic[ReturnType], DOMNode):
|
||||
"""Press some keys in the background."""
|
||||
asyncio.create_task(press_keys())
|
||||
|
||||
await self.process_messages(ready_callback=press_keys_task)
|
||||
await self._process_messages(ready_callback=press_keys_task)
|
||||
else:
|
||||
await self.process_messages()
|
||||
await self._process_messages()
|
||||
|
||||
if _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED:
|
||||
# N.B. This doesn't work with Python<3.10, as we end up with 2 event loops:
|
||||
@@ -971,7 +984,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
]
|
||||
|
||||
self._exit_renderables.extend(pre_rendered)
|
||||
self.close_messages_no_wait()
|
||||
self._close_messages_no_wait()
|
||||
|
||||
def on_exception(self, error: Exception) -> None:
|
||||
"""Called with an unhandled exception.
|
||||
@@ -996,14 +1009,14 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self._exit_renderables.append(
|
||||
Segments(self.console.render(traceback, self.console.options))
|
||||
)
|
||||
self.close_messages_no_wait()
|
||||
self._close_messages_no_wait()
|
||||
|
||||
def _print_error_renderables(self) -> None:
|
||||
for renderable in self._exit_renderables:
|
||||
self.error_console.print(renderable)
|
||||
self._exit_renderables.clear()
|
||||
|
||||
async def process_messages(
|
||||
async def _process_messages(
|
||||
self, ready_callback: CallbackType | None = None
|
||||
) -> None:
|
||||
self._set_active()
|
||||
@@ -1038,11 +1051,11 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self.set_interval(0.5, self.css_monitor, name="css monitor")
|
||||
self.log("[b green]STARTED[/]", self.css_monitor)
|
||||
|
||||
process_messages = super().process_messages
|
||||
process_messages = super()._process_messages
|
||||
|
||||
async def run_process_messages():
|
||||
mount_event = events.Mount(sender=self)
|
||||
await self.dispatch_message(mount_event)
|
||||
await self._dispatch_message(mount_event)
|
||||
|
||||
self.title = self._title
|
||||
self.stylesheet.update(self)
|
||||
@@ -1058,7 +1071,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self._running = True
|
||||
try:
|
||||
load_event = events.Load(sender=self)
|
||||
await self.dispatch_message(load_event)
|
||||
await self._dispatch_message(load_event)
|
||||
|
||||
driver: Driver
|
||||
driver_class = cast(
|
||||
@@ -1134,7 +1147,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self._registry.add(child)
|
||||
child._attach(parent)
|
||||
child._post_register(self)
|
||||
child.start_messages()
|
||||
child._start_messages()
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -1190,7 +1203,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
widget (Widget): The Widget to start.
|
||||
"""
|
||||
widget._attach(parent)
|
||||
widget.start_messages()
|
||||
widget._start_messages()
|
||||
widget.post_message_no_wait(events.Mount(sender=parent))
|
||||
|
||||
def is_mounted(self, widget: Widget) -> bool:
|
||||
@@ -1199,14 +1212,14 @@ class App(Generic[ReturnType], DOMNode):
|
||||
async def close_all(self) -> None:
|
||||
while self._registry:
|
||||
child = self._registry.pop()
|
||||
await child.close_messages()
|
||||
await child._close_messages()
|
||||
|
||||
async def shutdown(self):
|
||||
await self._disconnect_devtools()
|
||||
driver = self._driver
|
||||
if driver is not None:
|
||||
driver.disable_input()
|
||||
await self.close_messages()
|
||||
await self._close_messages()
|
||||
|
||||
def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
|
||||
self.screen.refresh(repaint=repaint, layout=layout)
|
||||
@@ -1343,9 +1356,9 @@ class App(Generic[ReturnType], DOMNode):
|
||||
action_target = default_namespace or self
|
||||
action_name = target
|
||||
|
||||
await self.dispatch_action(action_target, action_name, params)
|
||||
await self._dispatch_action(action_target, action_name, params)
|
||||
|
||||
async def dispatch_action(
|
||||
async def _dispatch_action(
|
||||
self, namespace: object, action_name: str, params: Any
|
||||
) -> None:
|
||||
log(
|
||||
@@ -1362,7 +1375,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
if callable(method):
|
||||
await invoke(method, *params)
|
||||
|
||||
async def broker_event(
|
||||
async def _broker_event(
|
||||
self, event_name: str, event: events.Event, default_namespace: object | None
|
||||
) -> bool:
|
||||
"""Allow the app an opportunity to dispatch events to action system.
|
||||
@@ -1411,7 +1424,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None:
|
||||
log("shutdown request")
|
||||
await self.close_messages()
|
||||
await self._close_messages()
|
||||
|
||||
async def _on_resize(self, event: events.Resize) -> None:
|
||||
event.stop()
|
||||
@@ -1428,7 +1441,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
for child in remove_widgets:
|
||||
self._unregister(child)
|
||||
for child in remove_widgets:
|
||||
await child.close_messages()
|
||||
await child._close_messages()
|
||||
|
||||
async def action_press(self, key: str) -> None:
|
||||
await self.press(key)
|
||||
|
||||
@@ -464,8 +464,8 @@ if __name__ == "__main__":
|
||||
app = App()
|
||||
main_view = View(id="main")
|
||||
help_view = View(id="help")
|
||||
app.add_child(main_view)
|
||||
app.add_child(help_view)
|
||||
app._add_child(main_view)
|
||||
app._add_child(help_view)
|
||||
|
||||
widget1 = Widget(id="widget1")
|
||||
widget2 = Widget(id="widget2")
|
||||
@@ -475,21 +475,21 @@ if __name__ == "__main__":
|
||||
helpbar = Widget(id="helpbar")
|
||||
helpbar.add_class("float")
|
||||
|
||||
main_view.add_child(widget1)
|
||||
main_view.add_child(widget2)
|
||||
main_view.add_child(sidebar)
|
||||
main_view._add_child(widget1)
|
||||
main_view._add_child(widget2)
|
||||
main_view._add_child(sidebar)
|
||||
|
||||
sub_view = View(id="sub")
|
||||
sub_view.add_class("-subview")
|
||||
main_view.add_child(sub_view)
|
||||
main_view._add_child(sub_view)
|
||||
|
||||
tooltip = Widget(id="tooltip")
|
||||
tooltip.add_class("float", "transient")
|
||||
sub_view.add_child(tooltip)
|
||||
sub_view._add_child(tooltip)
|
||||
|
||||
help = Widget(id="markdown")
|
||||
help_view.add_child(help)
|
||||
help_view.add_child(helpbar)
|
||||
help_view._add_child(help)
|
||||
help_view._add_child(helpbar)
|
||||
|
||||
from rich import print
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ from .css.parse import parse_declarations
|
||||
from .css.styles import Styles, RenderStyles
|
||||
from .css.query import NoMatchingNodesError
|
||||
from .message_pump import MessagePump
|
||||
from ._timer import Timer
|
||||
from .timer import Timer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .app import App
|
||||
@@ -515,7 +515,7 @@ class DOMNode(MessagePump):
|
||||
node._set_dirty()
|
||||
node._layout_required = True
|
||||
|
||||
def add_child(self, node: Widget) -> None:
|
||||
def _add_child(self, node: Widget) -> None:
|
||||
"""Add a new child node.
|
||||
|
||||
Args:
|
||||
@@ -524,7 +524,7 @@ class DOMNode(MessagePump):
|
||||
self.children._append(node)
|
||||
node._attach(self)
|
||||
|
||||
def add_children(self, *nodes: Widget, **named_nodes: Widget) -> None:
|
||||
def _add_children(self, *nodes: Widget, **named_nodes: Widget) -> None:
|
||||
"""Add multiple children to this node.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -243,6 +243,6 @@ if __name__ == "__main__":
|
||||
|
||||
class MyApp(App):
|
||||
async def on_mount(self, event: events.Mount) -> None:
|
||||
self.set_timer(5, callback=self.close_messages)
|
||||
self.set_timer(5, callback=self._close_messages)
|
||||
|
||||
MyApp.run()
|
||||
|
||||
@@ -13,8 +13,8 @@ from .message import Message
|
||||
MouseEventT = TypeVar("MouseEventT", bound="MouseEvent")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._timer import Timer as TimerClass
|
||||
from ._timer import TimerCallback
|
||||
from .timer import Timer as TimerClass
|
||||
from .timer import TimerCallback
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class CenterLayout(Layout):
|
||||
fraction_unit = Fraction(size.width)
|
||||
|
||||
for widget in children:
|
||||
width, height, margin = widget.get_box_model(
|
||||
width, height, margin = widget._get_box_model(
|
||||
size, parent_size, fraction_unit
|
||||
)
|
||||
margin_width = width + margin.width
|
||||
|
||||
@@ -33,7 +33,7 @@ class HorizontalLayout(Layout):
|
||||
fraction_unit = Fraction(size.width, total_fraction or 1)
|
||||
|
||||
box_models = [
|
||||
widget.get_box_model(size, parent_size, fraction_unit)
|
||||
widget._get_box_model(size, parent_size, fraction_unit)
|
||||
for widget in cast("list[Widget]", children)
|
||||
]
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class VerticalLayout(Layout):
|
||||
fraction_unit = Fraction(size.height, total_fraction or 1)
|
||||
|
||||
box_models = [
|
||||
widget.get_box_model(size, parent_size, fraction_unit)
|
||||
widget._get_box_model(size, parent_size, fraction_unit)
|
||||
for widget in children
|
||||
]
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
"""
|
||||
|
||||
A message pump is a class that processes messages.
|
||||
|
||||
It is a base class for the App, Screen, and Widgets.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@@ -10,7 +18,7 @@ from weakref import WeakSet
|
||||
from . import events, log, messages
|
||||
from ._callback import invoke
|
||||
from ._context import NoActiveAppError, active_app
|
||||
from ._timer import Timer, TimerCallback
|
||||
from .timer import Timer, TimerCallback
|
||||
from .case import camel_to_snake
|
||||
from .events import Event
|
||||
from .message import Message
|
||||
@@ -81,6 +89,9 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
"""
|
||||
Get the current app.
|
||||
|
||||
Returns:
|
||||
App: The current app.
|
||||
|
||||
Raises:
|
||||
NoActiveAppError: if no active app could be found for the current asyncio context
|
||||
"""
|
||||
@@ -90,14 +101,17 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
raise NoActiveAppError()
|
||||
|
||||
@property
|
||||
def is_parent_active(self):
|
||||
return self._parent and not self._parent._closed and not self._parent._closing
|
||||
def is_parent_active(self) -> bool:
|
||||
return bool(
|
||||
self._parent and not self._parent._closed and not self._parent._closing
|
||||
)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
return self._running
|
||||
|
||||
def log(self, *args, **kwargs) -> None:
|
||||
"""Write to logs or devtools."""
|
||||
return self.app.log(*args, **kwargs, _textual_calling_frame=inspect.stack()[1])
|
||||
|
||||
def _attach(self, parent: MessagePump) -> None:
|
||||
@@ -123,7 +137,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
"""Enable processing of messages types."""
|
||||
self._disabled_messages.difference_update(messages)
|
||||
|
||||
async def get_message(self) -> Message:
|
||||
async def _get_message(self) -> Message:
|
||||
"""Get the next event on the queue, or None if queue is closed.
|
||||
|
||||
Returns:
|
||||
@@ -142,7 +156,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
raise MessagePumpClosed("The message pump is now closed")
|
||||
return message
|
||||
|
||||
def peek_message(self) -> Message | None:
|
||||
def _peek_message(self) -> Message | None:
|
||||
"""Peek the message at the head of the queue (does not remove it from the queue),
|
||||
or return None if the queue is empty.
|
||||
|
||||
@@ -172,6 +186,17 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
name: str | None = None,
|
||||
pause: bool = False,
|
||||
) -> Timer:
|
||||
"""Make a function call after a delay.
|
||||
|
||||
Args:
|
||||
delay (float): Time to wait before invoking callback.
|
||||
callback (TimerCallback | None, optional): Callback to call after time has expired.. Defaults to None.
|
||||
name (str | None, optional): Name of the timer (for debug). Defaults to None.
|
||||
pause (bool, optional): Start timer paused. Defaults to False.
|
||||
|
||||
Returns:
|
||||
Timer: A timer object.
|
||||
"""
|
||||
timer = Timer(
|
||||
self,
|
||||
delay,
|
||||
@@ -194,6 +219,18 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
repeat: int = 0,
|
||||
pause: bool = False,
|
||||
):
|
||||
"""Call a function at periodic intervals.
|
||||
|
||||
Args:
|
||||
interval (float): Time between calls.
|
||||
callback (TimerCallback | None, optional): Function to call. Defaults to None.
|
||||
name (str | None, optional): Name of the timer object. Defaults to None.
|
||||
repeat (int, optional): Number of times to repeat the call or 0 for continuous. Defaults to 0.
|
||||
pause (bool, optional): Start the timer paused. Defaults to False.
|
||||
|
||||
Returns:
|
||||
Timer: A timer object.
|
||||
"""
|
||||
timer = Timer(
|
||||
self,
|
||||
interval,
|
||||
@@ -209,7 +246,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
|
||||
def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||
"""Schedule a callback to run after all messages are processed and the screen
|
||||
has been refreshed.
|
||||
has been refreshed. Positional and keyword arguments are passed to the callable.
|
||||
|
||||
Args:
|
||||
callback (Callable): A callable.
|
||||
@@ -219,15 +256,15 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
message = messages.InvokeLater(self, partial(callback, *args, **kwargs))
|
||||
self.post_message_no_wait(message)
|
||||
|
||||
def on_invoke_later(self, message: messages.InvokeLater) -> None:
|
||||
def _on_invoke_later(self, message: messages.InvokeLater) -> None:
|
||||
# Forward InvokeLater message to the Screen
|
||||
self.app.screen._invoke_later(message.callback)
|
||||
|
||||
def close_messages_no_wait(self) -> None:
|
||||
def _close_messages_no_wait(self) -> None:
|
||||
"""Request the message queue to exit."""
|
||||
self._message_queue.put_nowait(None)
|
||||
|
||||
async def close_messages(self) -> None:
|
||||
async def _close_messages(self) -> None:
|
||||
"""Close message queue, and optionally wait for queue to finish processing."""
|
||||
if self._closed or self._closing:
|
||||
return
|
||||
@@ -242,13 +279,14 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
# Ensure everything is closed before returning
|
||||
await self._task
|
||||
|
||||
def start_messages(self) -> None:
|
||||
self._task = asyncio.create_task(self.process_messages())
|
||||
def _start_messages(self) -> None:
|
||||
"""Start messages task."""
|
||||
self._task = asyncio.create_task(self._process_messages())
|
||||
|
||||
async def process_messages(self) -> None:
|
||||
async def _process_messages(self) -> None:
|
||||
self._running = True
|
||||
try:
|
||||
await self._process_messages()
|
||||
await self._process_messages_loop()
|
||||
except CancelledError:
|
||||
pass
|
||||
finally:
|
||||
@@ -256,14 +294,14 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
for timer in list(self._timers):
|
||||
await timer.stop()
|
||||
|
||||
async def _process_messages(self) -> None:
|
||||
async def _process_messages_loop(self) -> None:
|
||||
"""Process messages until the queue is closed."""
|
||||
_rich_traceback_guard = True
|
||||
|
||||
await Reactive.initialize_object(self)
|
||||
while not self._closed:
|
||||
try:
|
||||
message = await self.get_message()
|
||||
message = await self._get_message()
|
||||
except MessagePumpClosed:
|
||||
break
|
||||
except CancelledError:
|
||||
@@ -274,18 +312,18 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
# Combine any pending messages that may supersede this one
|
||||
while not (self._closed or self._closing):
|
||||
try:
|
||||
pending = self.peek_message()
|
||||
pending = self._peek_message()
|
||||
except MessagePumpClosed:
|
||||
break
|
||||
if pending is None or not message.can_replace(pending):
|
||||
break
|
||||
try:
|
||||
message = await self.get_message()
|
||||
message = await self._get_message()
|
||||
except MessagePumpClosed:
|
||||
break
|
||||
|
||||
try:
|
||||
await self.dispatch_message(message)
|
||||
await self._dispatch_message(message)
|
||||
except CancelledError:
|
||||
raise
|
||||
except Exception as error:
|
||||
@@ -307,7 +345,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
|
||||
log("CLOSED", self)
|
||||
|
||||
async def dispatch_message(self, message: Message) -> None:
|
||||
async def _dispatch_message(self, message: Message) -> None:
|
||||
"""Dispatch a message received from the message queue.
|
||||
|
||||
Args:
|
||||
@@ -458,6 +496,14 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
return False
|
||||
|
||||
async def emit(self, message: Message) -> bool:
|
||||
"""Send a message to the _parent_.
|
||||
|
||||
Args:
|
||||
message (Message): A message object.
|
||||
|
||||
Returns:
|
||||
bool: _True if the message was posted successfully.
|
||||
"""
|
||||
if self._parent:
|
||||
return await self._parent._post_message_from_child(message)
|
||||
else:
|
||||
|
||||
@@ -9,7 +9,7 @@ from rich.style import Style
|
||||
from . import errors, events, messages
|
||||
from ._callback import invoke
|
||||
from ._compositor import Compositor, MapGeometry
|
||||
from ._timer import Timer
|
||||
from .timer import Timer
|
||||
from ._types import CallbackType
|
||||
from .geometry import Offset, Region, Size
|
||||
from .reactive import Reactive
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
"""
|
||||
|
||||
Timer objects are created by [set_interval][textual.message_pump.MessagePump.set_interval] or
|
||||
[set_interval][textual.message_pump.MessagePump.set_timer].
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@@ -26,6 +33,19 @@ class EventTargetGone(Exception):
|
||||
|
||||
@rich_repr
|
||||
class Timer:
|
||||
"""A class to send timer-based events.
|
||||
|
||||
Args:
|
||||
event_target (MessageTarget): The object which will receive the timer events.
|
||||
interval (float): The time between timer events.
|
||||
sender (MessageTarget): The sender of the event.
|
||||
name (str | None, optional): A name to assign the event (for debugging). Defaults to None.
|
||||
callback (TimerCallback | None, optional): A optional callback to invoke when the event is handled. Defaults to None.
|
||||
repeat (int | None, optional): The number of times to repeat the timer, or None for no repeat. Defaults to None.
|
||||
skip (bool, optional): Enable skipping of scheduled events that couldn't be sent in time. Defaults to True.
|
||||
pause (bool, optional): Start the timer paused. Defaults to False.
|
||||
"""
|
||||
|
||||
_timer_count: int = 1
|
||||
|
||||
def __init__(
|
||||
@@ -40,18 +60,6 @@ class Timer:
|
||||
skip: bool = True,
|
||||
pause: bool = False,
|
||||
) -> None:
|
||||
"""A class to send timer-based events.
|
||||
|
||||
Args:
|
||||
event_target (MessageTarget): The object which will receive the timer events.
|
||||
interval (float): The time between timer events.
|
||||
sender (MessageTarget): The sender of the event.
|
||||
name (str | None, optional): A name to assign the event (for debugging). Defaults to None.
|
||||
callback (TimerCallback | None, optional): A optional callback to invoke when the event is handled. Defaults to None.
|
||||
repeat (int | None, optional): The number of times to repeat the timer, or None for no repeat. Defaults to None.
|
||||
skip (bool, optional): Enable skipping of scheduled events that couldn't be sent in time. Defaults to True.
|
||||
pause (bool, optional): Start the timer paused. Defaults to False.
|
||||
"""
|
||||
self._target_repr = repr(event_target)
|
||||
self._target = weakref.ref(event_target)
|
||||
self._interval = interval
|
||||
@@ -84,7 +92,7 @@ class Timer:
|
||||
Returns:
|
||||
Task: A Task instance for the timer.
|
||||
"""
|
||||
self._task = asyncio.create_task(self.run())
|
||||
self._task = asyncio.create_task(self._run_timer())
|
||||
return self._task
|
||||
|
||||
def stop_no_wait(self) -> None:
|
||||
@@ -101,14 +109,18 @@ class Timer:
|
||||
self._task = None
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause the timer."""
|
||||
"""Pause the timer.
|
||||
|
||||
A paused timer will not send events until it is resumed.
|
||||
|
||||
"""
|
||||
self._active.clear()
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Result a paused timer."""
|
||||
"""Resume a paused timer."""
|
||||
self._active.set()
|
||||
|
||||
async def run(self) -> None:
|
||||
async def _run_timer(self) -> None:
|
||||
"""Run the timer task."""
|
||||
try:
|
||||
await self._run()
|
||||
@@ -135,7 +135,7 @@ class Widget(DOMNode):
|
||||
id=id,
|
||||
classes=self.DEFAULT_CLASSES if classes is None else classes,
|
||||
)
|
||||
self.add_children(*children)
|
||||
self._add_children(*children)
|
||||
|
||||
virtual_size = Reactive(Size(0, 0), layout=True)
|
||||
auto_width = Reactive(True)
|
||||
@@ -152,7 +152,11 @@ class Widget(DOMNode):
|
||||
|
||||
@property
|
||||
def siblings(self) -> list[Widget]:
|
||||
"""Get the widget's siblings (self is removed from the return list)."""
|
||||
"""Get the widget's siblings (self is removed from the return list).
|
||||
|
||||
Returns:
|
||||
list[Widget]: A list of siblings.
|
||||
"""
|
||||
parent = self.parent
|
||||
if parent is not None:
|
||||
siblings = list(parent.children)
|
||||
@@ -163,14 +167,35 @@ class Widget(DOMNode):
|
||||
|
||||
@property
|
||||
def allow_vertical_scroll(self) -> bool:
|
||||
"""Check if vertical scroll is permitted."""
|
||||
"""Check if vertical scroll is permitted.
|
||||
|
||||
May be overridden if you want different logic regarding allowing scrolling.
|
||||
|
||||
Returns:
|
||||
bool: True if the widget may scroll _vertically_.
|
||||
"""
|
||||
return self.is_scrollable and self.show_vertical_scrollbar
|
||||
|
||||
@property
|
||||
def allow_horizontal_scroll(self) -> bool:
|
||||
"""Check if horizontal scroll is permitted."""
|
||||
"""Check if horizontal scroll is permitted.
|
||||
|
||||
May be overridden if you want different logic regarding allowing scrolling.
|
||||
|
||||
Returns:
|
||||
bool: True if the widget may scroll _horizontally_.
|
||||
"""
|
||||
return self.is_scrollable and self.show_horizontal_scrollbar
|
||||
|
||||
@property
|
||||
def _allow_scroll(self) -> bool:
|
||||
"""Check if both axis may be scrolled.
|
||||
|
||||
Returns:
|
||||
bool: True if horizontal and vertical scrolling is enabled.
|
||||
"""
|
||||
return self.allow_horizontal_scroll and self.allow_vertical_scroll
|
||||
|
||||
def _arrange(self, size: Size) -> DockArrangeResult:
|
||||
"""Arrange children.
|
||||
|
||||
@@ -193,6 +218,7 @@ class Widget(DOMNode):
|
||||
return self._arrangement
|
||||
|
||||
def _clear_arrangement_cache(self) -> None:
|
||||
"""Clear arrangement cache, forcing a new arrange operation."""
|
||||
self._arrangement = None
|
||||
|
||||
def watch_show_horizontal_scrollbar(self, value: bool) -> None:
|
||||
@@ -222,15 +248,30 @@ class Widget(DOMNode):
|
||||
the keys will be set as the Widget's id.
|
||||
|
||||
Example:
|
||||
```python
|
||||
self.mount(Static("hello"), header=Header())
|
||||
|
||||
```
|
||||
|
||||
"""
|
||||
self.app._register(self, *anon_widgets, **widgets)
|
||||
self.screen.refresh(layout=True)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Yield child widgets for a container."""
|
||||
"""Called by Textual to create child widgets.
|
||||
|
||||
Extend this to build a UI.
|
||||
|
||||
Example:
|
||||
```python
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Container(
|
||||
TreeControl(), Viewer()
|
||||
)
|
||||
yield Footer()
|
||||
```
|
||||
|
||||
"""
|
||||
return
|
||||
yield
|
||||
|
||||
@@ -246,7 +287,7 @@ class Widget(DOMNode):
|
||||
css, path=path, is_default_css=True, tie_breaker=tie_breaker
|
||||
)
|
||||
|
||||
def get_box_model(
|
||||
def _get_box_model(
|
||||
self, container: Size, viewport: Size, fraction_unit: Fraction
|
||||
) -> BoxModel:
|
||||
"""Process the box model for this widget.
|
||||
@@ -254,6 +295,7 @@ class Widget(DOMNode):
|
||||
Args:
|
||||
container (Size): The size of the container widget (with a layout)
|
||||
viewport (Size): The viewport size.
|
||||
fraction_unit (Fraction): The unit used for `fr` units.
|
||||
|
||||
Returns:
|
||||
BoxModel: The size and margin for this widget.
|
||||
@@ -279,9 +321,9 @@ class Widget(DOMNode):
|
||||
int: The optimal width of the content.
|
||||
"""
|
||||
if self.is_container:
|
||||
assert self.layout is not None
|
||||
assert self._layout is not None
|
||||
return (
|
||||
self.layout.get_content_width(self, container, viewport)
|
||||
self._layout.get_content_width(self, container, viewport)
|
||||
+ self.scrollbar_size_vertical
|
||||
)
|
||||
|
||||
@@ -313,9 +355,9 @@ class Widget(DOMNode):
|
||||
"""
|
||||
|
||||
if self.is_container:
|
||||
assert self.layout is not None
|
||||
assert self._layout is not None
|
||||
height = (
|
||||
self.layout.get_content_height(
|
||||
self._layout.get_content_height(
|
||||
self,
|
||||
container,
|
||||
viewport,
|
||||
@@ -330,8 +372,8 @@ class Widget(DOMNode):
|
||||
return self._content_height_cache[1]
|
||||
|
||||
renderable = self.render()
|
||||
options = self.console.options.update_width(width).update(highlight=False)
|
||||
segments = self.console.render(renderable, options)
|
||||
options = self._console.options.update_width(width).update(highlight=False)
|
||||
segments = self._console.render(renderable, options)
|
||||
# Cheaper than counting the lines returned from render_lines!
|
||||
height = sum(text.count("\n") for text, _, _ in segments)
|
||||
self._content_height_cache = (cache_key, height)
|
||||
@@ -611,7 +653,12 @@ class Widget(DOMNode):
|
||||
|
||||
@property
|
||||
def focusable_children(self) -> list[Widget]:
|
||||
"""Get the children which may be focused."""
|
||||
"""Get the children which may be focused.
|
||||
|
||||
Returns:
|
||||
list[Widget]: List of widgets that can receive focus.
|
||||
|
||||
"""
|
||||
focusable = [
|
||||
child for child in self.children if child.display and child.visible
|
||||
]
|
||||
@@ -619,12 +666,18 @@ class Widget(DOMNode):
|
||||
|
||||
@property
|
||||
def _focus_sort_key(self) -> tuple[int, int]:
|
||||
"""Key function to sort widgets in to tfocus order."""
|
||||
x, y, _, _ = self.virtual_region
|
||||
top, _, _, left = self.styles.margin
|
||||
return y - top, x - left
|
||||
|
||||
@property
|
||||
def scroll_offset(self) -> Offset:
|
||||
"""Get the current scroll offset.
|
||||
|
||||
Returns:
|
||||
Offset: Offset a container has been scrolled by.
|
||||
"""
|
||||
return Offset(int(self.scroll_x), int(self.scroll_y))
|
||||
|
||||
@property
|
||||
@@ -637,7 +690,7 @@ class Widget(DOMNode):
|
||||
return self.is_scrollable and self.styles.background.is_transparent
|
||||
|
||||
@property
|
||||
def console(self) -> Console:
|
||||
def _console(self) -> Console:
|
||||
"""Get the current console.
|
||||
|
||||
Returns:
|
||||
@@ -648,14 +701,29 @@ class Widget(DOMNode):
|
||||
|
||||
@property
|
||||
def animate(self) -> BoundAnimator:
|
||||
"""Get an animator to animate attributes on this widget.
|
||||
|
||||
Example:
|
||||
```python
|
||||
self.animate("brightness", 0.5)
|
||||
```
|
||||
|
||||
Returns:
|
||||
BoundAnimator: An animator bound to this widget.
|
||||
"""
|
||||
if self._animate is None:
|
||||
self._animate = self.app.animator.bind(self)
|
||||
assert self._animate is not None
|
||||
return self._animate
|
||||
|
||||
@property
|
||||
def layout(self) -> Layout:
|
||||
"""Get the layout object if set in styles, or a default layout."""
|
||||
def _layout(self) -> Layout:
|
||||
"""Get the layout object if set in styles, or a default layout.
|
||||
|
||||
Returns:
|
||||
Layout: A layout object.
|
||||
|
||||
"""
|
||||
return self.styles.layout or self._default_layout
|
||||
|
||||
@property
|
||||
@@ -745,9 +813,11 @@ class Widget(DOMNode):
|
||||
"""Scroll to a given (absolute) coordinate, optionally animating.
|
||||
|
||||
Args:
|
||||
x (int | None, optional): X coordinate (column) to scroll to, or ``None`` for no change. Defaults to None.
|
||||
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 False.
|
||||
x (int | None, optional): X coordinate (column) to scroll to, or None for no change. Defaults to None.
|
||||
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.
|
||||
|
||||
Returns:
|
||||
bool: True if the scroll position changed, otherwise False.
|
||||
@@ -809,6 +879,8 @@ class Widget(DOMNode):
|
||||
x (int | None, optional): X distance (columns) to scroll, or ``None`` for no change. Defaults to None.
|
||||
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.
|
||||
|
||||
Returns:
|
||||
bool: True if the scroll position changed, otherwise False.
|
||||
@@ -822,34 +894,114 @@ class Widget(DOMNode):
|
||||
)
|
||||
|
||||
def scroll_home(self, *, animate: bool = True) -> bool:
|
||||
"""Scroll to home position.
|
||||
|
||||
Args:
|
||||
animate (bool, optional): Animate scroll. Defaults to True.
|
||||
|
||||
Returns:
|
||||
bool: True if any scrolling was done.
|
||||
"""
|
||||
return self.scroll_to(0, 0, animate=animate, duration=1)
|
||||
|
||||
def scroll_end(self, *, animate: bool = True) -> bool:
|
||||
"""Scroll to the end of the container.
|
||||
|
||||
Args:
|
||||
animate (bool, optional): Animate scroll. Defaults to True.
|
||||
|
||||
Returns:
|
||||
bool: True if any scrolling was done.
|
||||
|
||||
"""
|
||||
return self.scroll_to(0, self.max_scroll_y, animate=animate, duration=1)
|
||||
|
||||
def scroll_left(self, *, animate: bool = True) -> bool:
|
||||
"""Scroll one cell left.
|
||||
|
||||
Args:
|
||||
animate (bool, optional): Animate scroll. Defaults to True.
|
||||
|
||||
Returns:
|
||||
bool: True if any scrolling was done.
|
||||
|
||||
"""
|
||||
return self.scroll_to(x=self.scroll_target_x - 1, animate=animate)
|
||||
|
||||
def scroll_right(self, *, animate: bool = True) -> bool:
|
||||
"""Scroll on cell right.
|
||||
|
||||
Args:
|
||||
animate (bool, optional): Animate scroll. Defaults to True.
|
||||
|
||||
Returns:
|
||||
bool: True if any scrolling was done.
|
||||
|
||||
"""
|
||||
return self.scroll_to(x=self.scroll_target_x + 1, animate=animate)
|
||||
|
||||
def scroll_up(self, *, animate: bool = True) -> bool:
|
||||
def scroll_down(self, *, animate: bool = True) -> bool:
|
||||
"""Scroll one line down.
|
||||
|
||||
Args:
|
||||
animate (bool, optional): Animate scroll. Defaults to True.
|
||||
|
||||
Returns:
|
||||
bool: True if any scrolling was done.
|
||||
|
||||
"""
|
||||
return self.scroll_to(y=self.scroll_target_y + 1, animate=animate)
|
||||
|
||||
def scroll_down(self, *, animate: bool = True) -> bool:
|
||||
def scroll_up(self, *, animate: bool = True) -> bool:
|
||||
"""Scroll one line up.
|
||||
|
||||
Args:
|
||||
animate (bool, optional): Animate scroll. Defaults to True.
|
||||
|
||||
Returns:
|
||||
bool: True if any scrolling was done.
|
||||
|
||||
"""
|
||||
return self.scroll_to(y=self.scroll_target_y - 1, animate=animate)
|
||||
|
||||
def scroll_page_up(self, *, animate: bool = True) -> bool:
|
||||
"""Scroll one page up.
|
||||
|
||||
Args:
|
||||
animate (bool, optional): Animate scroll. Defaults to True.
|
||||
|
||||
Returns:
|
||||
bool: True if any scrolling was done.
|
||||
|
||||
"""
|
||||
return self.scroll_to(
|
||||
y=self.scroll_target_y - self.container_size.height, animate=animate
|
||||
)
|
||||
|
||||
def scroll_page_down(self, *, animate: bool = True) -> bool:
|
||||
"""Scroll one page down.
|
||||
|
||||
Args:
|
||||
animate (bool, optional): Animate scroll. Defaults to True.
|
||||
|
||||
Returns:
|
||||
bool: True if any scrolling was done.
|
||||
|
||||
"""
|
||||
return self.scroll_to(
|
||||
y=self.scroll_target_y + self.container_size.height, animate=animate
|
||||
)
|
||||
|
||||
def scroll_page_left(self, *, animate: bool = True) -> bool:
|
||||
"""Scroll one page left.
|
||||
|
||||
Args:
|
||||
animate (bool, optional): Animate scroll. Defaults to True.
|
||||
|
||||
Returns:
|
||||
bool: True if any scrolling was done.
|
||||
|
||||
"""
|
||||
return self.scroll_to(
|
||||
x=self.scroll_target_x - self.container_size.width,
|
||||
animate=animate,
|
||||
@@ -857,6 +1009,15 @@ class Widget(DOMNode):
|
||||
)
|
||||
|
||||
def scroll_page_right(self, *, animate: bool = True) -> bool:
|
||||
"""Scroll one page right.
|
||||
|
||||
Args:
|
||||
animate (bool, optional): Animate scroll. Defaults to True.
|
||||
|
||||
Returns:
|
||||
bool: True if any scrolling was done.
|
||||
|
||||
"""
|
||||
return self.scroll_to(
|
||||
x=self.scroll_target_x + self.container_size.width,
|
||||
animate=animate,
|
||||
@@ -1031,7 +1192,12 @@ class Widget(DOMNode):
|
||||
yield self.horizontal_scrollbar, scrollbar_region
|
||||
|
||||
def get_pseudo_classes(self) -> Iterable[str]:
|
||||
"""Pseudo classes for a widget"""
|
||||
"""Pseudo classes for a widget.
|
||||
|
||||
Returns:
|
||||
Iterable[str]: Names of the pseudo classes.
|
||||
|
||||
"""
|
||||
if self.mouse_over:
|
||||
yield "hover"
|
||||
if self.has_focus:
|
||||
@@ -1039,9 +1205,6 @@ class Widget(DOMNode):
|
||||
if self.descendant_has_focus:
|
||||
yield "focus-within"
|
||||
|
||||
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
|
||||
watch(self, attribute_name, callback)
|
||||
|
||||
def post_render(self, renderable: RenderableType) -> RenderableType:
|
||||
"""Applies style attributes to the default renderable.
|
||||
|
||||
@@ -1098,11 +1261,11 @@ class Widget(DOMNode):
|
||||
width, height = self.size
|
||||
renderable = self.render()
|
||||
renderable = self.post_render(renderable)
|
||||
options = self.console.options.update_dimensions(width, height).update(
|
||||
options = self._console.options.update_dimensions(width, height).update(
|
||||
highlight=False
|
||||
)
|
||||
|
||||
segments = self.console.render(renderable, options)
|
||||
segments = self._console.render(renderable, options)
|
||||
lines = list(
|
||||
islice(
|
||||
Segment.split_and_crop_lines(
|
||||
@@ -1155,6 +1318,15 @@ class Widget(DOMNode):
|
||||
return lines
|
||||
|
||||
def get_style_at(self, x: int, y: int) -> Style:
|
||||
"""Get the Rich style at a given screen offset.
|
||||
|
||||
Args:
|
||||
x (int): X coordinate relative to the screen.
|
||||
y (int): Y coordinate relative to the screen.
|
||||
|
||||
Returns:
|
||||
Style: A rich Style object.
|
||||
"""
|
||||
offset_x, offset_y = self.screen.get_offset(self)
|
||||
return self.screen.get_style_at(x + offset_x, y + offset_y)
|
||||
|
||||
@@ -1170,7 +1342,16 @@ class Widget(DOMNode):
|
||||
This method sets an internal flag to perform a refresh, which will be done on the
|
||||
next idle event. Only one refresh will be done even if this method is called multiple times.
|
||||
|
||||
By default this method will cause the content of the widget to refresh, but not change its size. You can also
|
||||
set `layout=True` to perform a layout.
|
||||
|
||||
!!! warning
|
||||
|
||||
It is rarely necessary to call this method explicitly. Updating styles or reactive attributes will
|
||||
do this automatically.
|
||||
|
||||
Args:
|
||||
*regions (Region, optional): Additional screen regions to mark as dirty.
|
||||
repaint (bool, optional): Repaint the widget (will call render() again). Defaults to True.
|
||||
layout (bool, optional): Also layout widgets in the view. Defaults to False.
|
||||
"""
|
||||
@@ -1257,7 +1438,7 @@ class Widget(DOMNode):
|
||||
self.app.capture_mouse(None)
|
||||
|
||||
async def broker_event(self, event_name: str, event: events.Event) -> bool:
|
||||
return await self.app.broker_event(event_name, event, default_namespace=self)
|
||||
return await self.app._broker_event(event_name, event, default_namespace=self)
|
||||
|
||||
async def _on_mouse_down(self, event: events.MouseUp) -> None:
|
||||
await self.broker_event("mouse.down", event)
|
||||
@@ -1322,27 +1503,27 @@ class Widget(DOMNode):
|
||||
event.stop()
|
||||
|
||||
def _on_scroll_to(self, message: ScrollTo) -> None:
|
||||
if self.is_scrollable:
|
||||
if self._allow_scroll:
|
||||
self.scroll_to(message.x, message.y, animate=message.animate, duration=0.1)
|
||||
message.stop()
|
||||
|
||||
def _on_scroll_up(self, event: ScrollUp) -> None:
|
||||
if self.is_scrollable:
|
||||
if self.allow_vertical_scroll:
|
||||
self.scroll_page_up()
|
||||
event.stop()
|
||||
|
||||
def _on_scroll_down(self, event: ScrollDown) -> None:
|
||||
if self.is_scrollable:
|
||||
if self.allow_vertical_scroll:
|
||||
self.scroll_page_down()
|
||||
event.stop()
|
||||
|
||||
def _on_scroll_left(self, event: ScrollLeft) -> None:
|
||||
if self.is_scrollable:
|
||||
if self.allow_horizontal_scroll:
|
||||
self.scroll_page_left()
|
||||
event.stop()
|
||||
|
||||
def _on_scroll_right(self, event: ScrollRight) -> None:
|
||||
if self.is_scrollable:
|
||||
if self.allow_horizontal_scroll:
|
||||
self.scroll_page_right()
|
||||
event.stop()
|
||||
|
||||
@@ -1351,49 +1532,49 @@ class Widget(DOMNode):
|
||||
self.app._reset_focus(self)
|
||||
|
||||
def key_home(self) -> bool:
|
||||
if self.is_scrollable:
|
||||
if self._allow_scroll:
|
||||
self.scroll_home()
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_end(self) -> bool:
|
||||
if self.is_scrollable:
|
||||
if self._allow_scroll:
|
||||
self.scroll_end()
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_left(self) -> bool:
|
||||
if self.is_scrollable:
|
||||
if self.allow_horizontal_scroll:
|
||||
self.scroll_left()
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_right(self) -> bool:
|
||||
if self.is_scrollable:
|
||||
if self.allow_horizontal_scroll:
|
||||
self.scroll_right()
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_down(self) -> bool:
|
||||
if self.is_scrollable:
|
||||
self.scroll_up()
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_up(self) -> bool:
|
||||
if self.is_scrollable:
|
||||
if self.allow_vertical_scroll:
|
||||
self.scroll_down()
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_up(self) -> bool:
|
||||
if self.allow_vertical_scroll:
|
||||
self.scroll_up()
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_pagedown(self) -> bool:
|
||||
if self.is_scrollable:
|
||||
if self.allow_vertical_scroll:
|
||||
self.scroll_page_down()
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_pageup(self) -> bool:
|
||||
if self.is_scrollable:
|
||||
if self.allow_vertical_scroll:
|
||||
self.scroll_page_up()
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -9,7 +9,7 @@ from rich.text import Text
|
||||
|
||||
from textual import events, _clock
|
||||
from textual._text_backend import TextEditorBackend
|
||||
from textual._timer import Timer
|
||||
from textual.timer import Timer
|
||||
from textual._types import MessageTarget
|
||||
from textual.app import ComposeResult
|
||||
from textual.geometry import Size, clamp
|
||||
|
||||
@@ -268,7 +268,7 @@ async def test_scrollbar_gutter(
|
||||
|
||||
text_widget = TextWidget()
|
||||
text_widget.styles.height = "auto"
|
||||
container.add_child(text_widget)
|
||||
container._add_child(text_widget)
|
||||
|
||||
class MyTestApp(AppTest):
|
||||
def compose(self) -> ComposeResult:
|
||||
|
||||
@@ -21,7 +21,7 @@ def test_nodes_take_display_property_into_account_when_they_display_their_childr
|
||||
|
||||
screen = Screen()
|
||||
screen.styles.layout = layout
|
||||
screen.add_child(widget)
|
||||
screen._add_child(widget)
|
||||
|
||||
displayed_children = screen.displayed_children
|
||||
assert isinstance(displayed_children, list)
|
||||
|
||||
@@ -32,10 +32,10 @@ def parent():
|
||||
child1 = DOMNode(id="child1")
|
||||
child2 = DOMNode(id="child2")
|
||||
grandchild1 = DOMNode(id="grandchild1")
|
||||
child1.add_child(grandchild1)
|
||||
child1._add_child(grandchild1)
|
||||
|
||||
parent.add_child(child1)
|
||||
parent.add_child(child2)
|
||||
parent._add_child(child1)
|
||||
parent._add_child(child2)
|
||||
|
||||
yield parent
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ async def test_focus_chain():
|
||||
# Check empty focus chain
|
||||
assert not app.focus_chain
|
||||
|
||||
app.screen.add_children(
|
||||
app.screen._add_children(
|
||||
Focusable(id="foo"),
|
||||
NonFocusable(id="bar"),
|
||||
Focusable(Focusable(id="Paul"), id="container1"),
|
||||
@@ -37,7 +37,7 @@ async def test_focus_next_and_previous():
|
||||
app = App()
|
||||
app._set_active()
|
||||
app.push_screen(Screen())
|
||||
app.screen.add_children(
|
||||
app.screen._add_children(
|
||||
Focusable(id="foo"),
|
||||
NonFocusable(id="bar"),
|
||||
Focusable(Focusable(id="Paul"), id="container1"),
|
||||
|
||||
@@ -11,8 +11,8 @@ def test_query():
|
||||
app = App()
|
||||
main_view = View(id="main")
|
||||
help_view = View(id="help")
|
||||
app.add_child(main_view)
|
||||
app.add_child(help_view)
|
||||
app._add_child(main_view)
|
||||
app._add_child(help_view)
|
||||
|
||||
widget1 = Widget(id="widget1")
|
||||
widget2 = Widget(id="widget2")
|
||||
@@ -22,21 +22,21 @@ def test_query():
|
||||
helpbar = Widget(id="helpbar")
|
||||
helpbar.add_class("float")
|
||||
|
||||
main_view.add_child(widget1)
|
||||
main_view.add_child(widget2)
|
||||
main_view.add_child(sidebar)
|
||||
main_view._add_child(widget1)
|
||||
main_view._add_child(widget2)
|
||||
main_view._add_child(sidebar)
|
||||
|
||||
sub_view = View(id="sub")
|
||||
sub_view.add_class("-subview")
|
||||
main_view.add_child(sub_view)
|
||||
main_view._add_child(sub_view)
|
||||
|
||||
tooltip = Widget(id="tooltip")
|
||||
tooltip.add_class("float", "transient")
|
||||
sub_view.add_child(tooltip)
|
||||
sub_view._add_child(tooltip)
|
||||
|
||||
help = Widget(id="markdown")
|
||||
help_view.add_child(help)
|
||||
help_view.add_child(helpbar)
|
||||
help_view._add_child(help)
|
||||
help_view._add_child(helpbar)
|
||||
|
||||
# repeat tests to account for caching
|
||||
for repeat in range(3):
|
||||
|
||||
@@ -70,7 +70,7 @@ class AppTest(App):
|
||||
waiting_duration_after_yield: float = 0,
|
||||
) -> AsyncContextManager[ClockMock]:
|
||||
async def run_app() -> None:
|
||||
await self.process_messages()
|
||||
await self._process_messages()
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def get_running_state_context_manager():
|
||||
|
||||
Reference in New Issue
Block a user