mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
fix focus issues
This commit is contained in:
@@ -47,7 +47,7 @@ from .messages import CallbackType
|
||||
from .reactive import Reactive
|
||||
from .renderables.blank import Blank
|
||||
from .screen import Screen
|
||||
from .widget import Widget
|
||||
from .widget import Widget, _wait_for_mount
|
||||
|
||||
PLATFORM = platform.system()
|
||||
WINDOWS = PLATFORM == "Windows"
|
||||
@@ -1089,9 +1089,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
async def _on_compose(self) -> None:
|
||||
widgets = list(self.compose())
|
||||
self.mount_all(widgets)
|
||||
aws = [widget._mounted.wait() for widget in widgets]
|
||||
if aws:
|
||||
await asyncio.wait(aws)
|
||||
await _wait_for_mount(widgets)
|
||||
|
||||
def _on_idle(self) -> None:
|
||||
"""Perform actions when there are no messages in the queue."""
|
||||
@@ -1150,11 +1148,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
Args:
|
||||
widget (Widget): A Widget to unregister
|
||||
"""
|
||||
try:
|
||||
widget.screen._reset_focus(widget)
|
||||
except NoScreen:
|
||||
pass
|
||||
|
||||
widget.reset_focus()
|
||||
if isinstance(widget._parent, Widget):
|
||||
widget._parent.children._remove(widget)
|
||||
widget._detach()
|
||||
@@ -1402,16 +1396,17 @@ class App(Generic[ReturnType], DOMNode):
|
||||
async def _on_remove(self, event: events.Remove) -> None:
|
||||
widget = event.widget
|
||||
parent = widget.parent
|
||||
if parent is not None:
|
||||
parent.refresh(layout=True)
|
||||
|
||||
widget.reset_focus()
|
||||
|
||||
remove_widgets = widget.walk_children(
|
||||
Widget, with_self=True, method="depth", reverse=True
|
||||
)
|
||||
for child in remove_widgets:
|
||||
self._unregister(child)
|
||||
for child in remove_widgets:
|
||||
await child._close_messages()
|
||||
self._unregister(child)
|
||||
if parent is not None:
|
||||
parent.refresh(layout=True)
|
||||
|
||||
async def action_press(self, key: str) -> None:
|
||||
await self.press(key)
|
||||
|
||||
@@ -68,9 +68,13 @@ class ColorsApp(App):
|
||||
BINDINGS = [("d", "toggle_dark", "Toggle dark mode")]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Content(ColorButtons(), ColorsView())
|
||||
yield Content(ColorButtons())
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
content = self.query_one("Content", Content)
|
||||
content.mount(ColorsView())
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
self.query(ColorGroup).remove_class("-active")
|
||||
group = self.query_one(f"#group-{event.button.id}", ColorGroup)
|
||||
|
||||
@@ -73,7 +73,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
self._timers: WeakSet[Timer] = WeakSet()
|
||||
self._last_idle: float = time()
|
||||
self._max_idle: float | None = None
|
||||
self._mounted = asyncio.Event()
|
||||
self._mounted_event = asyncio.Event()
|
||||
|
||||
@property
|
||||
def task(self) -> Task:
|
||||
@@ -279,7 +279,6 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
await timer.stop()
|
||||
self._timers.clear()
|
||||
await self._message_queue.put(None)
|
||||
|
||||
if self._task is not None and asyncio.current_task() != self._task:
|
||||
# Ensure everything is closed before returning
|
||||
await self._task
|
||||
@@ -332,12 +331,12 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
except CancelledError:
|
||||
raise
|
||||
except Exception as error:
|
||||
self._mounted.set()
|
||||
self._mounted_event.set()
|
||||
self.app._handle_exception(error)
|
||||
break
|
||||
finally:
|
||||
if isinstance(message, events.Mount):
|
||||
self._mounted.set()
|
||||
self._mounted_event.set()
|
||||
self._message_queue.task_done()
|
||||
current_time = time()
|
||||
if self._message_queue.empty() or (
|
||||
|
||||
@@ -118,16 +118,15 @@ class Reactive(Generic[ReactiveType]):
|
||||
def __get__(self, obj: Reactable, obj_type: type[object]) -> ReactiveType:
|
||||
if not hasattr(obj, self.internal_name):
|
||||
init_name = f"_init_{self.name}"
|
||||
if hasattr(obj, init_name):
|
||||
default = getattr(obj, init_name)
|
||||
default_value = default() if callable(default) else default
|
||||
setattr(obj, self.name, default_value)
|
||||
return default_value
|
||||
default = getattr(obj, init_name)
|
||||
default_value = default() if callable(default) else default
|
||||
setattr(obj, self.internal_name, default_value)
|
||||
return default_value
|
||||
return getattr(obj, self.internal_name)
|
||||
|
||||
def __set__(self, obj: Reactable, value: ReactiveType) -> None:
|
||||
name = self.name
|
||||
current_value = getattr(obj, self.internal_name, None)
|
||||
current_value = getattr(obj, self.name)
|
||||
validate_function = getattr(obj, f"validate_{name}", None)
|
||||
first_set = getattr(obj, f"__first_set_{self.internal_name}", True)
|
||||
if callable(validate_function) and not first_set:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import Lock, wait
|
||||
from asyncio import Lock, wait, create_task
|
||||
from fractions import Fraction
|
||||
from itertools import islice
|
||||
from operator import attrgetter
|
||||
@@ -59,6 +59,13 @@ _JUSTIFY_MAP: dict[str, JustifyMethod] = {
|
||||
}
|
||||
|
||||
|
||||
async def _wait_for_mount(widgets: list[Widget]) -> None:
|
||||
"""Wait for widget to be mounted."""
|
||||
aws = [create_task(widget._mounted_event.wait()) for widget in widgets]
|
||||
if aws:
|
||||
await wait(aws)
|
||||
|
||||
|
||||
class _Styled:
|
||||
"""Apply a style to a renderable.
|
||||
|
||||
@@ -1812,8 +1819,15 @@ class Widget(DOMNode):
|
||||
scroll_visible (bool, optional): Scroll parent to make this widget
|
||||
visible. Defaults to True.
|
||||
"""
|
||||
|
||||
self.screen.set_focus(self, scroll_visible=scroll_visible)
|
||||
|
||||
def reset_focus(self) -> None:
|
||||
try:
|
||||
self.screen._reset_focus(self)
|
||||
except NoScreen:
|
||||
pass
|
||||
|
||||
def capture_mouse(self, capture: bool = True) -> None:
|
||||
"""Capture (or release) the mouse.
|
||||
|
||||
@@ -1860,16 +1874,10 @@ class Widget(DOMNode):
|
||||
async def _on_compose(self, event: events.Compose) -> None:
|
||||
widgets = list(self.compose())
|
||||
self.mount(*widgets)
|
||||
aws = [widget._mounted.wait() for widget in widgets]
|
||||
if aws:
|
||||
await wait(aws)
|
||||
Reactive.initialize_object(self)
|
||||
await _wait_for_mount(widgets)
|
||||
await self.post_message(events.Mount(self))
|
||||
|
||||
def _on_mount(self, event: events.Mount) -> None:
|
||||
# widgets = self.compose()
|
||||
# self.mount(*widgets)
|
||||
# Preset scrollbars if not automatic
|
||||
if self.styles.overflow_y == "scroll":
|
||||
self.show_vertical_scrollbar = True
|
||||
if self.styles.overflow_x == "scroll":
|
||||
@@ -1941,7 +1949,7 @@ class Widget(DOMNode):
|
||||
|
||||
def _on_hide(self, event: events.Hide) -> None:
|
||||
if self.has_focus:
|
||||
self.screen._reset_focus(self)
|
||||
self.reset_focus()
|
||||
|
||||
def _on_scroll_to_region(self, message: messages.ScrollToRegion) -> None:
|
||||
self.scroll_to_region(message.region, animate=True)
|
||||
|
||||
Reference in New Issue
Block a user