From afb1d0b38760edf91af41557f345ff8c020c6c30 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 11 Dec 2024 14:11:41 +0000 Subject: [PATCH 1/9] Dont select the text in the command palette input on focus --- src/textual/command.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/textual/command.py b/src/textual/command.py index 24ad46c8df..d6634e46ba 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -331,9 +331,7 @@ class SimpleCommand(NamedTuple): """The description of the command.""" -CommandListItem: TypeAlias = ( - "SimpleCommand | tuple[str, IgnoreReturnCallbackType, str | None] | tuple[str, IgnoreReturnCallbackType]" -) +CommandListItem: TypeAlias = "SimpleCommand | tuple[str, IgnoreReturnCallbackType, str | None] | tuple[str, IgnoreReturnCallbackType]" class SimpleProvider(Provider): @@ -776,7 +774,7 @@ def compose(self) -> ComposeResult: with Vertical(id="--container"): with Horizontal(id="--input"): yield SearchIcon() - yield CommandInput(placeholder=self._placeholder) + yield CommandInput(placeholder=self._placeholder, select_on_focus=False) if not self.run_on_select: yield Button("\u25b6") with Vertical(id="--results"): From eb4e192f18311378a692cdd1387b29b9f114a1fb Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 12 Dec 2024 11:40:49 +0000 Subject: [PATCH 2/9] Add missing changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46ff52f8ca..d1c99517a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `system` boolean to Binding, which hides the binding from the help panel https://github.com/Textualize/textual/pull/5352 - Added support for double/triple/etc clicks via `chain` attribute on `Click` events https://github.com/Textualize/textual/pull/5369 - Added `times` parameter to `Pilot.click` method, for simulating rapid clicks https://github.com/Textualize/textual/pull/5369 +- Text can now be select using mouse or keyboard in the Input widget https://github.com/Textualize/textual/pull/5340 + ### Changed From b444c0f7c33c2a8c701b261cea143535aa40dbea Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 12 Dec 2024 11:41:07 +0000 Subject: [PATCH 3/9] Changelog formatting --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c99517a4..e60a18a4d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `times` parameter to `Pilot.click` method, for simulating rapid clicks https://github.com/Textualize/textual/pull/5369 - Text can now be select using mouse or keyboard in the Input widget https://github.com/Textualize/textual/pull/5340 - ### Changed - Breaking change: Change default quit key to `ctrl+q` https://github.com/Textualize/textual/pull/5352 From d1464b547f0ba1ff2e8d4f093fe0b2b52f98fa6c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 12 Dec 2024 11:44:51 +0000 Subject: [PATCH 4/9] Enhance Focus event with from_app_focus argument Added a new argument `from_app_focus` to the Focus event class to indicate whether the focus event was triggered by the application regaining focus or by user interaction within the Textual app. Updated the constructor to initialize this new attribute. --- src/textual/events.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/textual/events.py b/src/textual/events.py index b977c1edf4..9b98af77f7 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -16,10 +16,10 @@ from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Type, TypeVar -from typing_extensions import Self import rich.repr from rich.style import Style +from typing_extensions import Self from textual._types import CallbackType from textual.geometry import Offset, Size @@ -722,8 +722,18 @@ class Focus(Event, bubble=False): - [ ] Bubbles - [ ] Verbose + + Args: + from_app_focus: True if this focus event has been sent because the app itself has + regained focus (via an AppFocus event). False if the focus came from within + the Textual app (e.g. via the user pressing tab or a programmatic setting + of the focused widget). """ + def __init__(self, from_app_focus: bool = False) -> None: + self.from_app_focus = from_app_focus + super().__init__() + class Blur(Event, bubble=False): """Sent when a widget is blurred (un-focussed). From b2d9dc3d2bc290840cbfb4ee3a6107fb743ab4c6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 12 Dec 2024 11:49:00 +0000 Subject: [PATCH 5/9] Include a flag in Focus events to indicate whether they're due to the App regaining focus or whether they're a standard focus. --- src/textual/screen.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index f120c95cba..b8d79116f2 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -869,12 +869,20 @@ def _update_focus_styles( [widget for widget in widgets if widget._has_focus_within], animate=True ) - def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: + def set_focus( + self, + widget: Widget | None, + scroll_visible: bool = True, + from_app_focus: bool = False, + ) -> None: """Focus (or un-focus) a widget. A focused widget will receive key events first. Args: widget: Widget to focus, or None to un-focus. scroll_visible: Scroll widget in to view. + from_app_focus: True if this focus is due to the app itself having regained + focus. False if the focus is being set because a widget within the app + regained focus. """ if widget is self.focused: # Widget is already focused @@ -899,7 +907,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: # Change focus self.focused = widget # Send focus event - widget.post_message(events.Focus()) + widget.post_message(events.Focus(from_app_focus=from_app_focus)) focused = widget if scroll_visible: From a7bacbbb5669ec43e48166c7e2b3a5eacd9eec75 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 12 Dec 2024 13:20:23 +0000 Subject: [PATCH 6/9] Only apply select_on_focus in Input if the focus event wasn't produced from an app focus event. --- src/textual/app.py | 4 +++- src/textual/events.py | 4 ++++ src/textual/widgets/_input.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 6c0c10e8d2..346cfcff0f 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -4052,7 +4052,9 @@ def _watch_app_focus(self, focus: bool) -> None: # ...settle focus back on that widget. # Don't scroll the newly focused widget, as this can be quite jarring self.screen.set_focus( - self._last_focused_on_app_blur, scroll_visible=False + self._last_focused_on_app_blur, + scroll_visible=False, + from_app_focus=True, ) except NoScreen: pass diff --git a/src/textual/events.py b/src/textual/events.py index 9b98af77f7..cf6d5706cc 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -734,6 +734,10 @@ def __init__(self, from_app_focus: bool = False) -> None: self.from_app_focus = from_app_focus super().__init__() + def __rich_repr__(self) -> rich.repr.Result: + yield from super().__rich_repr__() + yield "from_app_focus", self.from_app_focus + class Blur(Event, bubble=False): """Sent when a widget is blurred (un-focussed). diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 8f343b1df3..78e501ffe4 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -641,7 +641,7 @@ def _on_blur(self, event: Blur) -> None: def _on_focus(self, event: Focus) -> None: self._restart_blink() - if self.select_on_focus: + if self.select_on_focus and not event.from_app_focus: self.selection = Selection(0, len(self.value)) self.app.cursor_position = self.cursor_screen_offset self._suggestion = "" From b3456056820376061a1c2362e842403680d725d1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 12 Dec 2024 13:23:48 +0000 Subject: [PATCH 7/9] Fix formatting --- src/textual/command.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/textual/command.py b/src/textual/command.py index d6634e46ba..5833477234 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -331,7 +331,9 @@ class SimpleCommand(NamedTuple): """The description of the command.""" -CommandListItem: TypeAlias = "SimpleCommand | tuple[str, IgnoreReturnCallbackType, str | None] | tuple[str, IgnoreReturnCallbackType]" +CommandListItem: TypeAlias = ( + "SimpleCommand | tuple[str, IgnoreReturnCallbackType, str | None] | tuple[str, IgnoreReturnCallbackType]" +) class SimpleProvider(Provider): From 01befb2faf1fc2fc33741828227cd3b2953647dc Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 12 Dec 2024 13:28:50 +0000 Subject: [PATCH 8/9] Update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e60a18a4d2..21dcd4c1ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ 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 + +- Added `from_app_focus` to `Focus` event to indicate if a widget is being focused because the app itself has regained focus or not https://github.com/Textualize/textual/pull/5379 + +### Changed + +- The content of an `Input` will now only be automatically selected when the widget is focused by the user, not when the app itself has regained focus (similar to web browsers). https://github.com/Textualize/textual/pull/5379 + ## [1.0.0] - 2024-12-12 ### Added From 4953c3bd0f0a6524304d99b6110e485340e96290 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 12 Dec 2024 13:47:45 +0000 Subject: [PATCH 9/9] Test for Input.select_on_focus interaction with AppBlur and AppFocus --- tests/input/test_select_on_focus.py | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/input/test_select_on_focus.py diff --git a/tests/input/test_select_on_focus.py b/tests/input/test_select_on_focus.py new file mode 100644 index 0000000000..d804ac6ee6 --- /dev/null +++ b/tests/input/test_select_on_focus.py @@ -0,0 +1,30 @@ +"""The standard path of selecting text on focus is well covered by snapshot tests.""" + +from textual import events +from textual.app import App, ComposeResult +from textual.widgets import Input +from textual.widgets.input import Selection + + +class InputApp(App[None]): + """An app with an input widget.""" + + def compose(self) -> ComposeResult: + yield Input("Hello, world!") + + +async def test_focus_from_app_focus_does_not_select(): + """When an Input has focused and the *app* is blurred and then focused (e.g. by pressing + alt+tab or focusing another terminal pane), then the content of the Input should not be + fully selected when `Input.select_on_focus=True`. + """ + async with InputApp().run_test() as pilot: + input_widget = pilot.app.query_one(Input) + input_widget.focus() + input_widget.selection = Selection.cursor(0) + assert input_widget.selection == Selection.cursor(0) + pilot.app.post_message(events.AppBlur()) + await pilot.pause() + pilot.app.post_message(events.AppFocus()) + await pilot.pause() + assert input_widget.selection == Selection.cursor(0)