Skip to content

Commit fb5d649

Browse files
authored
Merge branch 'main' into directory-tree-reload
2 parents bffa84f + 300074d commit fb5d649

29 files changed

+977
-55
lines changed

CHANGELOG.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1111

1212
- Fixed `DirectoryTree.clear_node` not clearing the node specified https://github.com/Textualize/textual/issues/4122
1313

14-
### Added
15-
16-
- `Tree` (and `DirectoryTree`) grew an attribute `lock` that can be used for synchronization across coroutines https://github.com/Textualize/textual/issues/4056
17-
1814
### Changed
1915

2016
- `DirectoryTree.reload` and `DirectoryTree.reload_node` now preserve state when reloading https://github.com/Textualize/textual/issues/4056
17+
- Fixed a crash in the TextArea when performing a backward replace https://github.com/Textualize/textual/pull/4126
18+
- Fixed selection not updating correctly when pasting while there's a non-zero selection https://github.com/Textualize/textual/pull/4126
19+
- Breaking change: `TextArea` will not use `Escape` to shift focus if the `tab_behaviour` is the default https://github.com/Textualize/textual/issues/4110
20+
21+
### Added
22+
23+
- Added DOMQuery.set https://github.com/Textualize/textual/pull/4075
24+
- Added DOMNode.set_reactive https://github.com/Textualize/textual/pull/4075
25+
- Added DOMNode.data_bind https://github.com/Textualize/textual/pull/4075
26+
- Added DOMNode.action_toggle https://github.com/Textualize/textual/pull/4075
27+
- Added Worker.cancelled_event https://github.com/Textualize/textual/pull/4075
28+
- `Tree` (and `DirectoryTree`) grew an attribute `lock` that can be used for synchronization across coroutines https://github.com/Textualize/textual/issues/4056
2129

2230
## [0.48.2] - 2024-02-02
2331

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from textual.app import App, ComposeResult
2+
from textual.containers import Horizontal
3+
from textual.reactive import reactive, var
4+
from textual.widgets import Label
5+
6+
GREETINGS = [
7+
"Bonjour",
8+
"Hola",
9+
"こんにちは",
10+
"你好",
11+
"안녕하세요",
12+
"Hello",
13+
]
14+
15+
16+
class Greeter(Horizontal):
17+
"""Display a greeting and a name."""
18+
19+
DEFAULT_CSS = """
20+
Greeter {
21+
width: auto;
22+
height: 1;
23+
& Label {
24+
margin: 0 1;
25+
}
26+
}
27+
"""
28+
greeting: reactive[str] = reactive("")
29+
who: reactive[str] = reactive("")
30+
31+
def __init__(self, greeting: str = "Hello", who: str = "World!") -> None:
32+
super().__init__()
33+
self.greeting = greeting # (1)!
34+
self.who = who
35+
36+
def compose(self) -> ComposeResult:
37+
yield Label(self.greeting, id="greeting")
38+
yield Label(self.who, id="name")
39+
40+
def watch_greeting(self, greeting: str) -> None:
41+
self.query_one("#greeting", Label).update(greeting) # (2)!
42+
43+
def watch_who(self, who: str) -> None:
44+
self.query_one("#who", Label).update(who)
45+
46+
47+
class NameApp(App):
48+
49+
CSS = """
50+
Screen {
51+
align: center middle;
52+
}
53+
"""
54+
greeting_no: var[int] = var(0)
55+
BINDINGS = [("space", "greeting")]
56+
57+
def compose(self) -> ComposeResult:
58+
yield Greeter(who="Textual")
59+
60+
def action_greeting(self) -> None:
61+
self.greeting_no = (self.greeting_no + 1) % len(GREETINGS)
62+
self.query_one(Greeter).greeting = GREETINGS[self.greeting_no]
63+
64+
65+
if __name__ == "__main__":
66+
app = NameApp()
67+
app.run()
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from textual.app import App, ComposeResult
2+
from textual.containers import Horizontal
3+
from textual.reactive import reactive, var
4+
from textual.widgets import Label
5+
6+
GREETINGS = [
7+
"Bonjour",
8+
"Hola",
9+
"こんにちは",
10+
"你好",
11+
"안녕하세요",
12+
"Hello",
13+
]
14+
15+
16+
class Greeter(Horizontal):
17+
"""Display a greeting and a name."""
18+
19+
DEFAULT_CSS = """
20+
Greeter {
21+
width: auto;
22+
height: 1;
23+
& Label {
24+
margin: 0 1;
25+
}
26+
}
27+
"""
28+
greeting: reactive[str] = reactive("")
29+
who: reactive[str] = reactive("")
30+
31+
def __init__(self, greeting: str = "Hello", who: str = "World!") -> None:
32+
super().__init__()
33+
self.set_reactive(Greeter.greeting, greeting) # (1)!
34+
self.set_reactive(Greeter.who, who)
35+
36+
def compose(self) -> ComposeResult:
37+
yield Label(self.greeting, id="greeting")
38+
yield Label(self.who, id="name")
39+
40+
def watch_greeting(self, greeting: str) -> None:
41+
self.query_one("#greeting", Label).update(greeting)
42+
43+
def watch_who(self, who: str) -> None:
44+
self.query_one("#who", Label).update(who)
45+
46+
47+
class NameApp(App):
48+
49+
CSS = """
50+
Screen {
51+
align: center middle;
52+
}
53+
"""
54+
greeting_no: var[int] = var(0)
55+
BINDINGS = [("space", "greeting")]
56+
57+
def compose(self) -> ComposeResult:
58+
yield Greeter(who="Textual")
59+
60+
def action_greeting(self) -> None:
61+
self.greeting_no = (self.greeting_no + 1) % len(GREETINGS)
62+
self.query_one(Greeter).greeting = GREETINGS[self.greeting_no]
63+
64+
65+
if __name__ == "__main__":
66+
app = NameApp()
67+
app.run()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from datetime import datetime
2+
3+
from pytz import timezone
4+
5+
from textual.app import App, ComposeResult
6+
from textual.reactive import reactive
7+
from textual.widget import Widget
8+
from textual.widgets import Digits, Label
9+
10+
11+
class WorldClock(Widget):
12+
13+
time: reactive[datetime] = reactive(datetime.now)
14+
15+
def __init__(self, timezone: str) -> None:
16+
self.timezone = timezone
17+
super().__init__()
18+
19+
def compose(self) -> ComposeResult:
20+
yield Label(self.timezone)
21+
yield Digits()
22+
23+
def watch_time(self, time: datetime) -> None:
24+
localized_time = time.astimezone(timezone(self.timezone))
25+
self.query_one(Digits).update(localized_time.strftime("%H:%M:%S"))
26+
27+
28+
class WorldClockApp(App):
29+
CSS_PATH = "world_clock01.tcss"
30+
31+
time: reactive[datetime] = reactive(datetime.now)
32+
33+
def compose(self) -> ComposeResult:
34+
yield WorldClock("Europe/London")
35+
yield WorldClock("Europe/Paris")
36+
yield WorldClock("Asia/Tokyo")
37+
38+
def update_time(self) -> None:
39+
self.time = datetime.now()
40+
41+
def watch_time(self, time: datetime) -> None:
42+
for world_clock in self.query(WorldClock): # (1)!
43+
world_clock.time = time
44+
45+
def on_mount(self) -> None:
46+
self.update_time()
47+
self.set_interval(1, self.update_time)
48+
49+
50+
if __name__ == "__main__":
51+
app = WorldClockApp()
52+
app.run()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Screen {
2+
align: center middle;
3+
}
4+
5+
WorldClock {
6+
width: auto;
7+
height: auto;
8+
padding: 1 2;
9+
background: $panel;
10+
border: wide $background;
11+
12+
& Digits {
13+
width: auto;
14+
color: $secondary;
15+
}
16+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from datetime import datetime
2+
3+
from pytz import timezone
4+
5+
from textual.app import App, ComposeResult
6+
from textual.reactive import reactive
7+
from textual.widget import Widget
8+
from textual.widgets import Digits, Label
9+
10+
11+
class WorldClock(Widget):
12+
13+
time: reactive[datetime] = reactive(datetime.now)
14+
15+
def __init__(self, timezone: str) -> None:
16+
self.timezone = timezone
17+
super().__init__()
18+
19+
def compose(self) -> ComposeResult:
20+
yield Label(self.timezone)
21+
yield Digits()
22+
23+
def watch_time(self, time: datetime) -> None:
24+
localized_time = time.astimezone(timezone(self.timezone))
25+
self.query_one(Digits).update(localized_time.strftime("%H:%M:%S"))
26+
27+
28+
class WorldClockApp(App):
29+
CSS_PATH = "world_clock01.tcss"
30+
31+
time: reactive[datetime] = reactive(datetime.now)
32+
33+
def compose(self) -> ComposeResult:
34+
yield WorldClock("Europe/London").data_bind(WorldClockApp.time) # (1)!
35+
yield WorldClock("Europe/Paris").data_bind(WorldClockApp.time)
36+
yield WorldClock("Asia/Tokyo").data_bind(WorldClockApp.time)
37+
38+
def update_time(self) -> None:
39+
self.time = datetime.now()
40+
41+
def on_mount(self) -> None:
42+
self.update_time()
43+
self.set_interval(1, self.update_time)
44+
45+
46+
if __name__ == "__main__":
47+
WorldClockApp().run()
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from datetime import datetime
2+
3+
from pytz import timezone
4+
5+
from textual.app import App, ComposeResult
6+
from textual.reactive import reactive
7+
from textual.widget import Widget
8+
from textual.widgets import Digits, Label
9+
10+
11+
class WorldClock(Widget):
12+
13+
clock_time: reactive[datetime] = reactive(datetime.now)
14+
15+
def __init__(self, timezone: str) -> None:
16+
self.timezone = timezone
17+
super().__init__()
18+
19+
def compose(self) -> ComposeResult:
20+
yield Label(self.timezone)
21+
yield Digits()
22+
23+
def watch_clock_time(self, time: datetime) -> None:
24+
localized_time = time.astimezone(timezone(self.timezone))
25+
self.query_one(Digits).update(localized_time.strftime("%H:%M:%S"))
26+
27+
28+
class WorldClockApp(App):
29+
CSS_PATH = "world_clock01.tcss"
30+
31+
time: reactive[datetime] = reactive(datetime.now)
32+
33+
def compose(self) -> ComposeResult:
34+
yield WorldClock("Europe/London").data_bind(
35+
clock_time=WorldClockApp.time # (1)!
36+
)
37+
yield WorldClock("Europe/Paris").data_bind(clock_time=WorldClockApp.time)
38+
yield WorldClock("Asia/Tokyo").data_bind(clock_time=WorldClockApp.time)
39+
40+
def update_time(self) -> None:
41+
self.time = datetime.now()
42+
43+
def on_mount(self) -> None:
44+
self.update_time()
45+
self.set_interval(1, self.update_time)
46+
47+
48+
if __name__ == "__main__":
49+
WorldClockApp().run()

docs/guide/queries.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,12 @@ for widget in self.query("Button"):
161161

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

164-
- [set_class][textual.css.query.DOMQuery.set_class] Sets a CSS class (or classes) on matched widgets.
165164
- [add_class][textual.css.query.DOMQuery.add_class] Adds a CSS class (or classes) to matched widgets.
165+
- [blur][textual.css.query.DOMQuery.focus] Blurs (removes focus) from matching widgets.
166+
- [focus][textual.css.query.DOMQuery.focus] Focuses the first matching widgets.
167+
- [refresh][textual.css.query.DOMQuery.refresh] Refreshes matched widgets.
166168
- [remove_class][textual.css.query.DOMQuery.remove_class] Removes a CSS class (or classes) from matched widgets.
167-
- [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.
168169
- [remove][textual.css.query.DOMQuery.remove] Removes matched widgets from the DOM.
169-
- [refresh][textual.css.query.DOMQuery.refresh] Refreshes matched widgets.
170-
170+
- [set_class][textual.css.query.DOMQuery.set_class] Sets a CSS class (or classes) on matched widgets.
171+
- [set][textual.css.query.DOMQuery.set] Sets common attributes on a widget.
172+
- [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.

0 commit comments

Comments
 (0)