• 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, Optional, 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)
30
31from pw_console.get_pw_console_app import get_pw_console_app
32
33import pw_console.widgets.checkbox
34import pw_console.widgets.mouse_handlers
35import pw_console.style
36
37if TYPE_CHECKING:
38    from pw_console.console_app import ConsoleApp
39
40
41class WindowPaneHSplit(HSplit):
42    """PromptToolkit HSplit that saves the current width and height.
43
44    This overrides the write_to_screen function to save the width and height of
45    the container to be rendered.
46    """
47    def __init__(self, parent_window_pane, *args, **kwargs):
48        # Save a reference to the parent window pane.
49        self.parent_window_pane = parent_window_pane
50        super().__init__(*args, **kwargs)
51
52    def write_to_screen(
53        self,
54        screen,
55        mouse_handlers,
56        write_position,
57        parent_style: str,
58        erase_bg: bool,
59        z_index: Optional[int],
60    ) -> None:
61        # Save the width and height for the current render pass. This will be
62        # used by the log pane to render the correct amount of log lines.
63        self.parent_window_pane.update_pane_size(write_position.width,
64                                                 write_position.height)
65        # Continue writing content to the screen.
66        super().write_to_screen(screen, mouse_handlers, write_position,
67                                parent_style, erase_bg, z_index)
68
69
70class WindowPane(ABC):
71    """The Pigweed Console Window Pane parent class."""
72
73    # pylint: disable=too-many-instance-attributes
74    def __init__(
75        self,
76        application: Union['ConsoleApp', Any] = None,
77        pane_title: str = 'Window',
78        height: Optional[AnyDimension] = None,
79        width: Optional[AnyDimension] = None,
80    ):
81        if application:
82            self.application = application
83        else:
84            self.application = get_pw_console_app()
85
86        self._pane_title = pane_title
87        self._pane_subtitle: str = ''
88
89        # Default width and height to 10 lines each. They will be resized by the
90        # WindowManager later.
91        self.height = height if height else Dimension(preferred=10)
92        self.width = width if width else Dimension(preferred=10)
93
94        # Boolean to show or hide this window pane
95        self.show_pane = True
96        # Booleans for toggling top and bottom toolbars
97        self.show_top_toolbar = True
98        self.show_bottom_toolbar = True
99
100        # Height and width values for the current rendering pass.
101        self.current_pane_width = 0
102        self.current_pane_height = 0
103        self.last_pane_width = 0
104        self.last_pane_height = 0
105
106    def __repr__(self) -> str:
107        """Create a repr with this pane's title and subtitle."""
108        repr_str = f'{type(self).__qualname__}(pane_title="{self.pane_title()}"'
109        if self.pane_subtitle():
110            repr_str += f', pane_subtitle="{self.pane_subtitle()}"'
111        repr_str += ')'
112        return repr_str
113
114    def pane_title(self) -> str:
115        return self._pane_title
116
117    def set_pane_title(self, title: str) -> None:
118        self._pane_title = title
119
120    def menu_title(self) -> str:
121        """Return a title to display in the Window menu."""
122        return self.pane_title()
123
124    def pane_subtitle(self) -> str:  # pylint: disable=no-self-use
125        """Further title info for display in the Window menu."""
126        return ''
127
128    def redraw_ui(self) -> None:
129        """Redraw the prompt_toolkit UI."""
130        if not hasattr(self, 'application'):
131            return
132        # Thread safe way of sending a repaint trigger to the input event loop.
133        self.application.redraw_ui()
134
135    def focus_self(self) -> None:
136        """Switch prompt_toolkit focus to this window pane."""
137        if not hasattr(self, 'application'):
138            return
139        self.application.focus_on_container(self)
140
141    def __pt_container__(self):
142        """Return the prompt_toolkit root container for this log pane.
143
144        This allows self to be used wherever prompt_toolkit expects a container
145        object."""
146        return self.container  # pylint: disable=no-member
147
148    def get_all_key_bindings(self) -> list:
149        """Return keybinds for display in the help window.
150
151        For example:
152
153        Using a prompt_toolkit control:
154
155          return [self.some_content_control_instance.get_key_bindings()]
156
157        Hand-crafted bindings for display in the HelpWindow:
158
159          return [{
160              'Execute code': ['Enter', 'Option-Enter', 'Meta-Enter'],
161              'Reverse search history': ['Ctrl-R'],
162              'Erase input buffer.': ['Ctrl-C'],
163              'Show settings.': ['F2'],
164              'Show history.': ['F3'],
165          }]
166        """
167        # pylint: disable=no-self-use
168        return []
169
170    def get_all_menu_options(self) -> list:
171        """Return menu options for the window pane.
172
173        Should return a list of tuples containing with the display text and
174        callable to invoke on click.
175        """
176        # pylint: disable=no-self-use
177        return []
178
179    def pane_resized(self) -> bool:
180        """Return True if the current window size has changed."""
181        return (self.last_pane_width != self.current_pane_width
182                or self.last_pane_height != self.current_pane_height)
183
184    def update_pane_size(self, width, height) -> None:
185        """Save pane width and height for the current UI render pass."""
186        if width:
187            self.last_pane_width = self.current_pane_width
188            self.current_pane_width = width
189        if height:
190            self.last_pane_height = self.current_pane_height
191            self.current_pane_height = height
192
193    def _create_pane_container(self, *content) -> ConditionalContainer:
194        return ConditionalContainer(
195            WindowPaneHSplit(
196                self,
197                content,
198                # Window pane dimensions
199                height=lambda: self.height,
200                width=lambda: self.width,
201                style=functools.partial(pw_console.style.get_pane_style, self),
202            ),
203            filter=Condition(lambda: self.show_pane))
204
205    def has_child_container(self, child_container: AnyContainer) -> bool:
206        if not child_container:
207            return False
208        for container in walk(self.__pt_container__()):
209            if container == child_container:
210                return True
211        return False
212