xref: /aosp_15_r20/external/pigweed/pw_console/py/pw_console/widgets/window_pane.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Window pane base class."""
15
16from __future__ import annotations
17
18from abc import ABC
19from typing import Callable, TYPE_CHECKING
20import functools
21
22from prompt_toolkit.layout.dimension import AnyDimension
23
24from prompt_toolkit.filters import Condition
25from prompt_toolkit.layout import (
26    AnyContainer,
27    ConditionalContainer,
28    Dimension,
29    HSplit,
30    walk,
31)
32from prompt_toolkit.widgets import MenuItem
33
34from pw_console.get_pw_console_app import get_pw_console_app
35from pw_console.style import get_pane_style
36
37if TYPE_CHECKING:
38    from typing import Any
39    from pw_console.console_app import ConsoleApp
40
41
42class WindowPaneHSplit(HSplit):
43    """PromptToolkit HSplit that saves the current width and height.
44
45    This overrides the write_to_screen function to save the width and height of
46    the container to be rendered.
47    """
48
49    def __init__(self, parent_window_pane, *args, **kwargs):
50        # Save a reference to the parent window pane.
51        self.parent_window_pane = parent_window_pane
52        super().__init__(*args, **kwargs)
53
54    def write_to_screen(
55        self,
56        screen,
57        mouse_handlers,
58        write_position,
59        parent_style: str,
60        erase_bg: bool,
61        z_index: int | None,
62    ) -> None:
63        # Save the width and height for the current render pass. This will be
64        # used by the log pane to render the correct amount of log lines.
65        self.parent_window_pane.update_pane_size(
66            write_position.width, write_position.height
67        )
68        # Continue writing content to the screen.
69        super().write_to_screen(
70            screen,
71            mouse_handlers,
72            write_position,
73            parent_style,
74            erase_bg,
75            z_index,
76        )
77
78
79class WindowPane(ABC):
80    """The Pigweed Console Window Pane parent class."""
81
82    # pylint: disable=too-many-instance-attributes
83    def __init__(
84        self,
85        application: ConsoleApp | Any = None,
86        pane_title: str = 'Window',
87        height: AnyDimension | None = None,
88        width: AnyDimension | None = None,
89    ):
90        if application:
91            self.application = application
92        else:
93            self.application = get_pw_console_app()
94
95        self._pane_title = pane_title
96        self._pane_subtitle: str = ''
97
98        self.extra_tab_style: str | None = None
99
100        # Default width and height to 10 lines each. They will be resized by the
101        # WindowManager later.
102        self.height = height if height else Dimension(preferred=10)
103        self.width = width if width else Dimension(preferred=10)
104
105        # Boolean to show or hide this window pane
106        self.show_pane = True
107        # Booleans for toggling top and bottom toolbars
108        self.show_top_toolbar = True
109        self.show_bottom_toolbar = True
110
111        # Height and width values for the current rendering pass.
112        self.current_pane_width = 0
113        self.current_pane_height = 0
114        self.last_pane_width = 0
115        self.last_pane_height = 0
116
117    def __repr__(self) -> str:
118        """Create a repr with this pane's title and subtitle."""
119        repr_str = f'{type(self).__qualname__}(pane_title="{self.pane_title()}"'
120        if self.pane_subtitle():
121            repr_str += f', pane_subtitle="{self.pane_subtitle()}"'
122        repr_str += f', visible={self.show_pane}'
123        repr_str += ')'
124        return repr_str
125
126    def pane_title(self) -> str:
127        return self._pane_title
128
129    def set_pane_title(self, title: str) -> None:
130        self._pane_title = title
131
132    def menu_title(self) -> str:
133        """Return a title to display in the Window menu."""
134        return self.pane_title()
135
136    def pane_subtitle(self) -> str:  # pylint: disable=no-self-use
137        """Further title info for display in the Window menu."""
138        return ''
139
140    def redraw_ui(self) -> None:
141        """Redraw the prompt_toolkit UI."""
142        if not hasattr(self, 'application'):
143            return
144        # Thread safe way of sending a repaint trigger to the input event loop.
145        self.application.redraw_ui()
146
147    def focus_self(self) -> None:
148        """Switch prompt_toolkit focus to this window pane."""
149        if not hasattr(self, 'application'):
150            return
151        self.application.focus_on_container(self)
152
153    def __pt_container__(self):
154        """Return the prompt_toolkit root container for this log pane.
155
156        This allows self to be used wherever prompt_toolkit expects a container
157        object."""
158        return self.container  # pylint: disable=no-member
159
160    def get_all_key_bindings(self) -> list:
161        """Return keybinds for display in the help window.
162
163        For example:
164
165        Using a prompt_toolkit control:
166
167          return [self.some_content_control_instance.get_key_bindings()]
168
169        Hand-crafted bindings for display in the HelpWindow:
170
171          return [{
172              'Execute code': ['Enter', 'Option-Enter', 'Meta-Enter'],
173              'Reverse search history': ['Ctrl-R'],
174              'Erase input buffer.': ['Ctrl-C'],
175              'Show settings.': ['F2'],
176              'Show history.': ['F3'],
177          }]
178        """
179        # pylint: disable=no-self-use
180        return []
181
182    def get_window_menu_options(
183        self,
184    ) -> list[tuple[str, Callable | None]]:
185        """Return menu options for the window pane.
186
187        Should return a list of tuples containing with the display text and
188        callable to invoke on click.
189        """
190        # pylint: disable=no-self-use
191        return []
192
193    def get_top_level_menus(self) -> list[MenuItem]:
194        """Return MenuItems to be displayed on the main pw_console menu bar."""
195        # pylint: disable=no-self-use
196        return []
197
198    def pane_resized(self) -> bool:
199        """Return True if the current window size has changed."""
200        return (
201            self.last_pane_width != self.current_pane_width
202            or self.last_pane_height != self.current_pane_height
203        )
204
205    def update_pane_size(self, width, height) -> None:
206        """Save pane width and height for the current UI render pass."""
207        if width:
208            self.last_pane_width = self.current_pane_width
209            self.current_pane_width = width
210        if height:
211            self.last_pane_height = self.current_pane_height
212            self.current_pane_height = height
213
214    def _create_pane_container(self, *content) -> ConditionalContainer:
215        return ConditionalContainer(
216            WindowPaneHSplit(
217                self,
218                content,
219                # Window pane dimensions
220                height=lambda: self.height,
221                width=lambda: self.width,
222                style=functools.partial(get_pane_style, self),
223            ),
224            filter=Condition(lambda: self.show_pane),
225        )
226
227    def has_child_container(self, child_container: AnyContainer) -> bool:
228        if not child_container:
229            return False
230        for container in walk(self.__pt_container__()):
231            if container == child_container:
232                return True
233        return False
234
235
236class FloatingWindowPane(WindowPane):
237    """The Pigweed Console FloatingWindowPane class."""
238
239    def __init__(self, *args, **kwargs):
240        super().__init__(*args, **kwargs)
241
242        # Tracks the last focused container, to enable restoring focus after
243        # closing the dialog.
244        self.last_focused_pane = None
245
246    def close_dialog(self) -> None:
247        """Close runner dialog box."""
248        self.show_pane = False
249
250        # Restore original focus if possible.
251        if self.last_focused_pane:
252            self.application.focus_on_container(self.last_focused_pane)
253        else:
254            # Fallback to focusing on the main menu.
255            self.application.focus_main_menu()
256
257        self.application.update_menu_items()
258
259    def open_dialog(self) -> None:
260        self.show_pane = True
261        self.last_focused_pane = self.application.focused_window()
262        self.focus_self()
263        self.application.redraw_ui()
264
265        self.application.update_menu_items()
266
267    def toggle_dialog(self) -> bool:
268        if self.show_pane:
269            self.close_dialog()
270        else:
271            self.open_dialog()
272        # The focused window has changed. Return true so
273        # ConsoleApp.run_pane_menu_option does not set the focus to the main
274        # menu.
275        return True
276