From c5f4e3326da1ef7d830331d41e2fe57712f63b12 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 24 Aug 2024 20:43:10 +0100 Subject: [PATCH 01/15] maximize windows --- src/textual/app.py | 23 ++++++++++++++ src/textual/demo.py | 4 +-- src/textual/demo.tcss | 3 ++ src/textual/dom.py | 5 +++ src/textual/screen.py | 72 ++++++++++++++++++++++++++++++++++++++++++- src/textual/widget.py | 12 +++++++- 6 files changed, 115 insertions(+), 4 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 664a93052a..3135d23c85 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -306,6 +306,13 @@ class App(Generic[ReturnType], DOMNode): App { background: $background; color: $text; + Screen.-maximized { + padding: 1 2; + layout: vertical !important; + hatch: right $panel; + overflow-y: auto !important; + align: center middle; + } } *:disabled:can-focus { opacity: 0.7; @@ -992,6 +999,17 @@ def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]: self.action_show_help_panel, ) + if screen.maximized is not None: + yield SystemCommand( + "Minimize", + "Minimize the widget and restore to normal size", + screen.action_minimize, + ) + elif screen.focused is not None: + yield SystemCommand( + "Maximize", "Maximize the focused widget", screen.action_maximize + ) + # Don't save screenshot for web drivers until we have the deliver_file in place if self._driver.__class__.__name__ in {"LinuxDriver", "WindowsDriver"}: @@ -3441,6 +3459,11 @@ async def _on_layout(self, message: messages.Layout) -> None: message.stop() async def _on_key(self, event: events.Key) -> None: + # Special case for maximized widgets + # If something is maximized, then escape should minimize + if self.screen.maximized is not None and event.key == "escape": + self.screen.minimize() + return if not (await self._check_bindings(event.key)): await dispatch_key(self, event) diff --git a/src/textual/demo.py b/src/textual/demo.py index 5d2a67d74e..de100898d3 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -189,7 +189,7 @@ def on_switch_changed(self, event: Switch.Changed) -> None: self.app.dark = event.value -class Welcome(Container): +class Welcome(Container, allow_maximize=True): def compose(self) -> ComposeResult: yield Static(Markdown(WELCOME_MD)) yield Button("Start", variant="success") @@ -255,7 +255,7 @@ def on_click(self) -> None: app.add_note(f"Scrolling to [b]{self.reveal}[/b]") -class LoginForm(Container): +class LoginForm(Container, allow_maximize=True): def compose(self) -> ComposeResult: yield Static("Username", classes="label") yield Input(placeholder="Username") diff --git a/src/textual/demo.tcss b/src/textual/demo.tcss index fc5f45b23f..b584676ef5 100644 --- a/src/textual/demo.tcss +++ b/src/textual/demo.tcss @@ -8,6 +8,9 @@ Screen { &:inline { height: 50vh; } + &.-maximized { + overflow: auto; + } } diff --git a/src/textual/dom.py b/src/textual/dom.py index fe00a09933..28df8fc27e 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -157,6 +157,9 @@ class DOMNode(MessagePump): # True to inherit bindings from base class _inherit_bindings: ClassVar[bool] = True + # True if the widget may be maximized + _allow_maximize: ClassVar[bool] = False + # List of names of base classes that inherit CSS _css_type_names: ClassVar[frozenset[str]] = frozenset() @@ -495,6 +498,7 @@ def __init_subclass__( inherit_css: bool = True, inherit_bindings: bool = True, inherit_component_classes: bool = True, + allow_maximize: bool = False, ) -> None: super().__init_subclass__() @@ -511,6 +515,7 @@ def __init_subclass__( cls._inherit_css = inherit_css cls._inherit_bindings = inherit_bindings cls._inherit_component_classes = inherit_component_classes + cls._allow_maximize = allow_maximize css_type_names: set[str] = set() bases = cls._css_bases(cls) cls._css_type_name = bases[0].__name__ diff --git a/src/textual/screen.py b/src/textual/screen.py index 5ee75c05d2..b6e71aedb0 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -30,9 +30,11 @@ from rich.style import Style from . import constants, errors, events, messages +from ._arrange import arrange from ._callback import invoke from ._compositor import Compositor, MapGeometry from ._context import active_message_pump, visible_screen_stack +from ._layout import DockArrangeResult from ._path import CSSPathType, _css_path_type_as_list, _make_path_object_relative from ._types import CallbackType from .await_complete import AwaitComplete @@ -185,6 +187,9 @@ class Screen(Generic[ScreenResultType], Widget): Should be a set of [`command.Provider`][textual.command.Provider] classes. """ + maximized: Reactive[Widget | None] = Reactive(None, layout=True) + """The currently maximized widget, or `None` for no maximized widget.""" + BINDINGS = [ Binding("tab", "app.focus_next", "Focus Next", show=False), Binding("shift+tab", "app.focus_previous", "Focus Previous", show=False), @@ -276,7 +281,7 @@ def layers(self) -> tuple[str, ...]: extras.append("_tooltips") return (*super().layers, *extras) - def _watch_focused(self): + def _watch_focused(self, focused: Widget): self.refresh_bindings() def _watch_stack_updates(self): @@ -287,6 +292,9 @@ def refresh_bindings(self) -> None: self._bindings_updated = True self.check_idle() + def watch_maximized(self, maximized: Widget | None) -> None: + self.set_class(maximized is not None, "-maximized") + @property def _binding_chain(self) -> list[tuple[DOMNode, BindingsMap]]: """Binding chain from this screen.""" @@ -358,6 +366,29 @@ def active_bindings(self) -> dict[str, ActiveBinding]: return bindings_map + def _arrange(self, size: Size) -> DockArrangeResult: + """Arrange children. + + Args: + size: Size of container. + + Returns: + Widget locations. + """ + cache_key = (size, self._nodes._updates, self.maximized) + cached_result = self._arrangement_cache.get(cache_key) + if cached_result is not None: + return cached_result + + arrangement = self._arrangement_cache[cache_key] = arrange( + self, + [self.maximized] if self.maximized is not None else self._nodes, + size, + self.screen.size, + ) + + return arrangement + @property def is_active(self) -> bool: """Is the screen active (i.e. visible and top of the stack)?""" @@ -542,6 +573,7 @@ def _move_focus( is not `None`, then it is guaranteed that the widget returned matches the CSS selectors given in the argument. """ + # TODO: This shouldn't be required self._compositor._full_map_invalidated = True if not isinstance(selector, str): @@ -621,6 +653,44 @@ def focus_previous(self, selector: str | type[QueryType] = "*") -> Widget | None """ return self._move_focus(-1, selector) + def action_toggle_maximize(self) -> None: + if self.maximized is None: + if self.focused is not None: + self.maximize(self.focused) + else: + self.minimize() + + def maximize(self, widget: Widget) -> None: + """Maximize a widget, so it fills the screen. + + Args: + widget: Widget to maximize. + """ + for maximize_widget in widget.ancestors: + if not isinstance(maximize_widget, Widget): + break + if maximize_widget._allow_maximize: + widget = maximize_widget + break + self.maximized = widget + + def minimize(self) -> None: + """Restore any maximized widget to normal state.""" + current_maximized = self.maximized + self.maximized = None + if current_maximized is not None and self.focused is not None: + self.refresh(layout=True) + self.call_after_refresh(self.focused.scroll_visible, animate=False) + + def action_maximize(self) -> None: + """Action to maximize the currently focused widget.""" + if self.focused is not None: + self.maximize(self.focused) + + def action_minimize(self) -> None: + """Action to minimize the currently focused widget.""" + self.minimize() + def _reset_focus( self, widget: Widget, avoiding: list[Widget] | None = None ) -> None: diff --git a/src/textual/widget.py b/src/textual/widget.py index 868f39b6f2..275af7e37e 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -558,6 +558,14 @@ def is_mouse_over(self) -> bool: return True return False + @property + def is_maximized(self) -> bool: + """Is this widget maximized?""" + try: + return self.screen.maximized is self + except NoScreen: + return False + def anchor(self, *, animate: bool = False) -> None: """Anchor the widget, which scrolls it into view (like [scroll_visible][textual.widget.Widget.scroll_visible]), but also keeps it in view if the widget's size changes, or the size of its container changes. @@ -3041,16 +3049,18 @@ def __init_subclass__( can_focus_children: bool | None = None, inherit_css: bool = True, inherit_bindings: bool = True, + allow_maximize: bool = False, ) -> None: name = cls.__name__ if not name[0].isupper() and not name.startswith("_"): raise BadWidgetName( - f"Widget subclass {name!r} should be capitalised or start with '_'." + f"Widget subclass {name!r} should be capitalized or start with '_'." ) super().__init_subclass__( inherit_css=inherit_css, inherit_bindings=inherit_bindings, + allow_maximize=allow_maximize, ) base = cls.__mro__[0] if issubclass(base, Widget): From ab4ac473a820ce64aa3f35eddb2787163b243fab Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 24 Aug 2024 20:45:27 +0100 Subject: [PATCH 02/15] no toggle --- src/textual/screen.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index b6e71aedb0..5b000090d9 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -653,13 +653,6 @@ def focus_previous(self, selector: str | type[QueryType] = "*") -> Widget | None """ return self._move_focus(-1, selector) - def action_toggle_maximize(self) -> None: - if self.maximized is None: - if self.focused is not None: - self.maximize(self.focused) - else: - self.minimize() - def maximize(self, widget: Widget) -> None: """Maximize a widget, so it fills the screen. From db51116ae1d27cb5db22c5b5b55dd51098adca34 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 24 Aug 2024 20:48:41 +0100 Subject: [PATCH 03/15] changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d7e5489f..ddad51b49a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ 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 Maximize and Minimize system commands. +- Added `Screen.maximize`, `Screen.minimize`, `Screen.action_maximize`, `Screen.action_minimize`. + ## [0.77.0] - 2024-08-22 ### Added From b55341d3145808ee7feb18c6394f5c3b59711461 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 24 Aug 2024 20:56:22 +0100 Subject: [PATCH 04/15] maximized focus --- src/textual/screen.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/textual/screen.py b/src/textual/screen.py index 5b000090d9..86d64f1e2a 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -580,6 +580,11 @@ def _move_focus( selector = selector.__name__ selector_set = parse_selectors(selector) focus_chain = self.focus_chain + + if self.maximized is not None: + focusable = set(self.maximized.walk_children(with_self=True)) + focus_chain = [widget for widget in focus_chain if widget in focusable] + filtered_focus_chain = ( node for node in focus_chain if match(selector_set, node) ) From 8da07d67d1e7365f530d7117a52df6b1199e230d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 24 Aug 2024 21:04:48 +0100 Subject: [PATCH 05/15] allow textual system --- src/textual/screen.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index 86d64f1e2a..0dff71e39d 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -375,6 +375,7 @@ def _arrange(self, size: Size) -> DockArrangeResult: Returns: Widget locations. """ + # This is customized over the base class to allow for a widget to be maximized cache_key = (size, self._nodes._updates, self.maximized) cached_result = self._arrangement_cache.get(cache_key) if cached_result is not None: @@ -382,7 +383,11 @@ def _arrange(self, size: Size) -> DockArrangeResult: arrangement = self._arrangement_cache[cache_key] = arrange( self, - [self.maximized] if self.maximized is not None else self._nodes, + ( + [self.maximized, *self.query_children(".-textual-system")] + if self.maximized is not None + else self._nodes + ), size, self.screen.size, ) From 73dc101d009dfbb64a15f9169d90d6cb6bd81787 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 25 Aug 2024 09:52:53 +0100 Subject: [PATCH 06/15] maximize logic --- src/textual/app.py | 6 ++++-- src/textual/containers.py | 2 ++ src/textual/demo.py | 10 ++++++--- src/textual/demo.tcss | 2 +- src/textual/dom.py | 5 ----- src/textual/screen.py | 33 ++++++++++++++++++++---------- src/textual/scroll_view.py | 2 ++ src/textual/widget.py | 20 ++++++++++++++++-- src/textual/widgets/_data_table.py | 2 +- 9 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 3135d23c85..e1f86c4673 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -306,12 +306,14 @@ class App(Generic[ReturnType], DOMNode): App { background: $background; color: $text; - Screen.-maximized { - padding: 1 2; + Screen.-maximized-view { layout: vertical !important; hatch: right $panel; overflow-y: auto !important; align: center middle; + .-maximized { + dock: initial !important; + } } } *:disabled:can-focus { diff --git a/src/textual/containers.py b/src/textual/containers.py index 91b64b6a1a..0b4cd58456 100644 --- a/src/textual/containers.py +++ b/src/textual/containers.py @@ -29,6 +29,8 @@ class Container(Widget): class ScrollableContainer(Widget, can_focus=True, inherit_bindings=False): """A scrollable container with vertical layout, and auto scrollbars on both axis.""" + ALLOW_MAXIMIZE = False + DEFAULT_CSS = """ ScrollableContainer { width: 1fr; diff --git a/src/textual/demo.py b/src/textual/demo.py index de100898d3..9a5acf176d 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -162,7 +162,7 @@ } } } -} +} """ @@ -189,7 +189,9 @@ def on_switch_changed(self, event: Switch.Changed) -> None: self.app.dark = event.value -class Welcome(Container, allow_maximize=True): +class Welcome(Container): + ALLOW_MAXIMIZE = True + def compose(self) -> ComposeResult: yield Static(Markdown(WELCOME_MD)) yield Button("Start", variant="success") @@ -255,7 +257,9 @@ def on_click(self) -> None: app.add_note(f"Scrolling to [b]{self.reveal}[/b]") -class LoginForm(Container, allow_maximize=True): +class LoginForm(Container): + ALLOW_MAXIMIZE = True + def compose(self) -> ComposeResult: yield Static("Username", classes="label") yield Input(placeholder="Username") diff --git a/src/textual/demo.tcss b/src/textual/demo.tcss index b584676ef5..4febd734be 100644 --- a/src/textual/demo.tcss +++ b/src/textual/demo.tcss @@ -8,7 +8,7 @@ Screen { &:inline { height: 50vh; } - &.-maximized { + &.-maximized-view { overflow: auto; } } diff --git a/src/textual/dom.py b/src/textual/dom.py index 28df8fc27e..fe00a09933 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -157,9 +157,6 @@ class DOMNode(MessagePump): # True to inherit bindings from base class _inherit_bindings: ClassVar[bool] = True - # True if the widget may be maximized - _allow_maximize: ClassVar[bool] = False - # List of names of base classes that inherit CSS _css_type_names: ClassVar[frozenset[str]] = frozenset() @@ -498,7 +495,6 @@ def __init_subclass__( inherit_css: bool = True, inherit_bindings: bool = True, inherit_component_classes: bool = True, - allow_maximize: bool = False, ) -> None: super().__init_subclass__() @@ -515,7 +511,6 @@ def __init_subclass__( cls._inherit_css = inherit_css cls._inherit_bindings = inherit_bindings cls._inherit_component_classes = inherit_component_classes - cls._allow_maximize = allow_maximize css_type_names: set[str] = set() bases = cls._css_bases(cls) cls._css_type_name = bases[0].__name__ diff --git a/src/textual/screen.py b/src/textual/screen.py index 0dff71e39d..f96c3e3431 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -186,6 +186,8 @@ class Screen(Generic[ScreenResultType], Widget): Should be a set of [`command.Provider`][textual.command.Provider] classes. """ + ALLOW_IN_MAXIMIZED_VIEW: ClassVar[str] = ".-textual-system,Footer" + """A selector for the widgets (direct children of Screen) that are allowed in the maximized view (in addition to maximized widget).""" maximized: Reactive[Widget | None] = Reactive(None, layout=True) """The currently maximized widget, or `None` for no maximized widget.""" @@ -292,8 +294,14 @@ def refresh_bindings(self) -> None: self._bindings_updated = True self.check_idle() - def watch_maximized(self, maximized: Widget | None) -> None: - self.set_class(maximized is not None, "-maximized") + def watch_maximized( + self, previously_maximized: Widget | None, maximized: Widget | None + ) -> None: + self.set_class(maximized is not None, "-maximized-view") + if previously_maximized is not None: + previously_maximized.remove_class("-maximized") + if maximized is not None: + maximized.add_class("-maximized") @property def _binding_chain(self) -> list[tuple[DOMNode, BindingsMap]]: @@ -384,7 +392,7 @@ def _arrange(self, size: Size) -> DockArrangeResult: arrangement = self._arrangement_cache[cache_key] = arrange( self, ( - [self.maximized, *self.query_children(".-textual-system")] + [self.maximized, *self.query_children(self.ALLOW_IN_MAXIMIZED_VIEW)] if self.maximized is not None else self._nodes ), @@ -663,19 +671,22 @@ def focus_previous(self, selector: str | type[QueryType] = "*") -> Widget | None """ return self._move_focus(-1, selector) - def maximize(self, widget: Widget) -> None: + def maximize(self, widget: Widget, container: bool = True) -> None: """Maximize a widget, so it fills the screen. Args: widget: Widget to maximize. + container: Maximize container if possible. """ - for maximize_widget in widget.ancestors: - if not isinstance(maximize_widget, Widget): - break - if maximize_widget._allow_maximize: - widget = maximize_widget - break - self.maximized = widget + if container: + for maximize_widget in widget.ancestors: + if not isinstance(maximize_widget, Widget): + break + if maximize_widget.allow_maximize: + self.maximized = maximize_widget + return + if widget.allow_maximize: + self.maximized = widget def minimize(self) -> None: """Restore any maximized widget to normal state.""" diff --git a/src/textual/scroll_view.py b/src/textual/scroll_view.py index 88493f3ed1..52b5e8739b 100644 --- a/src/textual/scroll_view.py +++ b/src/textual/scroll_view.py @@ -18,6 +18,8 @@ class ScrollView(ScrollableContainer): on the compositor to render children). """ + ALLOW_MAXIMIZE = True + DEFAULT_CSS = """ ScrollView { overflow-y: auto; diff --git a/src/textual/widget.py b/src/textual/widget.py index 275af7e37e..36e8616ae8 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -304,6 +304,15 @@ class Widget(DOMNode): BORDER_SUBTITLE: ClassVar[str] = "" """Initial value for border_subtitle attribute.""" + ALLOW_MAXIMIZE: ClassVar[bool | None] = None + """Defines default logic to allow the widget to be maximized. + + - `None` Use default behavior (Focusable widgets may be maximized) + - `False` Do not allow widget to be maximized. + - `True` Allow widget to be maximized. + + """ + can_focus: bool = False """Widget may receive focus.""" can_focus_children: bool = True @@ -514,6 +523,15 @@ def _allow_scroll(self) -> bool: self.allow_horizontal_scroll or self.allow_vertical_scroll ) + @property + def allow_maximize(self) -> bool: + """Check if the widget may be maximized. + + Returns: + `True` if the widget may be maximized, or `False` if it should not be maximized. + """ + return self.can_focus if self.ALLOW_MAXIMIZE is None else self.ALLOW_MAXIMIZE + @property def offset(self) -> Offset: """Widget offset from origin. @@ -3049,7 +3067,6 @@ def __init_subclass__( can_focus_children: bool | None = None, inherit_css: bool = True, inherit_bindings: bool = True, - allow_maximize: bool = False, ) -> None: name = cls.__name__ if not name[0].isupper() and not name.startswith("_"): @@ -3060,7 +3077,6 @@ def __init_subclass__( super().__init_subclass__( inherit_css=inherit_css, inherit_bindings=inherit_bindings, - allow_maximize=allow_maximize, ) base = cls.__mro__[0] if issubclass(base, Widget): diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 275f8eda7d..2cf1757ea4 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -256,7 +256,7 @@ class RowRenderables(NamedTuple): cells: list[RenderableType] -class DataTable(ScrollView, Generic[CellType], can_focus=True): +class DataTable(ScrollView, Generic[CellType]): """A tabular widget that contains data.""" BINDINGS: ClassVar[list[BindingType]] = [ From efd0982a30458799619e3417642f592bf0344b3e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 25 Aug 2024 09:57:34 +0100 Subject: [PATCH 07/15] command logic --- src/textual/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/app.py b/src/textual/app.py index e1f86c4673..25b3f4ead1 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1007,7 +1007,7 @@ def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]: "Minimize the widget and restore to normal size", screen.action_minimize, ) - elif screen.focused is not None: + elif screen.focused is not None and screen.focused.allow_maximize: yield SystemCommand( "Maximize", "Maximize the focused widget", screen.action_maximize ) From e1fde156fd70cfbc36de01c85b3776ae2ea13921 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 25 Aug 2024 11:37:53 +0100 Subject: [PATCH 08/15] simplify and comments --- src/textual/screen.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index f96c3e3431..cf35736fb0 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -594,6 +594,7 @@ def _move_focus( selector_set = parse_selectors(selector) focus_chain = self.focus_chain + # If a widget is maximized we want to limit the focus chain to the visible widgets if self.maximized is not None: focusable = set(self.maximized.walk_children(with_self=True)) focus_chain = [widget for widget in focus_chain if widget in focusable] @@ -679,6 +680,7 @@ def maximize(self, widget: Widget, container: bool = True) -> None: container: Maximize container if possible. """ if container: + # If we want to maximize the container, look up the dom to find a suitable widget for maximize_widget in widget.ancestors: if not isinstance(maximize_widget, Widget): break @@ -690,11 +692,7 @@ def maximize(self, widget: Widget, container: bool = True) -> None: def minimize(self) -> None: """Restore any maximized widget to normal state.""" - current_maximized = self.maximized self.maximized = None - if current_maximized is not None and self.focused is not None: - self.refresh(layout=True) - self.call_after_refresh(self.focused.scroll_visible, animate=False) def action_maximize(self) -> None: """Action to maximize the currently focused widget.""" From a99b145ec7eadf903fe5e89286695344db8c5dde Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 25 Aug 2024 11:51:04 +0100 Subject: [PATCH 09/15] changelog and snapshots --- CHANGELOG.md | 5 +- src/textual/containers.py | 2 + src/textual/screen.py | 6 +- src/textual/widgets/_data_table.py | 2 +- .../test_snapshots/test_maximize.svg | 157 +++++++++++++++++ .../test_maximize_container.svg | 160 ++++++++++++++++++ .../test_snapshots/test_system_commands.svg | 156 ++++++++--------- tests/snapshot_tests/test_snapshots.py | 37 +++- 8 files changed, 441 insertions(+), 84 deletions(-) create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_maximize.svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_maximize_container.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index ddad51b49a..881e0ff386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added -- Added Maximize and Minimize system commands. -- Added `Screen.maximize`, `Screen.minimize`, `Screen.action_maximize`, `Screen.action_minimize`. +- Added Maximize and Minimize system commands. https://github.com/Textualize/textual/pull/4931 +- Added `Screen.maximize`, `Screen.minimize`, `Screen.action_maximize`, `Screen.action_minimize`, `Widget.is_maximized`, `Widget.allow_maximize`. https://github.com/Textualize/textual/pull/4931 +- Added `Widget.ALLOW_MAXIMIZE`, `Screen.ALLOW_IN_MAXIMIZED_VIEW` classvars https://github.com/Textualize/textual/pull/4931 ## [0.77.0] - 2024-08-22 diff --git a/src/textual/containers.py b/src/textual/containers.py index 0b4cd58456..19ee42dc87 100644 --- a/src/textual/containers.py +++ b/src/textual/containers.py @@ -29,6 +29,8 @@ class Container(Widget): class ScrollableContainer(Widget, can_focus=True, inherit_bindings=False): """A scrollable container with vertical layout, and auto scrollbars on both axis.""" + # We don't typically want to maximize scrollable containers, + # since the user can easily navigate the contents ALLOW_MAXIMIZE = False DEFAULT_CSS = """ diff --git a/src/textual/screen.py b/src/textual/screen.py index cf35736fb0..0e40e22267 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -283,7 +283,7 @@ def layers(self) -> tuple[str, ...]: extras.append("_tooltips") return (*super().layers, *extras) - def _watch_focused(self, focused: Widget): + def _watch_focused(self): self.refresh_bindings() def _watch_stack_updates(self): @@ -294,9 +294,11 @@ def refresh_bindings(self) -> None: self._bindings_updated = True self.check_idle() - def watch_maximized( + def _watch_maximized( self, previously_maximized: Widget | None, maximized: Widget | None ) -> None: + # The screen gets a `-maximized-view` class if there is a maximized widget + # The widget gets a `-maximized` class if it is maximized self.set_class(maximized is not None, "-maximized-view") if previously_maximized is not None: previously_maximized.remove_class("-maximized") diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 2cf1757ea4..275f8eda7d 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -256,7 +256,7 @@ class RowRenderables(NamedTuple): cells: list[RenderableType] -class DataTable(ScrollView, Generic[CellType]): +class DataTable(ScrollView, Generic[CellType], can_focus=True): """A tabular widget that contains data.""" BINDINGS: ClassVar[list[BindingType]] = [ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_maximize.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_maximize.svg new file mode 100644 index 0000000000..8a6cdb4bb6 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_maximize.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MaximizeApp + + + + + + + + + + ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ Hello ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + m maximize focused widget ^p palette + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_maximize_container.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_maximize_container.svg new file mode 100644 index 0000000000..33484d983b --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_maximize_container.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MaximizeApp + + + + + + + + + + ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱┌──────────────────────────────────────┐╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ Hello ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ World ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱└──────────────────────────────────────┘╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ + m maximize focused widget ^p palette + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg index 3968eb38ff..c4610b14b6 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg @@ -19,166 +19,166 @@ font-weight: 700; } - .terminal-2261778856-matrix { + .terminal-2036187654-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2261778856-title { + .terminal-2036187654-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2261778856-r1 { fill: #161616 } -.terminal-2261778856-r2 { fill: #0b3a5f } -.terminal-2261778856-r3 { fill: #c5c8c6 } -.terminal-2261778856-r4 { fill: #e0e0e0 } -.terminal-2261778856-r5 { fill: #004578 } -.terminal-2261778856-r6 { fill: #dfe1e2 } -.terminal-2261778856-r7 { fill: #00ff00 } -.terminal-2261778856-r8 { fill: #000000 } -.terminal-2261778856-r9 { fill: #1e1e1e } -.terminal-2261778856-r10 { fill: #697278 } -.terminal-2261778856-r11 { fill: #dfe1e2;font-weight: bold } -.terminal-2261778856-r12 { fill: #8b9296 } -.terminal-2261778856-r13 { fill: #646464 } + .terminal-2036187654-r1 { fill: #161616 } +.terminal-2036187654-r2 { fill: #0b3a5f } +.terminal-2036187654-r3 { fill: #c5c8c6 } +.terminal-2036187654-r4 { fill: #e0e0e0 } +.terminal-2036187654-r5 { fill: #004578 } +.terminal-2036187654-r6 { fill: #dfe1e2 } +.terminal-2036187654-r7 { fill: #00ff00 } +.terminal-2036187654-r8 { fill: #000000 } +.terminal-2036187654-r9 { fill: #1e1e1e } +.terminal-2036187654-r10 { fill: #697278 } +.terminal-2036187654-r11 { fill: #dfe1e2;font-weight: bold } +.terminal-2036187654-r12 { fill: #8b9296 } +.terminal-2036187654-r13 { fill: #646464 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SimpleApp + SimpleApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎Search for commands… - - -  Light mode                                                                                         -Switch to a light background -  Quit the application                                                                               -Quit the application as soon as possible -  Show keys and help panel                                                                           -Show help for the focused widget and a summary of available keys -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎Search for commands… + + +  Light mode                                                                                         +Switch to a light background +  Maximize                                                                                           +Maximize the focused widget +  Quit the application                                                                               +Quit the application as soon as possible +  Show keys and help panel                                                                           +Show help for the focused widget and a summary of available keys +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 89e893c0a1..c57dfc6d35 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -4,8 +4,9 @@ from tests.snapshot_tests.language_snippets import SNIPPETS from textual.app import App, ComposeResult +from textual.containers import Vertical from textual.pilot import Pilot -from textual.widgets import Button, Input, RichLog, TextArea +from textual.widgets import Button, Input, RichLog, TextArea, Footer from textual.widgets.text_area import BUILTIN_LANGUAGES, Selection, TextAreaTheme # These paths should be relative to THIS directory. @@ -1488,3 +1489,37 @@ def test_scroll_page_down(snap_compare): assert snap_compare( SNAPSHOT_APPS_DIR / "scroll_page.py", press=["pagedown"], terminal_size=(80, 25) ) + + +def test_maximize(snap_compare): + class MaximizeApp(App): + BINDINGS = [("m", "screen.maximize", "maximize focused widget")] + + def compose(self) -> ComposeResult: + yield Button("Hello") + yield Button("World") + yield Footer() + + assert snap_compare(MaximizeApp(), press=["m"]) + + +def test_maximize_container(snap_compare): + class FormContainer(Vertical): + ALLOW_MAXIMIZE = True + DEFAULT_CSS = """ + FormContainer { + width: 50%; + border: blue; + } + """ + + class MaximizeApp(App): + BINDINGS = [("m", "screen.maximize", "maximize focused widget")] + + def compose(self) -> ComposeResult: + with FormContainer(): + yield Button("Hello") + yield Button("World") + yield Footer() + + assert snap_compare(MaximizeApp(), press=["m"]) From c61ef2734631b8b4370db67d91d9b27ce67ff3f0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 25 Aug 2024 11:53:30 +0100 Subject: [PATCH 10/15] comments --- src/textual/widget.py | 4 ++-- tests/snapshot_tests/test_snapshots.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 36e8616ae8..c0f9d61d58 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -308,8 +308,8 @@ class Widget(DOMNode): """Defines default logic to allow the widget to be maximized. - `None` Use default behavior (Focusable widgets may be maximized) - - `False` Do not allow widget to be maximized. - - `True` Allow widget to be maximized. + - `False` Do not allow widget to be maximized + - `True` Allow widget to be maximized """ diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index c57dfc6d35..2f86c6772c 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -1492,6 +1492,8 @@ def test_scroll_page_down(snap_compare): def test_maximize(snap_compare): + """Check that maximize isolates a single widget.""" + class MaximizeApp(App): BINDINGS = [("m", "screen.maximize", "maximize focused widget")] @@ -1504,6 +1506,8 @@ def compose(self) -> ComposeResult: def test_maximize_container(snap_compare): + """Check maximizing a widget in a maximizeable container, maximizes the container.""" + class FormContainer(Vertical): ALLOW_MAXIMIZE = True DEFAULT_CSS = """ From cead6e37b41aafdf573a289be8a36911825852fd Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 25 Aug 2024 14:35:26 +0100 Subject: [PATCH 11/15] Rescroll --- src/textual/screen.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/textual/screen.py b/src/textual/screen.py index 0e40e22267..6cf10193f5 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -695,6 +695,10 @@ def maximize(self, widget: Widget, container: bool = True) -> None: def minimize(self) -> None: """Restore any maximized widget to normal state.""" self.maximized = None + if self.focused is not None: + self.call_after_refresh( + self.scroll_to_widget, self.focused, animate=False, center=True + ) def action_maximize(self) -> None: """Action to maximize the currently focused widget.""" From ab0b0708ba7ec30c6d4b49877ed30528f1c7ad26 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 26 Aug 2024 10:57:51 +0100 Subject: [PATCH 12/15] fix maximize logic --- src/textual/screen.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index 6cf10193f5..f6d17ad9d4 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -681,15 +681,16 @@ def maximize(self, widget: Widget, container: bool = True) -> None: widget: Widget to maximize. container: Maximize container if possible. """ - if container: - # If we want to maximize the container, look up the dom to find a suitable widget - for maximize_widget in widget.ancestors: - if not isinstance(maximize_widget, Widget): - break - if maximize_widget.allow_maximize: - self.maximized = maximize_widget - return if widget.allow_maximize: + if container: + # If we want to maximize the container, look up the dom to find a suitable widget + for maximize_widget in widget.ancestors: + if not isinstance(maximize_widget, Widget): + break + if maximize_widget.allow_maximize: + self.maximized = maximize_widget + return + self.maximized = widget def minimize(self) -> None: From c93d6c173dd52a813d1d9e3e1a57226d8abee08e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 26 Aug 2024 11:42:50 +0100 Subject: [PATCH 13/15] docstring --- src/textual/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index f6d17ad9d4..441aaa5328 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -679,7 +679,7 @@ def maximize(self, widget: Widget, container: bool = True) -> None: Args: widget: Widget to maximize. - container: Maximize container if possible. + container: If one of the widgets ancestors is a maximizeable widget, also maximize that. """ if widget.allow_maximize: if container: From 47b3a4c5fd43e98ce2feac703db509b71e908e04 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 26 Aug 2024 11:48:18 +0100 Subject: [PATCH 14/15] Update src/textual/screen.py Co-authored-by: Darren Burns --- src/textual/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index 441aaa5328..5a9002020e 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -707,7 +707,7 @@ def action_maximize(self) -> None: self.maximize(self.focused) def action_minimize(self) -> None: - """Action to minimize the currently focused widget.""" + """Action to minimize the currently maximized widget.""" self.minimize() def _reset_focus( From 82d61c168a8a4bdf955c396105f85865107a8505 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 26 Aug 2024 14:20:51 +0100 Subject: [PATCH 15/15] Update src/textual/screen.py Co-authored-by: Darren Burns --- src/textual/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index 5a9002020e..692fa7b141 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -679,7 +679,7 @@ def maximize(self, widget: Widget, container: bool = True) -> None: Args: widget: Widget to maximize. - container: If one of the widgets ancestors is a maximizeable widget, also maximize that. + container: If one of the widgets ancestors is a maximizeable widget, maximize that instead. """ if widget.allow_maximize: if container: