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