• 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 toolbar base class."""
15
16import logging
17from typing import Any, Callable, List, Optional
18import functools
19
20from prompt_toolkit.filters import Condition, has_focus
21from prompt_toolkit.layout import (
22    ConditionalContainer,
23    FormattedTextControl,
24    VSplit,
25    Window,
26    WindowAlign,
27)
28from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
29
30from pw_console.get_pw_console_app import get_pw_console_app
31from pw_console.style import (
32    get_pane_indicator,
33    get_button_style,
34    get_toolbar_style,
35)
36from pw_console.widgets import (
37    ToolbarButton,
38    mouse_handlers,
39    to_checkbox_with_keybind_indicator,
40    to_keybind_indicator,
41)
42
43_LOG = logging.getLogger(__package__)
44
45
46class WindowPaneResizeHandle(FormattedTextControl):
47    """Button to initiate window pane resize drag events."""
48
49    def __init__(self, parent_window_pane: Any, *args, **kwargs) -> None:
50        self.parent_window_pane = parent_window_pane
51        super().__init__(*args, **kwargs)
52
53    def mouse_handler(self, mouse_event: MouseEvent):
54        """Mouse handler for this control."""
55        # Start resize mouse drag event
56        if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
57            get_pw_console_app().window_manager.start_resize_pane(
58                self.parent_window_pane
59            )
60            # Mouse event handled, return None.
61            return None
62
63        # Mouse event not handled, return NotImplemented.
64        return NotImplemented
65
66
67class WindowPaneToolbar:
68    """One line toolbar for display at the bottom of of a window pane."""
69
70    # pylint: disable=too-many-instance-attributes
71    TOOLBAR_HEIGHT = 1
72
73    def get_left_text_tokens(self):
74        """Return toolbar indicator and title."""
75
76        title = self.title
77        if not title and self.parent_window_pane:
78            # No title was set, fetch the parent window pane title if available.
79            parent_pane_title = self.parent_window_pane.pane_title()
80            title = parent_pane_title if parent_pane_title else title
81        return get_pane_indicator(
82            self.focus_check_container, f' {title} ', self.focus_mouse_handler
83        )
84
85    def get_center_text_tokens(self):
86        """Return formatted text tokens for display in the center part of the
87        toolbar."""
88
89        button_style = get_button_style(self.focus_check_container)
90
91        # FormattedTextTuple contents: (Style, Text, Mouse handler)
92        separator_text = [('', '  ')]  # 2 spaces of separaton between keybinds.
93        if self.focus_mouse_handler:
94            separator_text = [('', '  ', self.focus_mouse_handler)]
95
96        fragments = []
97        fragments.extend(separator_text)
98
99        for button in self.buttons:
100            on_click_handler = None
101            if button.mouse_handler:
102                on_click_handler = functools.partial(
103                    mouse_handlers.on_click,
104                    button.mouse_handler,
105                )
106
107            if button.is_checkbox:
108                fragments.extend(
109                    to_checkbox_with_keybind_indicator(
110                        button.checked(),
111                        button.key,
112                        button.description,
113                        on_click_handler,
114                        base_style=button_style,
115                    )
116                )
117            else:
118                fragments.extend(
119                    to_keybind_indicator(
120                        button.key,
121                        button.description,
122                        on_click_handler,
123                        base_style=button_style,
124                    )
125                )
126
127            fragments.extend(separator_text)
128
129        # Remaining whitespace should focus on click.
130        fragments.extend(separator_text)
131
132        return fragments
133
134    def get_right_text_tokens(self):
135        """Return formatted text tokens for display."""
136        fragments = []
137        if not has_focus(self.focus_check_container.__pt_container__())():
138            if self.click_to_focus_text:
139                fragments.append(
140                    (
141                        'class:toolbar-button-inactive '
142                        'class:toolbar-button-decoration',
143                        ' ',
144                        self.focus_mouse_handler,
145                    )
146                )
147                fragments.append(
148                    (
149                        'class:toolbar-button-inactive class:keyhelp',
150                        self.click_to_focus_text,
151                        self.focus_mouse_handler,
152                    )
153                )
154                fragments.append(
155                    (
156                        'class:toolbar-button-inactive '
157                        'class:toolbar-button-decoration',
158                        ' ',
159                        self.focus_mouse_handler,
160                    )
161                )
162        if self.subtitle:
163            fragments.append(
164                ('', '  {} '.format(self.subtitle()), self.focus_mouse_handler)
165            )
166        return fragments
167
168    def get_resize_handle(self):
169        return get_pane_indicator(
170            self.focus_check_container, '─══─', hide_indicator=True
171        )
172
173    def add_button(self, button: ToolbarButton):
174        self.buttons.append(button)
175
176    def __init__(
177        self,
178        parent_window_pane: Optional[Any] = None,
179        title: Optional[str] = None,
180        subtitle: Optional[Callable[[], str]] = None,
181        focus_check_container: Optional[Any] = None,
182        focus_action_callable: Optional[Callable] = None,
183        center_section_align: WindowAlign = WindowAlign.LEFT,
184        include_resize_handle: bool = True,
185        click_to_focus_text: str = 'click to focus',
186    ):
187        self.parent_window_pane = parent_window_pane
188        self.title = title
189        self.subtitle = subtitle
190        self.click_to_focus_text = click_to_focus_text
191
192        # Assume check this container for focus
193        self.focus_check_container = self
194        self.focus_action_callable = None
195
196        # Set parent_window_pane related options
197        if self.parent_window_pane:
198            if not subtitle:
199                self.subtitle = self.parent_window_pane.pane_subtitle
200            self.focus_check_container = self.parent_window_pane
201            self.focus_action_callable = self.parent_window_pane.focus_self
202
203        # Set title overrides
204        if self.subtitle is None:
205
206            def empty_subtitle() -> str:
207                return ''
208
209            self.subtitle = empty_subtitle
210
211        if focus_check_container:
212            self.focus_check_container = focus_check_container
213        if focus_action_callable:
214            self.focus_action_callable = focus_action_callable
215
216        self.focus_mouse_handler = None
217        if self.focus_action_callable:
218            self.focus_mouse_handler = functools.partial(
219                mouse_handlers.on_click,
220                self.focus_action_callable,
221            )
222
223        self.buttons: List[ToolbarButton] = []
224        self.show_toolbar = True
225
226        self.left_section_window = Window(
227            content=FormattedTextControl(self.get_left_text_tokens),
228            align=WindowAlign.LEFT,
229            dont_extend_width=True,
230        )
231
232        self.center_section_window = Window(
233            content=FormattedTextControl(self.get_center_text_tokens),
234            align=center_section_align,
235            dont_extend_width=False,
236        )
237
238        self.right_section_window = Window(
239            content=FormattedTextControl(self.get_right_text_tokens),
240            # Right side text should appear at the far right of the toolbar
241            align=WindowAlign.RIGHT,
242            dont_extend_width=True,
243        )
244
245        wrapped_get_toolbar_style = functools.partial(
246            get_toolbar_style, self.focus_check_container
247        )
248
249        sections = [
250            self.left_section_window,
251            self.center_section_window,
252            self.right_section_window,
253        ]
254        if self.parent_window_pane and include_resize_handle:
255            resize_handle = Window(
256                content=WindowPaneResizeHandle(
257                    self.parent_window_pane,
258                    self.get_resize_handle,
259                ),
260                # Right side text should appear at the far right of the toolbar
261                align=WindowAlign.RIGHT,
262                dont_extend_width=True,
263            )
264            sections.append(resize_handle)
265
266        self.toolbar_vsplit = VSplit(
267            sections,
268            height=WindowPaneToolbar.TOOLBAR_HEIGHT,
269            style=wrapped_get_toolbar_style,
270        )
271
272        self.container = self._create_toolbar_container(self.toolbar_vsplit)
273
274    def _create_toolbar_container(self, content):
275        return ConditionalContainer(
276            content, filter=Condition(lambda: self.show_toolbar)
277        )
278
279    def __pt_container__(self):
280        """Return the prompt_toolkit root container for this log pane.
281
282        This allows self to be used wherever prompt_toolkit expects a container
283        object."""
284        return self.container  # pylint: disable=no-member
285