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