Skip to content

Commit

Permalink
App focus (#3767)
Browse files Browse the repository at this point in the history
* global focus

* change name to app focus

* app focus

* refactor

* changelog
  • Loading branch information
willmcgugan authored Nov 28, 2023
1 parent 2f86ee4 commit cb57d70
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Widgets can now have an ALLOW_CHILDREN (bool) classvar to disallow adding children to a widget https://github.com/Textualize/textual/pull/3758
- Added the ability to set the `label` property of a `Checkbox` https://github.com/Textualize/textual/pull/3765
- Added the ability to set the `label` property of a `RadioButton` https://github.com/Textualize/textual/pull/3765
- Added app focus/blur for textual-web https://github.com/Textualize/textual/pull/3767

### Changed

Expand Down
27 changes: 27 additions & 0 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@ class MyApp(App[None]):
self.app.dark = not self.app.dark # Toggle dark mode
```
"""
app_focus = Reactive(True, compute=False)
"""Indicates if the app has focus.
When run in the terminal, the app always has focus. When run in the web, the app will
get focus when the terminal widget has focus.
"""

def __init__(
self,
Expand Down Expand Up @@ -2672,6 +2678,8 @@ async def on_event(self, event: events.Event) -> None:
await super().on_event(event)

elif isinstance(event, events.InputEvent) and not event.is_forwarded:
if not self.app_focus and isinstance(event, (events.Key, events.MouseDown)):
self.app_focus = True
if isinstance(event, events.MouseEvent):
# Record current mouse position on App
self.mouse_position = Offset(event.x, event.y)
Expand Down Expand Up @@ -2819,6 +2827,16 @@ async def _on_resize(self, event: events.Resize) -> None:
for screen in self._background_screens:
screen.post_message(event)

async def _on_app_focus(self, event: events.AppFocus) -> None:
"""App has focus."""
# Required by textual-web to manage focus in a web page.
self.app_focus = True

async def _on_app_blur(self, event: events.AppBlur) -> None:
"""App has lost focus."""
# Required by textual-web to manage focus in a web page.
self.app_focus = False

def _detach_from_dom(self, widgets: list[Widget]) -> list[Widget]:
"""Detach a list of widgets from the DOM.
Expand Down Expand Up @@ -2976,6 +2994,15 @@ async def _prune_node(self, root: Widget) -> None:
await root._close_messages(wait=True)
self._unregister(root)

def _watch_app_focus(self, focus: bool) -> None:
"""Respond to changes in app focus."""
if focus:
focused = self.screen.focused
self.screen.set_focus(None)
self.screen.set_focus(focused)
else:
self.screen.set_focus(None)

async def action_check_bindings(self, key: str) -> None:
"""An [action](/guide/actions) to handle a key press using the binding system.
Expand Down
7 changes: 6 additions & 1 deletion src/textual/drivers/web_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ def do_exit() -> None:
self._enable_bracketed_paste()
self.flush()
self._key_thread.start()
self._app.post_message(events.AppBlur())

def disable_input(self) -> None:
"""Disable further input."""
Expand Down Expand Up @@ -188,7 +189,7 @@ def _on_meta(self, packet_type: str, payload: bytes) -> None:
payload: Meta payload (JSON encoded as bytes).
"""
payload_map = json.loads(payload)
_type = payload_map.get("type")
_type = payload_map.get("type", {})
if isinstance(payload_map, dict):
self.on_meta(_type, payload_map)

Expand All @@ -203,6 +204,10 @@ def on_meta(self, packet_type: str, payload: dict) -> None:
self._size = (payload["width"], payload["height"])
size = Size(*self._size)
self._app.post_message(events.Resize(size, size))
elif packet_type == "focus":
self._app.post_message(events.AppFocus())
elif packet_type == "blur":
self._app.post_message(events.AppBlur())
elif packet_type == "quit":
self._app.post_message(messages.ExitApp())
elif packet_type == "exit":
Expand Down
20 changes: 20 additions & 0 deletions src/textual/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,26 @@ class Blur(Event, bubble=False):
"""


class AppFocus(Event, bubble=False):
"""Sent when the app has focus.
Used by textual-web.
- [ ] Bubbles
- [ ] Verbose
"""


class AppBlur(Event, bubble=False):
"""Sent when the app loses focus.
Used by textual-web.
- [ ] Bubbles
- [ ] Verbose
"""


@dataclass
class DescendantFocus(Event, bubble=True, verbose=True):
"""Sent when a child widget is focussed.
Expand Down
16 changes: 10 additions & 6 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,12 +819,16 @@ def _on_screen_resume(self) -> None:
size = self.app.size
self._refresh_layout(size, full=True)
self.refresh()
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
# Only auto-focus when the app has focus (textual-web only)
if self.app.app_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

def _on_screen_suspend(self) -> None:
"""Screen has suspended."""
Expand Down

0 comments on commit cb57d70

Please sign in to comment.