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