From ed4d811451226cac847af0d7302f71876df39595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 9 May 2023 14:55:18 +0100 Subject: [PATCH 1/3] Add tests for Screen auto focus. Related issues: #2457. --- tests/test_screens.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_screens.py b/tests/test_screens.py index 6825c101c..7e20348a9 100644 --- a/tests/test_screens.py +++ b/tests/test_screens.py @@ -6,6 +6,7 @@ import pytest from textual.app import App, ScreenStackError from textual.screen import Screen +from textual.widgets import Button, Input skip_py310 = pytest.mark.skipif( sys.version_info.minor == 10 and sys.version_info.major == 3, @@ -150,3 +151,44 @@ async def test_screens(): screen2.remove() screen3.remove() await app._shutdown() + + +async def test_auto_focus(): + class MyScreen(Screen[None]): + def compose(self) -> None: + print("composing") + yield Button() + yield Input(id="one") + yield Input(id="two") + + class MyApp(App[None]): + pass + + app = MyApp() + async with app.run_test(): + 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" From 8d3f69a04d49d1e4e83db1c573d1232f7201aede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 9 May 2023 14:57:50 +0100 Subject: [PATCH 2/3] Add auto_focus attribute to screens. --- CHANGELOG.md | 7 +++++++ src/textual/screen.py | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2923fac..d5f95b549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). + +## Unreleased + +### Added + +- Attribute `auto_focus` to screens https://github.com/Textualize/textual/issues/2457 + ## [0.24.1] - 2023-05-08 ### Fixed diff --git a/src/textual/screen.py b/src/textual/screen.py index 34db473ef..8bc9c0532 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -30,7 +30,7 @@ from ._types import CallbackType from .binding import Binding from .css.match import match from .css.parse import parse_selectors -from .css.query import QueryType +from .css.query import NoMatches, QueryType from .dom import DOMNode from .geometry import Offset, Region, Size from .reactive import Reactive @@ -101,6 +101,12 @@ class Screen(Generic[ScreenResultType], Widget): } """ + auto_focus: str | 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. + """ focused: Reactive[Widget | None] = Reactive(None) """The focused [widget][textual.widget.Widget] or `None` for no focus.""" stack_updates: Reactive[int] = Reactive(0, repaint=False) @@ -659,6 +665,13 @@ class Screen(Generic[ScreenResultType], Widget): """Screen has resumed.""" self.stack_updates += 1 size = self.app.size + if self.auto_focus is not None and self.focused is None: + try: + to_focus = self.query(self.auto_focus).first() + except NoMatches: + pass + else: + self.set_focus(to_focus) self._refresh_layout(size, full=True) self.refresh() From 3245eb38bb5f6a4a95fbe282f30b88288c49505f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 9 May 2023 16:44:37 +0100 Subject: [PATCH 3/3] Make auto-focus a class var. Related comments: https://github.com/Textualize/textual/pull/2527\#discussion_r1188776849 --- src/textual/screen.py | 19 ++++++++++--------- tests/test_screens.py | 8 ++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index 8bc9c0532..af0b006be 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -9,6 +9,7 @@ from typing import ( TYPE_CHECKING, Awaitable, Callable, + ClassVar, Generic, Iterable, Iterator, @@ -93,6 +94,13 @@ class ResultCallback(Generic[ScreenResultType]): class Screen(Generic[ScreenResultType], Widget): """The base class for screens.""" + AUTO_FOCUS: ClassVar[str | 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. + """ + DEFAULT_CSS = """ Screen { layout: vertical; @@ -100,13 +108,6 @@ class Screen(Generic[ScreenResultType], Widget): background: $surface; } """ - - auto_focus: str | 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. - """ focused: Reactive[Widget | None] = Reactive(None) """The focused [widget][textual.widget.Widget] or `None` for no focus.""" stack_updates: Reactive[int] = Reactive(0, repaint=False) @@ -665,9 +666,9 @@ class Screen(Generic[ScreenResultType], Widget): """Screen has resumed.""" self.stack_updates += 1 size = self.app.size - if self.auto_focus is not None and self.focused is None: + if self.AUTO_FOCUS is not None and self.focused is None: try: - to_focus = self.query(self.auto_focus).first() + to_focus = self.query(self.AUTO_FOCUS).first() except NoMatches: pass else: diff --git a/tests/test_screens.py b/tests/test_screens.py index 7e20348a9..2e3dbfcbe 100644 --- a/tests/test_screens.py +++ b/tests/test_screens.py @@ -170,24 +170,24 @@ async def test_auto_focus(): assert isinstance(app.focused, Button) app.pop_screen() - MyScreen.auto_focus = None + MyScreen.AUTO_FOCUS = None await app.push_screen(MyScreen()) assert app.focused is None app.pop_screen() - MyScreen.auto_focus = "Input" + 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" + 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 + MyScreen.AUTO_FOCUS = None await app.push_screen(MyScreen()) assert app.focused is None app.pop_screen()