Skip to content

Commit

Permalink
Merge pull request #4075 from Textualize/cancelled-event
Browse files Browse the repository at this point in the history
Data binding and more
  • Loading branch information
willmcgugan authored Feb 6, 2024
2 parents ca2c11b + f39a7c9 commit 5d6c61a
Show file tree
Hide file tree
Showing 25 changed files with 840 additions and 39 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed a hang in the Linux driver when connected to a pipe https://github.com/Textualize/textual/issues/4104
- Fixed broken `OptionList` `Option.id` mappings https://github.com/Textualize/textual/issues/4101

### Added

- Added DOMQuery.set https://github.com/Textualize/textual/pull/4075
- Added DOMNode.set_reactive https://github.com/Textualize/textual/pull/4075
- Added DOMNode.data_bind https://github.com/Textualize/textual/pull/4075
- Added DOMNode.action_toggle https://github.com/Textualize/textual/pull/4075
- Added Worker.cancelled_event https://github.com/Textualize/textual/pull/4075

### Changed

- Breaking change: keyboard navigation in `RadioSet`, `ListView`, `OptionList`, and `SelectionList`, no longer allows highlighting disabled items https://github.com/Textualize/textual/issues/3881
Expand Down
67 changes: 67 additions & 0 deletions docs/examples/guide/reactivity/set_reactive01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.reactive import reactive, var
from textual.widgets import Label

GREETINGS = [
"Bonjour",
"Hola",
"こんにちは",
"你好",
"안녕하세요",
"Hello",
]


class Greeter(Horizontal):
"""Display a greeting and a name."""

DEFAULT_CSS = """
Greeter {
width: auto;
height: 1;
& Label {
margin: 0 1;
}
}
"""
greeting: reactive[str] = reactive("")
who: reactive[str] = reactive("")

def __init__(self, greeting: str = "Hello", who: str = "World!") -> None:
super().__init__()
self.greeting = greeting # (1)!
self.who = who

def compose(self) -> ComposeResult:
yield Label(self.greeting, id="greeting")
yield Label(self.who, id="name")

def watch_greeting(self, greeting: str) -> None:
self.query_one("#greeting", Label).update(greeting) # (2)!

def watch_who(self, who: str) -> None:
self.query_one("#who", Label).update(who)


class NameApp(App):

CSS = """
Screen {
align: center middle;
}
"""
greeting_no: var[int] = var(0)
BINDINGS = [("space", "greeting")]

def compose(self) -> ComposeResult:
yield Greeter(who="Textual")

def action_greeting(self) -> None:
self.greeting_no = (self.greeting_no + 1) % len(GREETINGS)
self.query_one(Greeter).greeting = GREETINGS[self.greeting_no]


if __name__ == "__main__":
app = NameApp()
app.run()
67 changes: 67 additions & 0 deletions docs/examples/guide/reactivity/set_reactive02.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.reactive import reactive, var
from textual.widgets import Label

GREETINGS = [
"Bonjour",
"Hola",
"こんにちは",
"你好",
"안녕하세요",
"Hello",
]


class Greeter(Horizontal):
"""Display a greeting and a name."""

DEFAULT_CSS = """
Greeter {
width: auto;
height: 1;
& Label {
margin: 0 1;
}
}
"""
greeting: reactive[str] = reactive("")
who: reactive[str] = reactive("")

def __init__(self, greeting: str = "Hello", who: str = "World!") -> None:
super().__init__()
self.set_reactive(Greeter.greeting, greeting) # (1)!
self.set_reactive(Greeter.who, who)

def compose(self) -> ComposeResult:
yield Label(self.greeting, id="greeting")
yield Label(self.who, id="name")

def watch_greeting(self, greeting: str) -> None:
self.query_one("#greeting", Label).update(greeting)

def watch_who(self, who: str) -> None:
self.query_one("#who", Label).update(who)


class NameApp(App):

CSS = """
Screen {
align: center middle;
}
"""
greeting_no: var[int] = var(0)
BINDINGS = [("space", "greeting")]

def compose(self) -> ComposeResult:
yield Greeter(who="Textual")

def action_greeting(self) -> None:
self.greeting_no = (self.greeting_no + 1) % len(GREETINGS)
self.query_one(Greeter).greeting = GREETINGS[self.greeting_no]


if __name__ == "__main__":
app = NameApp()
app.run()
52 changes: 52 additions & 0 deletions docs/examples/guide/reactivity/world_clock01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from datetime import datetime

from pytz import timezone

from textual.app import App, ComposeResult
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Digits, Label


class WorldClock(Widget):

time: reactive[datetime] = reactive(datetime.now)

def __init__(self, timezone: str) -> None:
self.timezone = timezone
super().__init__()

def compose(self) -> ComposeResult:
yield Label(self.timezone)
yield Digits()

def watch_time(self, time: datetime) -> None:
localized_time = time.astimezone(timezone(self.timezone))
self.query_one(Digits).update(localized_time.strftime("%H:%M:%S"))


class WorldClockApp(App):
CSS_PATH = "world_clock01.tcss"

time: reactive[datetime] = reactive(datetime.now)

def compose(self) -> ComposeResult:
yield WorldClock("Europe/London")
yield WorldClock("Europe/Paris")
yield WorldClock("Asia/Tokyo")

def update_time(self) -> None:
self.time = datetime.now()

def watch_time(self, time: datetime) -> None:
for world_clock in self.query(WorldClock): # (1)!
world_clock.time = time

def on_mount(self) -> None:
self.update_time()
self.set_interval(1, self.update_time)


if __name__ == "__main__":
app = WorldClockApp()
app.run()
16 changes: 16 additions & 0 deletions docs/examples/guide/reactivity/world_clock01.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Screen {
align: center middle;
}

WorldClock {
width: auto;
height: auto;
padding: 1 2;
background: $panel;
border: wide $background;

& Digits {
width: auto;
color: $secondary;
}
}
47 changes: 47 additions & 0 deletions docs/examples/guide/reactivity/world_clock02.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from datetime import datetime

from pytz import timezone

from textual.app import App, ComposeResult
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Digits, Label


class WorldClock(Widget):

time: reactive[datetime] = reactive(datetime.now)

def __init__(self, timezone: str) -> None:
self.timezone = timezone
super().__init__()

def compose(self) -> ComposeResult:
yield Label(self.timezone)
yield Digits()

def watch_time(self, time: datetime) -> None:
localized_time = time.astimezone(timezone(self.timezone))
self.query_one(Digits).update(localized_time.strftime("%H:%M:%S"))


class WorldClockApp(App):
CSS_PATH = "world_clock01.tcss"

time: reactive[datetime] = reactive(datetime.now)

def compose(self) -> ComposeResult:
yield WorldClock("Europe/London").data_bind(WorldClockApp.time) # (1)!
yield WorldClock("Europe/Paris").data_bind(WorldClockApp.time)
yield WorldClock("Asia/Tokyo").data_bind(WorldClockApp.time)

def update_time(self) -> None:
self.time = datetime.now()

def on_mount(self) -> None:
self.update_time()
self.set_interval(1, self.update_time)


if __name__ == "__main__":
WorldClockApp().run()
49 changes: 49 additions & 0 deletions docs/examples/guide/reactivity/world_clock03.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from datetime import datetime

from pytz import timezone

from textual.app import App, ComposeResult
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Digits, Label


class WorldClock(Widget):

clock_time: reactive[datetime] = reactive(datetime.now)

def __init__(self, timezone: str) -> None:
self.timezone = timezone
super().__init__()

def compose(self) -> ComposeResult:
yield Label(self.timezone)
yield Digits()

def watch_clock_time(self, time: datetime) -> None:
localized_time = time.astimezone(timezone(self.timezone))
self.query_one(Digits).update(localized_time.strftime("%H:%M:%S"))


class WorldClockApp(App):
CSS_PATH = "world_clock01.tcss"

time: reactive[datetime] = reactive(datetime.now)

def compose(self) -> ComposeResult:
yield WorldClock("Europe/London").data_bind(
clock_time=WorldClockApp.time # (1)!
)
yield WorldClock("Europe/Paris").data_bind(clock_time=WorldClockApp.time)
yield WorldClock("Asia/Tokyo").data_bind(clock_time=WorldClockApp.time)

def update_time(self) -> None:
self.time = datetime.now()

def on_mount(self) -> None:
self.update_time()
self.set_interval(1, self.update_time)


if __name__ == "__main__":
WorldClockApp().run()
10 changes: 6 additions & 4 deletions docs/guide/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,12 @@ for widget in self.query("Button"):

Here are the other loop-free methods on query objects:

- [set_class][textual.css.query.DOMQuery.set_class] Sets a CSS class (or classes) on matched widgets.
- [add_class][textual.css.query.DOMQuery.add_class] Adds a CSS class (or classes) to matched widgets.
- [blur][textual.css.query.DOMQuery.focus] Blurs (removes focus) from matching widgets.
- [focus][textual.css.query.DOMQuery.focus] Focuses the first matching widgets.
- [refresh][textual.css.query.DOMQuery.refresh] Refreshes matched widgets.
- [remove_class][textual.css.query.DOMQuery.remove_class] Removes a CSS class (or classes) from matched widgets.
- [toggle_class][textual.css.query.DOMQuery.toggle_class] Sets a CSS class (or classes) if it is not set, or removes the class (or classes) if they are set on the matched widgets.
- [remove][textual.css.query.DOMQuery.remove] Removes matched widgets from the DOM.
- [refresh][textual.css.query.DOMQuery.refresh] Refreshes matched widgets.

- [set_class][textual.css.query.DOMQuery.set_class] Sets a CSS class (or classes) on matched widgets.
- [set][textual.css.query.DOMQuery.set] Sets common attributes on a widget.
- [toggle_class][textual.css.query.DOMQuery.toggle_class] Sets a CSS class (or classes) if it is not set, or removes the class (or classes) if they are set on the matched widgets.
Loading

0 comments on commit 5d6c61a

Please sign in to comment.