Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't apply Input "select on focus" behaviour when app is focused #5379

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,7 +24,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

- Breaking change: Change default quit key to `ctrl+q` https://github.com/Textualize/textual/pull/5352
Expand Down
4 changes: 3 additions & 1 deletion src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/textual/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,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"):
Expand Down
16 changes: 15 additions & 1 deletion src/textual/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -722,8 +722,22 @@ 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__()

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).
Expand Down
12 changes: 10 additions & 2 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/textual/widgets/_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand Down
30 changes: 30 additions & 0 deletions tests/input/test_select_on_focus.py
Original file line number Diff line number Diff line change
@@ -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)
Loading