mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
stopwatch04
This commit is contained in:
@@ -4,27 +4,15 @@ Stopwatch {
|
||||
height: 5;
|
||||
min-width: 50;
|
||||
margin: 1;
|
||||
padding: 1 1;
|
||||
transition: background 300ms linear;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
Stopwatch.started {
|
||||
text-style: bold;
|
||||
background: $success;
|
||||
color: $text-success;
|
||||
}
|
||||
|
||||
|
||||
TimeDisplay {
|
||||
content-align: center middle;
|
||||
opacity: 60%;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
Stopwatch.started TimeDisplay {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
Button {
|
||||
width: 16;
|
||||
}
|
||||
@@ -42,6 +30,16 @@ Button {
|
||||
dock: right;
|
||||
}
|
||||
|
||||
Stopwatch.started {
|
||||
text-style: bold;
|
||||
background: $success;
|
||||
color: $text-success;
|
||||
}
|
||||
|
||||
Stopwatch.started TimeDisplay {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
Stopwatch.started #start {
|
||||
display: none
|
||||
}
|
||||
|
||||
@@ -25,23 +25,6 @@ class Stopwatch(Static):
|
||||
total = Reactive(0.0)
|
||||
started = Reactive(False)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Called when widget is first added."""
|
||||
self.update_timer = self.set_interval(1 / 30, self.update_elapsed, pause=True)
|
||||
|
||||
def update_elapsed(self) -> None:
|
||||
"""Updates elapsed time."""
|
||||
self.query_one(TimeDisplay).time = (
|
||||
self.total + monotonic() - self.start_time if self.started else self.total
|
||||
)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Composes the timer widget."""
|
||||
yield Button("Start", id="start", variant="success")
|
||||
yield Button("Stop", id="stop", variant="error")
|
||||
yield TimeDisplay()
|
||||
yield Button("Reset", id="reset")
|
||||
|
||||
def watch_started(self, started: bool) -> None:
|
||||
"""Called when the 'started' attribute changes."""
|
||||
if started:
|
||||
@@ -63,6 +46,23 @@ class Stopwatch(Static):
|
||||
self.total = 0.0
|
||||
self.update_elapsed()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Called when widget is first added."""
|
||||
self.update_timer = self.set_interval(1 / 30, self.update_elapsed, pause=True)
|
||||
|
||||
def update_elapsed(self) -> None:
|
||||
"""Updates elapsed time."""
|
||||
self.query_one(TimeDisplay).time = (
|
||||
self.total + monotonic() - self.start_time if self.started else self.total
|
||||
)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Composes the timer widget."""
|
||||
yield Button("Start", id="start", variant="success")
|
||||
yield Button("Stop", id="stop", variant="error")
|
||||
yield Button("Reset", id="reset")
|
||||
yield TimeDisplay()
|
||||
|
||||
|
||||
class StopwatchApp(App):
|
||||
"""Manage the timers."""
|
||||
@@ -87,7 +87,7 @@ class StopwatchApp(App):
|
||||
|
||||
def action_remove_timer(self) -> None:
|
||||
"""Called to remove a timer."""
|
||||
timers = self.query("#timers TimerWidget")
|
||||
timers = self.query("#timers Stopwatch")
|
||||
if timers:
|
||||
timers.last().remove()
|
||||
|
||||
|
||||
54
docs/examples/introduction/stopwatch04.css
Normal file
54
docs/examples/introduction/stopwatch04.css
Normal file
@@ -0,0 +1,54 @@
|
||||
Stopwatch {
|
||||
layout: horizontal;
|
||||
background: $panel-darken-1;
|
||||
height: 5;
|
||||
min-width: 50;
|
||||
margin: 1;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
TimeDisplay {
|
||||
content-align: center middle;
|
||||
opacity: 60%;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
Button {
|
||||
width: 16;
|
||||
}
|
||||
|
||||
#start {
|
||||
dock: left;
|
||||
}
|
||||
|
||||
#stop {
|
||||
dock: left;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#reset {
|
||||
dock: right;
|
||||
}
|
||||
|
||||
Stopwatch.started {
|
||||
text-style: bold;
|
||||
background: $success;
|
||||
color: $text-success;
|
||||
}
|
||||
|
||||
Stopwatch.started TimeDisplay {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
Stopwatch.started #start {
|
||||
display: none
|
||||
}
|
||||
|
||||
Stopwatch.started #stop {
|
||||
display: block
|
||||
}
|
||||
|
||||
Stopwatch.started #reset {
|
||||
visibility: hidden
|
||||
}
|
||||
|
||||
47
docs/examples/introduction/stopwatch04.py
Normal file
47
docs/examples/introduction/stopwatch04.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.layout import Container
|
||||
from textual.reactive import Reactive
|
||||
from textual.widgets import Button, Header, Footer, Static
|
||||
|
||||
|
||||
class TimeDisplay(Static):
|
||||
pass
|
||||
|
||||
|
||||
class Stopwatch(Static):
|
||||
started = Reactive(False)
|
||||
|
||||
def watch_started(self, started: bool) -> None:
|
||||
if started:
|
||||
self.add_class("started")
|
||||
else:
|
||||
self.remove_class("started")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Called when a button is pressed."""
|
||||
button_id = event.button.id
|
||||
self.started = button_id == "start"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Button("Start", id="start", variant="success")
|
||||
yield Button("Stop", id="stop", variant="error")
|
||||
yield Button("Reset", id="reset")
|
||||
yield TimeDisplay("00:00:00.00")
|
||||
|
||||
|
||||
class StopwatchApp(App):
|
||||
def compose(self):
|
||||
yield Header()
|
||||
yield Footer()
|
||||
yield Container(Stopwatch(), Stopwatch(), Stopwatch())
|
||||
|
||||
def on_load(self):
|
||||
self.bind("d", "toggle_dark", description="Dark mode")
|
||||
|
||||
def action_toggle_dark(self):
|
||||
self.dark = not self.dark
|
||||
|
||||
|
||||
app = StopwatchApp(css_path="stopwatch04.css")
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -46,7 +46,7 @@ Hit ++ctrl+c++ to exit the app and return to the command prompt.
|
||||
|
||||
### Looking at the code
|
||||
|
||||
Let's example stopwatch01.py in more detail.
|
||||
Let's examine stopwatch01.py in more detail.
|
||||
|
||||
```python title="stopwatch01.py" hl_lines="1 2"
|
||||
--8<-- "docs/examples/introduction/stopwatch01.py"
|
||||
@@ -70,7 +70,7 @@ There are three methods in our stopwatch app currently.
|
||||
|
||||
- **`on_load()`** is an _event handler_ method. Event handlers are called by Textual in response to external events like keys and mouse movements, and internal events needed to manage your application. Event handler methods begin with `on_` followed by the name of the event (in lower case). Hence, `on_load` it is called in response to the Load event which is sent just after the app starts. We're using this event to call `App.bind()` which connects a key to an _action_.
|
||||
|
||||
- **`action_toggle_dark()`** defines an _action_ method. Actions are methods beginning with `action_` followed by the name of the action. The call to `bind()` in `on_load()` binds this action to the ++d++ key. The body of this method flips the state of the `dark` boolean to toggle dark mode.
|
||||
- **`action_toggle_dark()`** defines an _action_ method. Actions are methods beginning with `action_` followed by the name of the action. The call to `bind()` in `on_load()` binds this the ++d++ key to this action. The body of this method flips the state of the `dark` boolean to toggle dark mode.
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -85,7 +85,7 @@ The last lines in "stopwatch01.py" may be familiar to you. We create an instance
|
||||
|
||||
## Creating a custom widget
|
||||
|
||||
The header and footer were builtin widgets. For our stopwatch application we will need to build a custom widget for stopwatches.
|
||||
The header and footer were builtin widgets. We will to build a custom widget for the stopwatches in our application.
|
||||
|
||||
Let's sketch out what we are trying to achieve here:
|
||||
|
||||
@@ -96,7 +96,7 @@ Let's sketch out what we are trying to achieve here:
|
||||
|
||||
An individual stopwatch consists of several parts, which themselves can be widgets.
|
||||
|
||||
Out stopwatch widgets is going to need the following widgets:
|
||||
The Stopwatch widget consists of the be built with the following _child_ widgets:
|
||||
|
||||
- A "start" button
|
||||
- A "stop" button
|
||||
@@ -113,22 +113,22 @@ Let's add those to our app:
|
||||
|
||||
### New widgets
|
||||
|
||||
We've imported two new widgets in this code: Button, which creates a clickable button, and Static which is a base class for a simple control. We've also imported `Container` from `textual.layout`. As the name suggests, `Container` is a Widget which contains other widgets. We will use this container to form a scrolling list of stopwatches.
|
||||
We've imported two new widgets in this code: `Button`, which creates a clickable button, and `Static` which is a base class for a simple control. We've also imported `Container` from `textual.layout`. As the name suggests, `Container` is a Widget which contains other widgets. We will use this container to create a scrolling list of stopwatches.
|
||||
|
||||
We're extending Static as a foundation for our `TimeDisplay` widget. There are no methods on this class yet.
|
||||
|
||||
The Stopwatch also extends Static to define a new widget. This class has a `compose()` method which yields its _child_ widgets, consisting of of three `Button` objects and a single `TimeDisplay`. These are all we need to build a stopwatch like the sketch.
|
||||
The Stopwatch class also extends Static to define a new widget. This class has a `compose()` method which yields its child widgets, consisting of of three `Button` objects and a single `TimeDisplay`. These are all we need to build a stopwatch as in the sketch.
|
||||
|
||||
The Button constructor takes a label to be displayed to the user ("Start", "Stop", or "Reset") so they know what will happen when they click on it. There are two additional parameters to the Button constructor we are using:
|
||||
The Button constructor takes a label to be displayed in the button ("Start", "Stop", or "Reset"). There are two additional parameters to the Button constructor we are using:
|
||||
|
||||
- **`id`** is an identifier so we can tell the buttons apart in code. We can also use this to style the buttons. More on that later.
|
||||
- **`variant`** is a string which selects a default style. The "success" variant makes the button green, and the "error" variant makes it red.
|
||||
- **`id`** is an identifier we can use to tell the buttons apart in code and apply styles. More on that later.
|
||||
- **`variant`** is a string which selects a default style. The "success" variant makes the button green, and the "error" variant makes it red.
|
||||
|
||||
### Composing the widgets
|
||||
|
||||
To see our widgets with we need to yield them from the app's `compose()` method:
|
||||
To see our widgets with we first need to yield them from the app's `compose()` method:
|
||||
|
||||
This new line in `Stopwatch.compose()` adds a single `Container` object which will create a scrolling list. The constructor for `Container` takes its _child_ widgets as positional arguments, to which we pass three instances of the `Stopwatch` we just built.
|
||||
The new line in `Stopwatch.compose()` yields a single `Container` object which will create a scrolling list. When classes contain other widgets (like `Container`) they will typically accept their child widgets as positional arguments. We want to start the app with three stopwatches, so we construct three `Stopwatch` instances as child widgets of the container.
|
||||
|
||||
|
||||
### The unstyled app
|
||||
@@ -142,9 +142,9 @@ The elements of the stopwatch application are there. The buttons are clickable a
|
||||
|
||||
## Writing Textual CSS
|
||||
|
||||
Every widget has a `styles` object which contains information regarding how that widget will look. Setting any of the attributes on that styles object will change how Textual renders the widget.
|
||||
Every widget has a `styles` object which contains information regarding how that widget will look. Setting any of the attributes on that styles object will change how Textual displays the widget.
|
||||
|
||||
Here's how you might change the widget to use white text on a blue background:
|
||||
Here's how you might set white text and a blue background for a widget:
|
||||
|
||||
```python
|
||||
self.styles.background = "blue"
|
||||
@@ -153,34 +153,36 @@ self.styles.color = "white"
|
||||
|
||||
While its possible to set all styles for an app this way, Textual prefers to use CSS.
|
||||
|
||||
CSS files are data files loaded by your app which contain information about what styles to apply to your widgets.
|
||||
CSS files are data files loaded by your app which contain information about styles to apply to your widgets.
|
||||
|
||||
!!! note
|
||||
|
||||
Don't worry if you have never worked with CSS before. The dialect of CSS we use is greatly simplified over web based CSS and easy to learn!
|
||||
|
||||
To load a CSS file you can set the `css_path` attribute of your app.
|
||||
Let's add a CSS file to our application.
|
||||
|
||||
```python title="stopwatch03.py" hl_lines="31"
|
||||
--8<-- "docs/examples/introduction/stopwatch03.py"
|
||||
```
|
||||
|
||||
This will tell Textual to load the following file when it starts the app:
|
||||
Adding the `css_path` attribute to the app constructor tells textual to load the following file when it starts the app:
|
||||
|
||||
```css title="stopwatch03.css"
|
||||
```sass title="stopwatch03.css"
|
||||
--8<-- "docs/examples/introduction/stopwatch03.css"
|
||||
```
|
||||
|
||||
The only change was setting the css path. Our app will now look very different:
|
||||
If we run the app now, it will look *very* different.
|
||||
|
||||
```{.textual path="docs/examples/introduction/stopwatch03.py" title="stopwatch03.py"}
|
||||
```
|
||||
|
||||
This app looks much more like our sketch. Textual has read style information from `stopwatch03.css` and applied it to the widgets. In effect setting attributes on `widget.styles`.
|
||||
This app looks much more like our sketch. Textual has read style information from `stopwatch03.css` and applied it to the widgets.
|
||||
|
||||
### CSS basics
|
||||
|
||||
CSS files contain a number of _declaration blocks_. Here's the first such block from `stopwatch03.css` again:
|
||||
|
||||
```css
|
||||
```sass
|
||||
Stopwatch {
|
||||
layout: horizontal;
|
||||
background: $panel-darken-1;
|
||||
@@ -198,7 +200,7 @@ Here's how the Stopwatch block in the CSS impacts our `Stopwatch` widget:
|
||||
--8<-- "docs/images/stopwatch_widgets.excalidraw.svg"
|
||||
</div>
|
||||
|
||||
- `layout: horizontal` aligns child widgets from left to right rather than top to bottom.
|
||||
- `layout: horizontal` aligns child widgets horizontally from left to right.
|
||||
- `background: $panel-darken-1` sets the background color to `$panel-darken-1`. The `$` prefix picks a pre-defined color from the builtin theme. There are other ways to specify colors such as `"blue"` or `rgb(20,46,210)`.
|
||||
- `height: 5` sets the height of our widget to 5 lines of text.
|
||||
- `padding: 1` sets a padding of 1 cell around the child widgets.
|
||||
@@ -207,7 +209,7 @@ Here's how the Stopwatch block in the CSS impacts our `Stopwatch` widget:
|
||||
|
||||
Here's the rest of `stopwatch03.css` which contains further declaration blocks:
|
||||
|
||||
```css
|
||||
```sass
|
||||
TimeDisplay {
|
||||
content-align: center middle;
|
||||
opacity: 60%;
|
||||
@@ -236,8 +238,18 @@ The `TimeDisplay` block aligns text to the center (`content-align`), fades it sl
|
||||
|
||||
The `Button` block sets the width (`width`) of buttons to 16 cells (character widths).
|
||||
|
||||
The last 3 blocks have a slightly different format. When the declaration begins with a `#` then the styles will be applied to any widget with a matching "id" attribute. We've set an id attribute on the Button widgets we yielded in compose. For instance the first button has `id="start"` which matches `#start` in the CSS.
|
||||
The last 3 blocks have a slightly different format. When the declaration begins with a `#` then the styles will be applied widgets with a matching "id" attribute. We've set an ID attribute on the Button widgets we yielded in compose. For instance the first button has `id="start"` which matches `#start` in the CSS.
|
||||
|
||||
The buttons have a `dock` style which aligns the widget to a given edge. The start and stop buttons are docked to the left edge, while the reset button is docked to the right edge.
|
||||
|
||||
You may have noticed that the stop button (`#stop` in the CSS) has `display: none;`. This tells Textual to not show the button. We do this because there is no point in displaying the stop button when the timer is *not* running. Similarly we don't want to show the start button when the timer is running. We will cover how to manage such dynamic user interfaces in the next section.
|
||||
You may have noticed that the stop button (`#stop` in the CSS) has `display: none;`. This tells Textual to not show the button. We do this because we don't want to dsplay the stop button when the timer is *not* running. Similarly we don't want to show the start button when the timer is running. We will cover how to manage such dynamic user interfaces in the next section.
|
||||
|
||||
### Dynamic CSS
|
||||
|
||||
We want our Stopwatch widget to have two states. An _unstarted_ state with a Start and Reset button, and a _started_ state with a Stop button.
|
||||
|
||||
There are other differences between the two states. It would be nice if the stopwatch turns green when it is started. And we could make the time text bold, so it is clear it is running. It's possible to do this in code, but
|
||||
|
||||
|
||||
```{.textual path="docs/examples/introduction/stopwatch04.py" title="stopwatch04.py" press="tab,enter"}
|
||||
```
|
||||
|
||||
27
sandbox/will/order.py
Normal file
27
sandbox/will/order.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import asyncio
|
||||
from textual.app import App
|
||||
from textual import events
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class OrderWidget(Widget, can_focus=True):
|
||||
def on_key(self, event) -> None:
|
||||
self.log("PRESS", event.key)
|
||||
|
||||
|
||||
class OrderApp(App):
|
||||
def compose(self):
|
||||
yield OrderWidget()
|
||||
|
||||
async def on_mount(self):
|
||||
async def send_keys():
|
||||
self.query_one(OrderWidget).focus()
|
||||
chars = ["tab", "enter", "h", "e", "l", "l", "o"]
|
||||
for char in chars:
|
||||
self.log("SENDING", char)
|
||||
await self.post_message(events.Key(self, key=char))
|
||||
|
||||
self.set_timer(1, lambda: asyncio.create_task(send_keys()))
|
||||
|
||||
|
||||
app = OrderApp()
|
||||
@@ -6,6 +6,7 @@ __all__ = ["log", "panic"]
|
||||
|
||||
|
||||
def log(*args: object, verbosity: int = 0, **kwargs) -> None:
|
||||
# TODO: There may be an early-out here for when there is no endpoint for logs
|
||||
from ._context import active_app
|
||||
|
||||
app = active_app.get()
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import os
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import cast, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from textual.app import App
|
||||
|
||||
# This module defines our "Custom Fences", powered by SuperFences
|
||||
# @link https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#custom-fences
|
||||
def format_svg(source, language, css_class, options, md, attrs, **kwargs):
|
||||
def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str:
|
||||
"""A superfences formatter to insert a SVG screenshot."""
|
||||
|
||||
path = attrs.get("path")
|
||||
press = attrs.get("press", "").split(",")
|
||||
_press = attrs.get("press", None)
|
||||
press = _press.split(",") if _press else []
|
||||
title = attrs.get("title")
|
||||
|
||||
os.environ["TEXTUAL"] = "headless"
|
||||
@@ -27,20 +33,19 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs):
|
||||
os.chdir(examples_path)
|
||||
with open(filename, "rt") as python_code:
|
||||
source = python_code.read()
|
||||
app_vars = {}
|
||||
app_vars: dict[str, object] = {}
|
||||
exec(source, app_vars)
|
||||
|
||||
app = app_vars["app"]
|
||||
app: App = cast("App", app_vars["app"])
|
||||
app.run(press=press or None)
|
||||
svg = app._screenshot
|
||||
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
else:
|
||||
app_vars = {}
|
||||
exec(source, app_vars)
|
||||
app = app_vars["app"]
|
||||
app.run()
|
||||
app = cast(App, app_vars["app"])
|
||||
app.run(press=press or None)
|
||||
svg = app._screenshot
|
||||
|
||||
assert svg is not None
|
||||
return svg
|
||||
|
||||
@@ -232,6 +232,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
if ((watch_css or self.debug) and self.css_path)
|
||||
else None
|
||||
)
|
||||
self._screenshot: str | None = None
|
||||
|
||||
def __init_subclass__(
|
||||
cls, css_path: str | None = None, inherit_css: bool = True
|
||||
@@ -449,6 +450,8 @@ class App(Generic[ReturnType], DOMNode):
|
||||
"""
|
||||
if verbosity > self.log_verbosity:
|
||||
return
|
||||
if self._log_console is None and not self.devtools.is_connected:
|
||||
return
|
||||
|
||||
if self.devtools.is_connected and not _textual_calling_frame:
|
||||
_textual_calling_frame = inspect.stack()[1]
|
||||
@@ -572,13 +575,15 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self.set_timer(quit_after, self.shutdown)
|
||||
if press is not None:
|
||||
|
||||
async def press_keys():
|
||||
async def press_keys(app: App):
|
||||
assert press
|
||||
await asyncio.sleep(0.05)
|
||||
for key in press:
|
||||
print(f"press {key!r}")
|
||||
await app.post_message(events.Key(self, key))
|
||||
await asyncio.sleep(0.01)
|
||||
await self.press(key)
|
||||
|
||||
self.call_later(press_keys)
|
||||
self.call_later(lambda: asyncio.create_task(press_keys(self)))
|
||||
|
||||
await self.process_messages()
|
||||
|
||||
|
||||
@@ -684,7 +684,10 @@ class DOMNode(MessagePump):
|
||||
*class_names (str): CSS class names to add.
|
||||
|
||||
"""
|
||||
old_classes = self._classes.copy()
|
||||
self._classes.update(class_names)
|
||||
if old_classes == self._classes:
|
||||
return
|
||||
try:
|
||||
self.app.stylesheet.update(self.app, animate=True)
|
||||
except NoActiveAppError:
|
||||
@@ -697,7 +700,10 @@ class DOMNode(MessagePump):
|
||||
*class_names (str): CSS class names to remove.
|
||||
|
||||
"""
|
||||
old_classes = self._classes.copy()
|
||||
self._classes.difference_update(class_names)
|
||||
if old_classes == self._classes:
|
||||
return
|
||||
try:
|
||||
self.app.stylesheet.update(self.app, animate=True)
|
||||
except NoActiveAppError:
|
||||
@@ -710,7 +716,10 @@ class DOMNode(MessagePump):
|
||||
*class_names (str): CSS class names to toggle.
|
||||
|
||||
"""
|
||||
old_classes = self._classes.copy()
|
||||
self._classes.symmetric_difference_update(class_names)
|
||||
if old_classes == self._classes:
|
||||
return
|
||||
try:
|
||||
self.app.stylesheet.update(self.app, animate=True)
|
||||
except NoActiveAppError:
|
||||
|
||||
@@ -16,7 +16,7 @@ MouseEventT = TypeVar("MouseEventT", bound="MouseEvent")
|
||||
if TYPE_CHECKING:
|
||||
from ._timer import Timer as TimerClass
|
||||
from ._timer import TimerCallback
|
||||
from .widget import WIdget
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from functools import total_ordering
|
||||
import inspect
|
||||
from asyncio import CancelledError
|
||||
from asyncio import PriorityQueue, QueueEmpty, Task
|
||||
from functools import partial, total_ordering
|
||||
from typing import TYPE_CHECKING, Awaitable, Iterable, Callable
|
||||
from asyncio import CancelledError, Queue, QueueEmpty, Task
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable
|
||||
from weakref import WeakSet
|
||||
|
||||
from . import events
|
||||
from . import log
|
||||
from .case import camel_to_snake
|
||||
from ._timer import Timer, TimerCallback
|
||||
from . import events, log, messages
|
||||
from ._callback import invoke
|
||||
from ._context import active_app, NoActiveAppError
|
||||
from .message import Message
|
||||
from ._context import NoActiveAppError, active_app
|
||||
from ._timer import Timer, TimerCallback
|
||||
from .case import camel_to_snake
|
||||
from .events import Event
|
||||
from . import messages
|
||||
from .message import Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .app import App
|
||||
@@ -35,25 +32,6 @@ class MessagePumpClosed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@total_ordering
|
||||
class MessagePriority:
|
||||
"""Wraps a messages with a priority, and provides equality."""
|
||||
|
||||
__slots__ = ["message", "priority"]
|
||||
|
||||
def __init__(self, message: Message | None = None, priority: int = 0):
|
||||
self.message = message
|
||||
self.priority = priority
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
assert isinstance(other, MessagePriority)
|
||||
return self.priority == other.priority
|
||||
|
||||
def __gt__(self, other: object) -> bool:
|
||||
assert isinstance(other, MessagePriority)
|
||||
return self.priority > other.priority
|
||||
|
||||
|
||||
class MessagePumpMeta(type):
|
||||
"""Metaclass for message pump. This exists to populate a Message inner class of a Widget with the
|
||||
parent classes' name.
|
||||
@@ -79,7 +57,7 @@ class MessagePumpMeta(type):
|
||||
|
||||
class MessagePump(metaclass=MessagePumpMeta):
|
||||
def __init__(self, parent: MessagePump | None = None) -> None:
|
||||
self._message_queue: PriorityQueue[MessagePriority] = PriorityQueue()
|
||||
self._message_queue: Queue[Message | None] = Queue()
|
||||
self._parent = parent
|
||||
self._running: bool = False
|
||||
self._closing: bool = False
|
||||
@@ -158,7 +136,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
return self._pending_message
|
||||
finally:
|
||||
self._pending_message = None
|
||||
message = (await self._message_queue.get()).message
|
||||
message = await self._message_queue.get()
|
||||
if message is None:
|
||||
self._closed = True
|
||||
raise MessagePumpClosed("The message pump is now closed")
|
||||
@@ -173,7 +151,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
"""
|
||||
if self._pending_message is None:
|
||||
try:
|
||||
message = self._message_queue.get_nowait().message
|
||||
message = self._message_queue.get_nowait()
|
||||
except QueueEmpty:
|
||||
pass
|
||||
else:
|
||||
@@ -247,7 +225,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
|
||||
def close_messages_no_wait(self) -> None:
|
||||
"""Request the message queue to exit."""
|
||||
self._message_queue.put_nowait(MessagePriority(None))
|
||||
self._message_queue.put_nowait(None)
|
||||
|
||||
async def close_messages(self) -> None:
|
||||
"""Close message queue, and optionally wait for queue to finish processing."""
|
||||
@@ -258,7 +236,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
for timer in stop_timers:
|
||||
await timer.stop()
|
||||
self._timers.clear()
|
||||
await self._message_queue.put(MessagePriority(None))
|
||||
await self._message_queue.put(None)
|
||||
|
||||
if self._task is not None and asyncio.current_task() != self._task:
|
||||
# Ensure everything is closed before returning
|
||||
@@ -358,9 +336,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
for cls in self.__class__.__mro__:
|
||||
if message._no_default_action:
|
||||
break
|
||||
method = cls.__dict__.get(private_method, None) or cls.__dict__.get(
|
||||
method_name, None
|
||||
)
|
||||
method = cls.__dict__.get(private_method) or cls.__dict__.get(method_name)
|
||||
if method is not None:
|
||||
yield cls, method.__get__(self, cls)
|
||||
|
||||
@@ -420,7 +396,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
return False
|
||||
if not self.check_message_enabled(message):
|
||||
return True
|
||||
await self._message_queue.put(MessagePriority(message))
|
||||
await self._message_queue.put(message)
|
||||
return True
|
||||
|
||||
# TODO: This may not be needed, or may only be needed by the timer
|
||||
@@ -437,11 +413,12 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
Returns:
|
||||
bool: True if the messages was processed, False if it wasn't.
|
||||
"""
|
||||
# TODO: Allow priority messages to jump the queue
|
||||
if self._closing or self._closed:
|
||||
return False
|
||||
if not self.check_message_enabled(message):
|
||||
return False
|
||||
await self._message_queue.put(MessagePriority(message, -1))
|
||||
await self._message_queue.put(message)
|
||||
return True
|
||||
|
||||
def post_message_no_wait(self, message: Message) -> bool:
|
||||
@@ -457,7 +434,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
return False
|
||||
if not self.check_message_enabled(message):
|
||||
return False
|
||||
self._message_queue.put_nowait(MessagePriority(message))
|
||||
self._message_queue.put_nowait(message)
|
||||
return True
|
||||
|
||||
async def _post_message_from_child(self, message: Message) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user