Skip to content

Commit

Permalink
Merge pull request #2628 from Textualize/auto-focus-improv
Browse files Browse the repository at this point in the history
Add auto focus to app.
  • Loading branch information
rodrigogiraoserrao authored May 25, 2023
2 parents afbf52d + bc92cf5 commit 5cb30b5
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 6 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added

- `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

- `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
- 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

Expand Down
8 changes: 8 additions & 0 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ class MyApp(App[None]):
"""
SCREENS: ClassVar[dict[str, Screen | Callable[[], Screen]]] = {}
"""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
CSS_PATH: ClassVar[CSSPathType | None] = None
"""File paths to load CSS from."""
Expand Down
10 changes: 6 additions & 4 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,12 @@ def __call__(self, result: ScreenResultType) -> None:
class Screen(Generic[ScreenResultType], Widget):
"""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.
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 = """
Expand Down Expand Up @@ -681,8 +682,9 @@ def _on_screen_resume(self) -> None:
size = self.app.size
self._refresh_layout(size, full=True)
self.refresh()
if self.AUTO_FOCUS is not None and self.focused is None:
for widget in self.query(self.AUTO_FOCUS):
auto_focus = self.app.AUTO_FOCUS if self.AUTO_FOCUS is None else self.AUTO_FOCUS
if auto_focus and self.focused is None:
for widget in self.query(auto_focus):
if widget.focusable:
self.set_focus(widget)
break
Expand Down
81 changes: 79 additions & 2 deletions tests/test_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,21 @@ async def test_screens():
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]):
def compose(self):
yield Button()
yield Input(id="one")
yield Input(id="two")

class MyApp(App[None]):
pass
AUTO_FOCUS = None

app = MyApp()
async with app.run_test():
MyScreen.AUTO_FOCUS = "*"
await app.push_screen(MyScreen())
assert isinstance(app.focused, Button)
app.pop_screen()
Expand Down Expand Up @@ -193,6 +196,80 @@ class MyApp(App[None]):
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():
class MyScreen(Screen[None]):
def compose(self):
Expand Down

0 comments on commit 5cb30b5

Please sign in to comment.