• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 += ')'
123        return repr_str
124
125    def pane_title(self) -> str:
126        return self._pane_title
127
128    def set_pane_title(self, title: str) -> None:
129        self._pane_title = title
130
131    def menu_title(self) -> str:
132        """Return a title to display in the Window menu."""
133        return self.pane_title()
134
135    def pane_subtitle(self) -> str:  # pylint: disable=no-self-use
136        """Further title info for display in the Window menu."""
137        return ''
138
139    def redraw_ui(self) -> None:
140        """Redraw the prompt_toolkit UI."""
141        if not hasattr(self, 'application'):
142            return
143        # Thread safe way of sending a repaint trigger to the input event loop.
144        self.application.redraw_ui()
145
146    def focus_self(self) -> None:
147        """Switch prompt_toolkit focus to this window pane."""
148        if not hasattr(self, 'application'):
149            return
150        self.application.focus_on_container(self)
151
152    def __pt_container__(self):
153        """Return the prompt_toolkit root container for this log pane.
154
155        This allows self to be used wherever prompt_toolkit expects a container
156        object."""
157        return self.container  # pylint: disable=no-member
158
159    def get_all_key_bindings(self) -> list:
160        """Return keybinds for display in the help window.
161
162        For example:
163
164        Using a prompt_toolkit control:
165
166          return [self.some_content_control_instance.get_key_bindings()]
167
168        Hand-crafted bindings for display in the HelpWindow:
169
170          return [{
171              'Execute code': ['Enter', 'Option-Enter', 'Meta-Enter'],
172              'Reverse search history': ['Ctrl-R'],
173              'Erase input buffer.': ['Ctrl-C'],
174              'Show settings.': ['F2'],
175              'Show history.': ['F3'],
176          }]
177        """
178        # pylint: disable=no-self-use
179        return []
180
181    def get_window_menu_options(
182        self,
183    ) -> list[tuple[str, Callable | None]]:
184        """Return menu options for the window pane.
185
186        Should return a list of tuples containing with the display text and
187        callable to invoke on click.
188        """
189        # pylint: disable=no-self-use
190        return []
191
192    def get_top_level_menus(self) -> list[MenuItem]:
193        """Return MenuItems to be displayed on the main pw_console menu bar."""
194        # pylint: disable=no-self-use
195        return []
196
197    def pane_resized(self) -> bool:
198        """Return True if the current window size has changed."""
199        return (
200            self.last_pane_width != self.current_pane_width
201            or self.last_pane_height != self.current_pane_height
202        )
203
204    def update_pane_size(self, width, height) -> None:
205        """Save pane width and height for the current UI render pass."""
206        if width:
207            self.last_pane_width = self.current_pane_width
208            self.current_pane_width = width
209        if height:
210            self.last_pane_height = self.current_pane_height
211            self.current_pane_height = height
212
213    def _create_pane_container(self, *content) -> ConditionalContainer:
214        return ConditionalContainer(
215            WindowPaneHSplit(
216                self,
217                content,
218                # Window pane dimensions
219                height=lambda: self.height,
220                width=lambda: self.width,
221                style=functools.partial(get_pane_style, self),
222            ),
223            filter=Condition(lambda: self.show_pane),
224        )
225
226    def has_child_container(self, child_container: AnyContainer) -> bool:
227        if not child_container:
228            return False
229        for container in walk(self.__pt_container__()):
230            if container == child_container:
231                return True
232        return False
233
234
235class FloatingWindowPane(WindowPane):
236    """The Pigweed Console FloatingWindowPane class."""
237
238    def __init__(self, *args, **kwargs):
239        super().__init__(*args, **kwargs)
240
241        # Tracks the last focused container, to enable restoring focus after
242        # closing the dialog.
243        self.last_focused_pane = None
244
245    def close_dialog(self) -> None:
246        """Close runner dialog box."""
247        self.show_pane = False
248
249        # Restore original focus if possible.
250        if self.last_focused_pane:
251            self.application.focus_on_container(self.last_focused_pane)
252        else:
253            # Fallback to focusing on the main menu.
254            self.application.focus_main_menu()
255
256        self.application.update_menu_items()
257
258    def open_dialog(self) -> None:
259        self.show_pane = True
260        self.last_focused_pane = self.application.focused_window()
261        self.focus_self()
262        self.application.redraw_ui()
263
264        self.application.update_menu_items()
265
266    def toggle_dialog(self) -> bool:
267        if self.show_pane:
268            self.close_dialog()
269        else:
270            self.open_dialog()
271        # The focused window has changed. Return true so
272        # ConsoleApp.run_pane_menu_option does not set the focus to the main
273        # menu.
274        return True
275