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