|
30 | 30 | from rich.style import Style |
31 | 31 |
|
32 | 32 | from . import constants, errors, events, messages |
| 33 | +from ._arrange import arrange |
33 | 34 | from ._callback import invoke |
34 | 35 | from ._compositor import Compositor, MapGeometry |
35 | 36 | from ._context import active_message_pump, visible_screen_stack |
| 37 | +from ._layout import DockArrangeResult |
36 | 38 | from ._path import CSSPathType, _css_path_type_as_list, _make_path_object_relative |
37 | 39 | from ._types import CallbackType |
38 | 40 | from .await_complete import AwaitComplete |
@@ -184,6 +186,11 @@ class Screen(Generic[ScreenResultType], Widget): |
184 | 186 |
|
185 | 187 | Should be a set of [`command.Provider`][textual.command.Provider] classes. |
186 | 188 | """ |
| 189 | + ALLOW_IN_MAXIMIZED_VIEW: ClassVar[str] = ".-textual-system,Footer" |
| 190 | + """A selector for the widgets (direct children of Screen) that are allowed in the maximized view (in addition to maximized widget).""" |
| 191 | + |
| 192 | + maximized: Reactive[Widget | None] = Reactive(None, layout=True) |
| 193 | + """The currently maximized widget, or `None` for no maximized widget.""" |
187 | 194 |
|
188 | 195 | BINDINGS = [ |
189 | 196 | Binding("tab", "app.focus_next", "Focus Next", show=False), |
@@ -287,6 +294,17 @@ def refresh_bindings(self) -> None: |
287 | 294 | self._bindings_updated = True |
288 | 295 | self.check_idle() |
289 | 296 |
|
| 297 | + def _watch_maximized( |
| 298 | + self, previously_maximized: Widget | None, maximized: Widget | None |
| 299 | + ) -> None: |
| 300 | + # The screen gets a `-maximized-view` class if there is a maximized widget |
| 301 | + # The widget gets a `-maximized` class if it is maximized |
| 302 | + self.set_class(maximized is not None, "-maximized-view") |
| 303 | + if previously_maximized is not None: |
| 304 | + previously_maximized.remove_class("-maximized") |
| 305 | + if maximized is not None: |
| 306 | + maximized.add_class("-maximized") |
| 307 | + |
290 | 308 | @property |
291 | 309 | def _binding_chain(self) -> list[tuple[DOMNode, BindingsMap]]: |
292 | 310 | """Binding chain from this screen.""" |
@@ -358,6 +376,34 @@ def active_bindings(self) -> dict[str, ActiveBinding]: |
358 | 376 |
|
359 | 377 | return bindings_map |
360 | 378 |
|
| 379 | + def _arrange(self, size: Size) -> DockArrangeResult: |
| 380 | + """Arrange children. |
| 381 | +
|
| 382 | + Args: |
| 383 | + size: Size of container. |
| 384 | +
|
| 385 | + Returns: |
| 386 | + Widget locations. |
| 387 | + """ |
| 388 | + # This is customized over the base class to allow for a widget to be maximized |
| 389 | + cache_key = (size, self._nodes._updates, self.maximized) |
| 390 | + cached_result = self._arrangement_cache.get(cache_key) |
| 391 | + if cached_result is not None: |
| 392 | + return cached_result |
| 393 | + |
| 394 | + arrangement = self._arrangement_cache[cache_key] = arrange( |
| 395 | + self, |
| 396 | + ( |
| 397 | + [self.maximized, *self.query_children(self.ALLOW_IN_MAXIMIZED_VIEW)] |
| 398 | + if self.maximized is not None |
| 399 | + else self._nodes |
| 400 | + ), |
| 401 | + size, |
| 402 | + self.screen.size, |
| 403 | + ) |
| 404 | + |
| 405 | + return arrangement |
| 406 | + |
361 | 407 | @property |
362 | 408 | def is_active(self) -> bool: |
363 | 409 | """Is the screen active (i.e. visible and top of the stack)?""" |
@@ -542,12 +588,19 @@ def _move_focus( |
542 | 588 | is not `None`, then it is guaranteed that the widget returned matches |
543 | 589 | the CSS selectors given in the argument. |
544 | 590 | """ |
| 591 | + |
545 | 592 | # TODO: This shouldn't be required |
546 | 593 | self._compositor._full_map_invalidated = True |
547 | 594 | if not isinstance(selector, str): |
548 | 595 | selector = selector.__name__ |
549 | 596 | selector_set = parse_selectors(selector) |
550 | 597 | focus_chain = self.focus_chain |
| 598 | + |
| 599 | + # If a widget is maximized we want to limit the focus chain to the visible widgets |
| 600 | + if self.maximized is not None: |
| 601 | + focusable = set(self.maximized.walk_children(with_self=True)) |
| 602 | + focus_chain = [widget for widget in focus_chain if widget in focusable] |
| 603 | + |
551 | 604 | filtered_focus_chain = ( |
552 | 605 | node for node in focus_chain if match(selector_set, node) |
553 | 606 | ) |
@@ -621,6 +674,42 @@ def focus_previous(self, selector: str | type[QueryType] = "*") -> Widget | None |
621 | 674 | """ |
622 | 675 | return self._move_focus(-1, selector) |
623 | 676 |
|
| 677 | + def maximize(self, widget: Widget, container: bool = True) -> None: |
| 678 | + """Maximize a widget, so it fills the screen. |
| 679 | +
|
| 680 | + Args: |
| 681 | + widget: Widget to maximize. |
| 682 | + container: If one of the widgets ancestors is a maximizeable widget, maximize that instead. |
| 683 | + """ |
| 684 | + if widget.allow_maximize: |
| 685 | + if container: |
| 686 | + # If we want to maximize the container, look up the dom to find a suitable widget |
| 687 | + for maximize_widget in widget.ancestors: |
| 688 | + if not isinstance(maximize_widget, Widget): |
| 689 | + break |
| 690 | + if maximize_widget.allow_maximize: |
| 691 | + self.maximized = maximize_widget |
| 692 | + return |
| 693 | + |
| 694 | + self.maximized = widget |
| 695 | + |
| 696 | + def minimize(self) -> None: |
| 697 | + """Restore any maximized widget to normal state.""" |
| 698 | + self.maximized = None |
| 699 | + if self.focused is not None: |
| 700 | + self.call_after_refresh( |
| 701 | + self.scroll_to_widget, self.focused, animate=False, center=True |
| 702 | + ) |
| 703 | + |
| 704 | + def action_maximize(self) -> None: |
| 705 | + """Action to maximize the currently focused widget.""" |
| 706 | + if self.focused is not None: |
| 707 | + self.maximize(self.focused) |
| 708 | + |
| 709 | + def action_minimize(self) -> None: |
| 710 | + """Action to minimize the currently maximized widget.""" |
| 711 | + self.minimize() |
| 712 | + |
624 | 713 | def _reset_focus( |
625 | 714 | self, widget: Widget, avoiding: list[Widget] | None = None |
626 | 715 | ) -> None: |
|
0 commit comments