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 += f', visible={self.show_pane}' 123 repr_str += ')' 124 return repr_str 125 126 def pane_title(self) -> str: 127 return self._pane_title 128 129 def set_pane_title(self, title: str) -> None: 130 self._pane_title = title 131 132 def menu_title(self) -> str: 133 """Return a title to display in the Window menu.""" 134 return self.pane_title() 135 136 def pane_subtitle(self) -> str: # pylint: disable=no-self-use 137 """Further title info for display in the Window menu.""" 138 return '' 139 140 def redraw_ui(self) -> None: 141 """Redraw the prompt_toolkit UI.""" 142 if not hasattr(self, 'application'): 143 return 144 # Thread safe way of sending a repaint trigger to the input event loop. 145 self.application.redraw_ui() 146 147 def focus_self(self) -> None: 148 """Switch prompt_toolkit focus to this window pane.""" 149 if not hasattr(self, 'application'): 150 return 151 self.application.focus_on_container(self) 152 153 def __pt_container__(self): 154 """Return the prompt_toolkit root container for this log pane. 155 156 This allows self to be used wherever prompt_toolkit expects a container 157 object.""" 158 return self.container # pylint: disable=no-member 159 160 def get_all_key_bindings(self) -> list: 161 """Return keybinds for display in the help window. 162 163 For example: 164 165 Using a prompt_toolkit control: 166 167 return [self.some_content_control_instance.get_key_bindings()] 168 169 Hand-crafted bindings for display in the HelpWindow: 170 171 return [{ 172 'Execute code': ['Enter', 'Option-Enter', 'Meta-Enter'], 173 'Reverse search history': ['Ctrl-R'], 174 'Erase input buffer.': ['Ctrl-C'], 175 'Show settings.': ['F2'], 176 'Show history.': ['F3'], 177 }] 178 """ 179 # pylint: disable=no-self-use 180 return [] 181 182 def get_window_menu_options( 183 self, 184 ) -> list[tuple[str, Callable | None]]: 185 """Return menu options for the window pane. 186 187 Should return a list of tuples containing with the display text and 188 callable to invoke on click. 189 """ 190 # pylint: disable=no-self-use 191 return [] 192 193 def get_top_level_menus(self) -> list[MenuItem]: 194 """Return MenuItems to be displayed on the main pw_console menu bar.""" 195 # pylint: disable=no-self-use 196 return [] 197 198 def pane_resized(self) -> bool: 199 """Return True if the current window size has changed.""" 200 return ( 201 self.last_pane_width != self.current_pane_width 202 or self.last_pane_height != self.current_pane_height 203 ) 204 205 def update_pane_size(self, width, height) -> None: 206 """Save pane width and height for the current UI render pass.""" 207 if width: 208 self.last_pane_width = self.current_pane_width 209 self.current_pane_width = width 210 if height: 211 self.last_pane_height = self.current_pane_height 212 self.current_pane_height = height 213 214 def _create_pane_container(self, *content) -> ConditionalContainer: 215 return ConditionalContainer( 216 WindowPaneHSplit( 217 self, 218 content, 219 # Window pane dimensions 220 height=lambda: self.height, 221 width=lambda: self.width, 222 style=functools.partial(get_pane_style, self), 223 ), 224 filter=Condition(lambda: self.show_pane), 225 ) 226 227 def has_child_container(self, child_container: AnyContainer) -> bool: 228 if not child_container: 229 return False 230 for container in walk(self.__pt_container__()): 231 if container == child_container: 232 return True 233 return False 234 235 236class FloatingWindowPane(WindowPane): 237 """The Pigweed Console FloatingWindowPane class.""" 238 239 def __init__(self, *args, **kwargs): 240 super().__init__(*args, **kwargs) 241 242 # Tracks the last focused container, to enable restoring focus after 243 # closing the dialog. 244 self.last_focused_pane = None 245 246 def close_dialog(self) -> None: 247 """Close runner dialog box.""" 248 self.show_pane = False 249 250 # Restore original focus if possible. 251 if self.last_focused_pane: 252 self.application.focus_on_container(self.last_focused_pane) 253 else: 254 # Fallback to focusing on the main menu. 255 self.application.focus_main_menu() 256 257 self.application.update_menu_items() 258 259 def open_dialog(self) -> None: 260 self.show_pane = True 261 self.last_focused_pane = self.application.focused_window() 262 self.focus_self() 263 self.application.redraw_ui() 264 265 self.application.update_menu_items() 266 267 def toggle_dialog(self) -> bool: 268 if self.show_pane: 269 self.close_dialog() 270 else: 271 self.open_dialog() 272 # The focused window has changed. Return true so 273 # ConsoleApp.run_pane_menu_option does not set the focus to the main 274 # menu. 275 return True 276