Skip to content

Commit 39ddb85

Browse files
authored
Merge pull request #5379 from Textualize/command-palette-dont-select-on-focus
Don't apply Input "select on focus" behaviour when app is focused
2 parents 3cd079a + 4953c3b commit 39ddb85

File tree

7 files changed

+72
-7
lines changed

7 files changed

+72
-7
lines changed

CHANGELOG.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## Unreleased
9+
10+
### Added
11+
12+
- 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
13+
14+
### Changed
15+
16+
- 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
17+
818
## [1.0.0] - 2024-12-12
919

1020
### Added
@@ -14,7 +24,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1424
- Added `system` boolean to Binding, which hides the binding from the help panel https://github.com/Textualize/textual/pull/5352
1525
- Added support for double/triple/etc clicks via `chain` attribute on `Click` events https://github.com/Textualize/textual/pull/5369
1626
- Added `times` parameter to `Pilot.click` method, for simulating rapid clicks https://github.com/Textualize/textual/pull/5369
17-
27+
- Text can now be select using mouse or keyboard in the Input widget https://github.com/Textualize/textual/pull/5340
28+
1829
### Changed
1930

2031
- Breaking change: Change default quit key to `ctrl+q` https://github.com/Textualize/textual/pull/5352

src/textual/app.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -4052,7 +4052,9 @@ def _watch_app_focus(self, focus: bool) -> None:
40524052
# ...settle focus back on that widget.
40534053
# Don't scroll the newly focused widget, as this can be quite jarring
40544054
self.screen.set_focus(
4055-
self._last_focused_on_app_blur, scroll_visible=False
4055+
self._last_focused_on_app_blur,
4056+
scroll_visible=False,
4057+
from_app_focus=True,
40564058
)
40574059
except NoScreen:
40584060
pass

src/textual/command.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,7 @@ def compose(self) -> ComposeResult:
776776
with Vertical(id="--container"):
777777
with Horizontal(id="--input"):
778778
yield SearchIcon()
779-
yield CommandInput(placeholder=self._placeholder)
779+
yield CommandInput(placeholder=self._placeholder, select_on_focus=False)
780780
if not self.run_on_select:
781781
yield Button("\u25b6")
782782
with Vertical(id="--results"):

src/textual/events.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
from dataclasses import dataclass
1717
from pathlib import Path
1818
from typing import TYPE_CHECKING, Type, TypeVar
19-
from typing_extensions import Self
2019

2120
import rich.repr
2221
from rich.style import Style
22+
from typing_extensions import Self
2323

2424
from textual._types import CallbackType
2525
from textual.geometry import Offset, Size
@@ -722,8 +722,22 @@ class Focus(Event, bubble=False):
722722
723723
- [ ] Bubbles
724724
- [ ] Verbose
725+
726+
Args:
727+
from_app_focus: True if this focus event has been sent because the app itself has
728+
regained focus (via an AppFocus event). False if the focus came from within
729+
the Textual app (e.g. via the user pressing tab or a programmatic setting
730+
of the focused widget).
725731
"""
726732

733+
def __init__(self, from_app_focus: bool = False) -> None:
734+
self.from_app_focus = from_app_focus
735+
super().__init__()
736+
737+
def __rich_repr__(self) -> rich.repr.Result:
738+
yield from super().__rich_repr__()
739+
yield "from_app_focus", self.from_app_focus
740+
727741

728742
class Blur(Event, bubble=False):
729743
"""Sent when a widget is blurred (un-focussed).

src/textual/screen.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -869,12 +869,20 @@ def _update_focus_styles(
869869
[widget for widget in widgets if widget._has_focus_within], animate=True
870870
)
871871

872-
def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None:
872+
def set_focus(
873+
self,
874+
widget: Widget | None,
875+
scroll_visible: bool = True,
876+
from_app_focus: bool = False,
877+
) -> None:
873878
"""Focus (or un-focus) a widget. A focused widget will receive key events first.
874879
875880
Args:
876881
widget: Widget to focus, or None to un-focus.
877882
scroll_visible: Scroll widget in to view.
883+
from_app_focus: True if this focus is due to the app itself having regained
884+
focus. False if the focus is being set because a widget within the app
885+
regained focus.
878886
"""
879887
if widget is self.focused:
880888
# Widget is already focused
@@ -899,7 +907,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None:
899907
# Change focus
900908
self.focused = widget
901909
# Send focus event
902-
widget.post_message(events.Focus())
910+
widget.post_message(events.Focus(from_app_focus=from_app_focus))
903911
focused = widget
904912

905913
if scroll_visible:

src/textual/widgets/_input.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ def _on_blur(self, event: Blur) -> None:
641641

642642
def _on_focus(self, event: Focus) -> None:
643643
self._restart_blink()
644-
if self.select_on_focus:
644+
if self.select_on_focus and not event.from_app_focus:
645645
self.selection = Selection(0, len(self.value))
646646
self.app.cursor_position = self.cursor_screen_offset
647647
self._suggestion = ""

tests/input/test_select_on_focus.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""The standard path of selecting text on focus is well covered by snapshot tests."""
2+
3+
from textual import events
4+
from textual.app import App, ComposeResult
5+
from textual.widgets import Input
6+
from textual.widgets.input import Selection
7+
8+
9+
class InputApp(App[None]):
10+
"""An app with an input widget."""
11+
12+
def compose(self) -> ComposeResult:
13+
yield Input("Hello, world!")
14+
15+
16+
async def test_focus_from_app_focus_does_not_select():
17+
"""When an Input has focused and the *app* is blurred and then focused (e.g. by pressing
18+
alt+tab or focusing another terminal pane), then the content of the Input should not be
19+
fully selected when `Input.select_on_focus=True`.
20+
"""
21+
async with InputApp().run_test() as pilot:
22+
input_widget = pilot.app.query_one(Input)
23+
input_widget.focus()
24+
input_widget.selection = Selection.cursor(0)
25+
assert input_widget.selection == Selection.cursor(0)
26+
pilot.app.post_message(events.AppBlur())
27+
await pilot.pause()
28+
pilot.app.post_message(events.AppFocus())
29+
await pilot.pause()
30+
assert input_widget.selection == Selection.cursor(0)

0 commit comments

Comments
 (0)