aboutsummaryrefslogtreecommitdiff
path: root/pw_console/py/pw_console/widgets/window_pane.py
blob: 1c9bd7284fba7fb706c3f318d9eaba11e9d265db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# Copyright 2021 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Window pane base class."""

from abc import ABC
from typing import Any, Callable, List, Optional, Tuple, TYPE_CHECKING, Union
import functools

from prompt_toolkit.layout.dimension import AnyDimension

from prompt_toolkit.filters import Condition
from prompt_toolkit.layout import (
    AnyContainer,
    ConditionalContainer,
    Dimension,
    HSplit,
    walk,
)
from prompt_toolkit.widgets import MenuItem

from pw_console.get_pw_console_app import get_pw_console_app
from pw_console.style import get_pane_style

if TYPE_CHECKING:
    from pw_console.console_app import ConsoleApp


class WindowPaneHSplit(HSplit):
    """PromptToolkit HSplit that saves the current width and height.

    This overrides the write_to_screen function to save the width and height of
    the container to be rendered.
    """

    def __init__(self, parent_window_pane, *args, **kwargs):
        # Save a reference to the parent window pane.
        self.parent_window_pane = parent_window_pane
        super().__init__(*args, **kwargs)

    def write_to_screen(
        self,
        screen,
        mouse_handlers,
        write_position,
        parent_style: str,
        erase_bg: bool,
        z_index: Optional[int],
    ) -> None:
        # Save the width and height for the current render pass. This will be
        # used by the log pane to render the correct amount of log lines.
        self.parent_window_pane.update_pane_size(
            write_position.width, write_position.height
        )
        # Continue writing content to the screen.
        super().write_to_screen(
            screen,
            mouse_handlers,
            write_position,
            parent_style,
            erase_bg,
            z_index,
        )


class WindowPane(ABC):
    """The Pigweed Console Window Pane parent class."""

    # pylint: disable=too-many-instance-attributes
    def __init__(
        self,
        application: Union['ConsoleApp', Any] = None,
        pane_title: str = 'Window',
        height: Optional[AnyDimension] = None,
        width: Optional[AnyDimension] = None,
    ):
        if application:
            self.application = application
        else:
            self.application = get_pw_console_app()

        self._pane_title = pane_title
        self._pane_subtitle: str = ''

        self.extra_tab_style: Optional[str] = None

        # Default width and height to 10 lines each. They will be resized by the
        # WindowManager later.
        self.height = height if height else Dimension(preferred=10)
        self.width = width if width else Dimension(preferred=10)

        # Boolean to show or hide this window pane
        self.show_pane = True
        # Booleans for toggling top and bottom toolbars
        self.show_top_toolbar = True
        self.show_bottom_toolbar = True

        # Height and width values for the current rendering pass.
        self.current_pane_width = 0
        self.current_pane_height = 0
        self.last_pane_width = 0
        self.last_pane_height = 0

    def __repr__(self) -> str:
        """Create a repr with this pane's title and subtitle."""
        repr_str = f'{type(self).__qualname__}(pane_title="{self.pane_title()}"'
        if self.pane_subtitle():
            repr_str += f', pane_subtitle="{self.pane_subtitle()}"'
        repr_str += ')'
        return repr_str

    def pane_title(self) -> str:
        return self._pane_title

    def set_pane_title(self, title: str) -> None:
        self._pane_title = title

    def menu_title(self) -> str:
        """Return a title to display in the Window menu."""
        return self.pane_title()

    def pane_subtitle(self) -> str:  # pylint: disable=no-self-use
        """Further title info for display in the Window menu."""
        return ''

    def redraw_ui(self) -> None:
        """Redraw the prompt_toolkit UI."""
        if not hasattr(self, 'application'):
            return
        # Thread safe way of sending a repaint trigger to the input event loop.
        self.application.redraw_ui()

    def focus_self(self) -> None:
        """Switch prompt_toolkit focus to this window pane."""
        if not hasattr(self, 'application'):
            return
        self.application.focus_on_container(self)

    def __pt_container__(self):
        """Return the prompt_toolkit root container for this log pane.

        This allows self to be used wherever prompt_toolkit expects a container
        object."""
        return self.container  # pylint: disable=no-member

    def get_all_key_bindings(self) -> List:
        """Return keybinds for display in the help window.

        For example:

        Using a prompt_toolkit control:

          return [self.some_content_control_instance.get_key_bindings()]

        Hand-crafted bindings for display in the HelpWindow:

          return [{
              'Execute code': ['Enter', 'Option-Enter', 'Meta-Enter'],
              'Reverse search history': ['Ctrl-R'],
              'Erase input buffer.': ['Ctrl-C'],
              'Show settings.': ['F2'],
              'Show history.': ['F3'],
          }]
        """
        # pylint: disable=no-self-use
        return []

    def get_window_menu_options(
        self,
    ) -> List[Tuple[str, Union[Callable, None]]]:
        """Return menu options for the window pane.

        Should return a list of tuples containing with the display text and
        callable to invoke on click.
        """
        # pylint: disable=no-self-use
        return []

    def get_top_level_menus(self) -> List[MenuItem]:
        """Return MenuItems to be displayed on the main pw_console menu bar."""
        # pylint: disable=no-self-use
        return []

    def pane_resized(self) -> bool:
        """Return True if the current window size has changed."""
        return (
            self.last_pane_width != self.current_pane_width
            or self.last_pane_height != self.current_pane_height
        )

    def update_pane_size(self, width, height) -> None:
        """Save pane width and height for the current UI render pass."""
        if width:
            self.last_pane_width = self.current_pane_width
            self.current_pane_width = width
        if height:
            self.last_pane_height = self.current_pane_height
            self.current_pane_height = height

    def _create_pane_container(self, *content) -> ConditionalContainer:
        return ConditionalContainer(
            WindowPaneHSplit(
                self,
                content,
                # Window pane dimensions
                height=lambda: self.height,
                width=lambda: self.width,
                style=functools.partial(get_pane_style, self),
            ),
            filter=Condition(lambda: self.show_pane),
        )

    def has_child_container(self, child_container: AnyContainer) -> bool:
        if not child_container:
            return False
        for container in walk(self.__pt_container__()):
            if container == child_container:
                return True
        return False


class FloatingWindowPane(WindowPane):
    """The Pigweed Console FloatingWindowPane class."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Tracks the last focused container, to enable restoring focus after
        # closing the dialog.
        self.last_focused_pane = None

    def close_dialog(self) -> None:
        """Close runner dialog box."""
        self.show_pane = False

        # Restore original focus if possible.
        if self.last_focused_pane:
            self.application.focus_on_container(self.last_focused_pane)
        else:
            # Fallback to focusing on the main menu.
            self.application.focus_main_menu()

        self.application.update_menu_items()

    def open_dialog(self) -> None:
        self.show_pane = True
        self.last_focused_pane = self.application.focused_window()
        self.focus_self()
        self.application.redraw_ui()

        self.application.update_menu_items()

    def toggle_dialog(self) -> bool:
        if self.show_pane:
            self.close_dialog()
        else:
            self.open_dialog()
        # The focused window has changed. Return true so
        # ConsoleApp.run_pane_menu_option does not set the focus to the main
        # menu.
        return True