mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
@@ -10,12 +10,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `work` decorator accepts `description` parameter to add debug string https://github.com/Textualize/textual/issues/2597
|
- `work` decorator accepts `description` parameter to add debug string https://github.com/Textualize/textual/issues/2597
|
||||||
|
- `App.AUTO_FOCUS` to set auto focus on all screens https://github.com/Textualize/textual/issues/2594
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `Placeholder` now sets its color cycle per app https://github.com/Textualize/textual/issues/2590
|
- `Placeholder` now sets its color cycle per app https://github.com/Textualize/textual/issues/2590
|
||||||
- Footer now clears key highlight regardless of whether it's in the active screen or not https://github.com/Textualize/textual/issues/2606
|
- Footer now clears key highlight regardless of whether it's in the active screen or not https://github.com/Textualize/textual/issues/2606
|
||||||
- The default Widget repr no longer displays classes and pseudo-classes (to reduce noise in logs). Add them to your `__rich_repr__` method if needed. https://github.com/Textualize/textual/pull/2623
|
- The default Widget repr no longer displays classes and pseudo-classes (to reduce noise in logs). Add them to your `__rich_repr__` method if needed. https://github.com/Textualize/textual/pull/2623
|
||||||
|
- Setting `Screen.AUTO_FOCUS` to `None` will inherit `AUTO_FOCUS` from the app instead of disabling it https://github.com/Textualize/textual/issues/2594
|
||||||
|
- Setting `Screen.AUTO_FOCUS` to `""` will disable it on the screen https://github.com/Textualize/textual/issues/2594
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
|||||||
@@ -275,6 +275,14 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
"""
|
"""
|
||||||
SCREENS: ClassVar[dict[str, Screen | Callable[[], Screen]]] = {}
|
SCREENS: ClassVar[dict[str, Screen | Callable[[], Screen]]] = {}
|
||||||
"""Screens associated with the app for the lifetime of the app."""
|
"""Screens associated with the app for the lifetime of the app."""
|
||||||
|
|
||||||
|
AUTO_FOCUS: ClassVar[str | None] = "*"
|
||||||
|
"""A selector to determine what to focus automatically when a screen is activated.
|
||||||
|
|
||||||
|
The widget focused is the first that matches the given [CSS selector](/guide/queries/#query-selectors).
|
||||||
|
Setting to `None` or `""` disables auto focus.
|
||||||
|
"""
|
||||||
|
|
||||||
_BASE_PATH: str | None = None
|
_BASE_PATH: str | None = None
|
||||||
CSS_PATH: ClassVar[CSSPathType | None] = None
|
CSS_PATH: ClassVar[CSSPathType | None] = None
|
||||||
"""File paths to load CSS from."""
|
"""File paths to load CSS from."""
|
||||||
|
|||||||
@@ -94,11 +94,12 @@ class ResultCallback(Generic[ScreenResultType]):
|
|||||||
class Screen(Generic[ScreenResultType], Widget):
|
class Screen(Generic[ScreenResultType], Widget):
|
||||||
"""The base class for screens."""
|
"""The base class for screens."""
|
||||||
|
|
||||||
AUTO_FOCUS: ClassVar[str | None] = "*"
|
AUTO_FOCUS: ClassVar[str | None] = None
|
||||||
"""A selector to determine what to focus automatically when the screen is activated.
|
"""A selector to determine what to focus automatically when the screen is activated.
|
||||||
|
|
||||||
The widget focused is the first that matches the given [CSS selector](/guide/queries/#query-selectors).
|
The widget focused is the first that matches the given [CSS selector](/guide/queries/#query-selectors).
|
||||||
Set to `None` to disable auto focus.
|
Set to `None` to inherit the value from the screen's app.
|
||||||
|
Set to `""` to disable auto focus.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
@@ -680,8 +681,9 @@ class Screen(Generic[ScreenResultType], Widget):
|
|||||||
size = self.app.size
|
size = self.app.size
|
||||||
self._refresh_layout(size, full=True)
|
self._refresh_layout(size, full=True)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
if self.AUTO_FOCUS is not None and self.focused is None:
|
auto_focus = self.app.AUTO_FOCUS if self.AUTO_FOCUS is None else self.AUTO_FOCUS
|
||||||
for widget in self.query(self.AUTO_FOCUS):
|
if auto_focus and self.focused is None:
|
||||||
|
for widget in self.query(auto_focus):
|
||||||
if widget.focusable:
|
if widget.focusable:
|
||||||
self.set_focus(widget)
|
self.set_focus(widget)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -153,7 +153,9 @@ async def test_screens():
|
|||||||
await app._shutdown()
|
await app._shutdown()
|
||||||
|
|
||||||
|
|
||||||
async def test_auto_focus():
|
async def test_auto_focus_on_screen_if_app_auto_focus_is_none():
|
||||||
|
"""Setting app.AUTO_FOCUS = `None` means it is not taken into consideration."""
|
||||||
|
|
||||||
class MyScreen(Screen[None]):
|
class MyScreen(Screen[None]):
|
||||||
def compose(self):
|
def compose(self):
|
||||||
yield Button()
|
yield Button()
|
||||||
@@ -161,10 +163,11 @@ async def test_auto_focus():
|
|||||||
yield Input(id="two")
|
yield Input(id="two")
|
||||||
|
|
||||||
class MyApp(App[None]):
|
class MyApp(App[None]):
|
||||||
pass
|
AUTO_FOCUS = None
|
||||||
|
|
||||||
app = MyApp()
|
app = MyApp()
|
||||||
async with app.run_test():
|
async with app.run_test():
|
||||||
|
MyScreen.AUTO_FOCUS = "*"
|
||||||
await app.push_screen(MyScreen())
|
await app.push_screen(MyScreen())
|
||||||
assert isinstance(app.focused, Button)
|
assert isinstance(app.focused, Button)
|
||||||
app.pop_screen()
|
app.pop_screen()
|
||||||
@@ -193,6 +196,80 @@ async def test_auto_focus():
|
|||||||
assert app.focused.id == "two"
|
assert app.focused.id == "two"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auto_focus_on_screen_if_app_auto_focus_is_disabled():
|
||||||
|
"""Setting app.AUTO_FOCUS = `None` means it is not taken into consideration."""
|
||||||
|
|
||||||
|
class MyScreen(Screen[None]):
|
||||||
|
def compose(self):
|
||||||
|
yield Button()
|
||||||
|
yield Input(id="one")
|
||||||
|
yield Input(id="two")
|
||||||
|
|
||||||
|
class MyApp(App[None]):
|
||||||
|
AUTO_FOCUS = ""
|
||||||
|
|
||||||
|
app = MyApp()
|
||||||
|
async with app.run_test():
|
||||||
|
MyScreen.AUTO_FOCUS = "*"
|
||||||
|
await app.push_screen(MyScreen())
|
||||||
|
assert isinstance(app.focused, Button)
|
||||||
|
app.pop_screen()
|
||||||
|
|
||||||
|
MyScreen.AUTO_FOCUS = None
|
||||||
|
await app.push_screen(MyScreen())
|
||||||
|
assert app.focused is None
|
||||||
|
app.pop_screen()
|
||||||
|
|
||||||
|
MyScreen.AUTO_FOCUS = "Input"
|
||||||
|
await app.push_screen(MyScreen())
|
||||||
|
assert isinstance(app.focused, Input)
|
||||||
|
assert app.focused.id == "one"
|
||||||
|
app.pop_screen()
|
||||||
|
|
||||||
|
MyScreen.AUTO_FOCUS = "#two"
|
||||||
|
await app.push_screen(MyScreen())
|
||||||
|
assert isinstance(app.focused, Input)
|
||||||
|
assert app.focused.id == "two"
|
||||||
|
|
||||||
|
# If we push and pop another screen, focus should be preserved for #two.
|
||||||
|
MyScreen.AUTO_FOCUS = None
|
||||||
|
await app.push_screen(MyScreen())
|
||||||
|
assert app.focused is None
|
||||||
|
app.pop_screen()
|
||||||
|
assert app.focused.id == "two"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auto_focus_inheritance():
|
||||||
|
"""Setting app.AUTO_FOCUS = `None` means it is not taken into consideration."""
|
||||||
|
|
||||||
|
class MyScreen(Screen[None]):
|
||||||
|
def compose(self):
|
||||||
|
yield Button()
|
||||||
|
yield Input(id="one")
|
||||||
|
yield Input(id="two")
|
||||||
|
|
||||||
|
class MyApp(App[None]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
app = MyApp()
|
||||||
|
async with app.run_test():
|
||||||
|
MyApp.AUTO_FOCUS = "Input"
|
||||||
|
MyScreen.AUTO_FOCUS = "*"
|
||||||
|
await app.push_screen(MyScreen())
|
||||||
|
assert isinstance(app.focused, Button)
|
||||||
|
app.pop_screen()
|
||||||
|
|
||||||
|
MyScreen.AUTO_FOCUS = None
|
||||||
|
await app.push_screen(MyScreen())
|
||||||
|
assert isinstance(app.focused, Input)
|
||||||
|
app.pop_screen()
|
||||||
|
|
||||||
|
MyScreen.AUTO_FOCUS = ""
|
||||||
|
await app.push_screen(MyScreen())
|
||||||
|
assert app.focused is None
|
||||||
|
app.pop_screen()
|
||||||
|
|
||||||
|
|
||||||
async def test_auto_focus_skips_non_focusable_widgets():
|
async def test_auto_focus_skips_non_focusable_widgets():
|
||||||
class MyScreen(Screen[None]):
|
class MyScreen(Screen[None]):
|
||||||
def compose(self):
|
def compose(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user