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