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"""WindowManager""" 15 16import collections 17import copy 18import functools 19from itertools import chain 20import logging 21import operator 22from typing import Any, Dict, Iterable, List, Optional 23 24from prompt_toolkit.key_binding import KeyBindings 25from prompt_toolkit.layout import ( 26 Dimension, 27 HSplit, 28 VSplit, 29 FormattedTextControl, 30 Window, 31 WindowAlign, 32) 33from prompt_toolkit.mouse_events import MouseEvent, MouseEventType, MouseButton 34from prompt_toolkit.widgets import MenuItem 35 36from pw_console.console_prefs import ConsolePrefs, error_unknown_window 37from pw_console.log_pane import LogPane 38import pw_console.widgets.checkbox 39from pw_console.widgets import WindowPaneToolbar 40import pw_console.widgets.mouse_handlers 41from pw_console.window_list import WindowList, DisplayMode 42 43_LOG = logging.getLogger(__package__) 44 45# Amount for adjusting window dimensions when enlarging and shrinking. 46_WINDOW_SPLIT_ADJUST = 1 47 48 49class WindowListResizeHandle(FormattedTextControl): 50 """Button to initiate window list resize drag events.""" 51 def __init__(self, window_manager, window_list: Any, *args, 52 **kwargs) -> None: 53 self.window_manager = window_manager 54 self.window_list = window_list 55 super().__init__(*args, **kwargs) 56 57 def mouse_handler(self, mouse_event: MouseEvent): 58 """Mouse handler for this control.""" 59 # Start resize mouse drag event 60 if mouse_event.event_type == MouseEventType.MOUSE_DOWN: 61 self.window_manager.start_resize(self.window_list) 62 # Mouse event handled, return None. 63 return None 64 65 # Mouse event not handled, return NotImplemented. 66 return NotImplemented 67 68 69class WindowManagerVSplit(VSplit): 70 """PromptToolkit VSplit class with some additions for size and mouse resize. 71 72 This VSplit has a write_to_screen function that saves the width and height 73 of the container for the current render pass. It also handles overriding 74 mouse handlers for triggering window resize adjustments. 75 """ 76 def __init__(self, parent_window_manager, *args, **kwargs): 77 # Save a reference to the parent window pane. 78 self.parent_window_manager = parent_window_manager 79 super().__init__(*args, **kwargs) 80 81 def write_to_screen( 82 self, 83 screen, 84 mouse_handlers, 85 write_position, 86 parent_style: str, 87 erase_bg: bool, 88 z_index: Optional[int], 89 ) -> None: 90 new_mouse_handlers = mouse_handlers 91 # Is resize mode active? 92 if self.parent_window_manager.resize_mode: 93 # Ignore future mouse_handler updates. 94 new_mouse_handlers = ( 95 pw_console.widgets.mouse_handlers.EmptyMouseHandler()) 96 # Set existing mouse_handlers to the parent_window_managers's 97 # mouse_handler. This will handle triggering resize events. 98 mouse_handlers.set_mouse_handler_for_range( 99 write_position.xpos, 100 write_position.xpos + write_position.width, 101 write_position.ypos, 102 write_position.ypos + write_position.height, 103 self.parent_window_manager.mouse_handler) 104 105 # Save the width and height for the current render pass. 106 self.parent_window_manager.update_window_manager_size( 107 write_position.width, write_position.height) 108 # Continue writing content to the screen. 109 super().write_to_screen(screen, new_mouse_handlers, write_position, 110 parent_style, erase_bg, z_index) 111 112 113class WindowManagerHSplit(HSplit): 114 """PromptToolkit HSplit class with some additions for size and mouse resize. 115 116 This HSplit has a write_to_screen function that saves the width and height 117 of the container for the current render pass. It also handles overriding 118 mouse handlers for triggering window resize adjustments. 119 """ 120 def __init__(self, parent_window_manager, *args, **kwargs): 121 # Save a reference to the parent window pane. 122 self.parent_window_manager = parent_window_manager 123 super().__init__(*args, **kwargs) 124 125 def write_to_screen( 126 self, 127 screen, 128 mouse_handlers, 129 write_position, 130 parent_style: str, 131 erase_bg: bool, 132 z_index: Optional[int], 133 ) -> None: 134 new_mouse_handlers = mouse_handlers 135 # Is resize mode active? 136 if self.parent_window_manager.resize_mode: 137 # Ignore future mouse_handler updates. 138 new_mouse_handlers = ( 139 pw_console.widgets.mouse_handlers.EmptyMouseHandler()) 140 # Set existing mouse_handlers to the parent_window_managers's 141 # mouse_handler. This will handle triggering resize events. 142 mouse_handlers.set_mouse_handler_for_range( 143 write_position.xpos, 144 write_position.xpos + write_position.width, 145 write_position.ypos, 146 write_position.ypos + write_position.height, 147 self.parent_window_manager.mouse_handler) 148 149 # Save the width and height for the current render pass. 150 self.parent_window_manager.update_window_manager_size( 151 write_position.width, write_position.height) 152 # Continue writing content to the screen. 153 super().write_to_screen(screen, new_mouse_handlers, write_position, 154 parent_style, erase_bg, z_index) 155 156 157class WindowManager: 158 """WindowManager class 159 160 This class handles adding/removing/resizing windows and rendering the 161 prompt_toolkit split layout.""" 162 163 # pylint: disable=too-many-public-methods,too-many-instance-attributes 164 165 def __init__( 166 self, 167 application: Any, 168 ): 169 self.application = application 170 self.window_lists: collections.deque = collections.deque() 171 self.window_lists.append(WindowList(self)) 172 self.key_bindings = self._create_key_bindings() 173 self.top_toolbars: List[WindowPaneToolbar] = [] 174 self.bottom_toolbars: List[WindowPaneToolbar] = [] 175 176 self.resize_mode: bool = False 177 self.resize_target_window_list_index: Optional[int] = None 178 self.resize_target_window_list: Optional[int] = None 179 self.resize_current_row: int = 0 180 self.resize_current_column: int = 0 181 182 self.current_window_manager_width: int = 0 183 self.current_window_manager_height: int = 0 184 self.last_window_manager_width: int = 0 185 self.last_window_manager_height: int = 0 186 187 def update_window_manager_size(self, width, height): 188 """Save width and height for the current UI render pass.""" 189 if width: 190 self.last_window_manager_width = self.current_window_manager_width 191 self.current_window_manager_width = width 192 if height: 193 self.last_window_manager_height = self.current_window_manager_height 194 self.current_window_manager_height = height 195 196 if (self.current_window_manager_width != self.last_window_manager_width 197 or self.current_window_manager_height != 198 self.last_window_manager_height): 199 self.rebalance_window_list_sizes() 200 201 def _set_window_list_sizes(self, new_heights: List[int], 202 new_widths: List[int]) -> None: 203 for window_list in self.window_lists: 204 window_list.height = Dimension(preferred=new_heights[0]) 205 new_heights = new_heights[1:] 206 window_list.width = Dimension(preferred=new_widths[0]) 207 new_widths = new_widths[1:] 208 209 def vertical_window_list_spliting(self) -> bool: 210 return self.application.prefs.window_column_split_method == 'vertical' 211 212 def rebalance_window_list_sizes(self) -> None: 213 """Adjust relative split sizes to fill available space.""" 214 available_height = self.current_window_manager_height 215 available_width = self.current_window_manager_width 216 217 old_heights = [w.height.preferred for w in self.window_lists] 218 old_widths = [w.width.preferred for w in self.window_lists] 219 220 # Make sure the old totals are not zero. 221 old_height_total = max(sum(old_heights), 1) 222 old_width_total = max(sum(old_widths), 1) 223 224 height_percentages = [ 225 value / old_height_total for value in old_heights 226 ] 227 width_percentages = [value / old_width_total for value in old_widths] 228 229 new_heights = [ 230 int(available_height * percentage) 231 for percentage in height_percentages 232 ] 233 new_widths = [ 234 int(available_width * percentage) 235 for percentage in width_percentages 236 ] 237 238 if self.vertical_window_list_spliting(): 239 new_heights = [ 240 self.current_window_manager_height for h in new_heights 241 ] 242 else: 243 new_widths = [ 244 self.current_window_manager_width for h in new_widths 245 ] 246 247 self._set_window_list_sizes(new_heights, new_widths) 248 249 def _create_key_bindings(self) -> KeyBindings: 250 key_bindings = KeyBindings() 251 register = self.application.prefs.register_keybinding 252 253 @register('window-manager.move-pane-left', key_bindings) 254 def move_pane_left(_event): 255 """Move window pane left.""" 256 self.move_pane_left() 257 258 @register('window-manager.move-pane-right', key_bindings) 259 def move_pane_right(_event): 260 """Move window pane right.""" 261 self.move_pane_right() 262 263 @register('window-manager.move-pane-down', key_bindings) 264 def move_pane_down(_event): 265 """Move window pane down.""" 266 self.move_pane_down() 267 268 @register('window-manager.move-pane-up', key_bindings) 269 def move_pane_up(_event): 270 """Move window pane up.""" 271 self.move_pane_up() 272 273 @register('window-manager.enlarge-pane', key_bindings) 274 def enlarge_pane(_event): 275 """Enlarge the active window pane.""" 276 self.enlarge_pane() 277 278 @register('window-manager.shrink-pane', key_bindings) 279 def shrink_pane(_event): 280 """Shrink the active window pane.""" 281 self.shrink_pane() 282 283 @register('window-manager.shrink-split', key_bindings) 284 def shrink_split(_event): 285 """Shrink the current window split.""" 286 self.shrink_split() 287 288 @register('window-manager.enlarge-split', key_bindings) 289 def enlarge_split(_event): 290 """Enlarge the current window split.""" 291 self.enlarge_split() 292 293 @register('window-manager.focus-prev-pane', key_bindings) 294 def focus_prev_pane(_event): 295 """Switch focus to the previous window pane or tab.""" 296 self.focus_previous_pane() 297 298 @register('window-manager.focus-next-pane', key_bindings) 299 def focus_next_pane(_event): 300 """Switch focus to the next window pane or tab.""" 301 self.focus_next_pane() 302 303 @register('window-manager.balance-window-panes', key_bindings) 304 def balance_window_panes(_event): 305 """Balance all window sizes.""" 306 self.balance_window_sizes() 307 308 return key_bindings 309 310 def delete_empty_window_lists(self): 311 empty_lists = [ 312 window_list for window_list in self.window_lists 313 if window_list.empty() 314 ] 315 for empty_list in empty_lists: 316 self.window_lists.remove(empty_list) 317 318 def add_top_toolbar(self, toolbar: WindowPaneToolbar) -> None: 319 self.top_toolbars.append(toolbar) 320 321 def add_bottom_toolbar(self, toolbar: WindowPaneToolbar) -> None: 322 self.bottom_toolbars.append(toolbar) 323 324 def create_root_container(self): 325 """Create vertical or horizontal splits for all active panes.""" 326 self.delete_empty_window_lists() 327 328 for window_list in self.window_lists: 329 window_list.update_container() 330 331 vertical_split = self.vertical_window_list_spliting() 332 333 window_containers = [] 334 for i, window_list in enumerate(self.window_lists): 335 window_containers.append(window_list.container) 336 if (i + 1) >= len(self.window_lists): 337 continue 338 339 if vertical_split: 340 separator_padding = Window( 341 content=WindowListResizeHandle(self, window_list, "│"), 342 char='│', 343 width=1, 344 dont_extend_height=False, 345 ) 346 resize_separator = HSplit( 347 [ 348 separator_padding, 349 Window( 350 content=WindowListResizeHandle( 351 self, window_list, "║\n║\n║"), 352 char='│', 353 width=1, 354 dont_extend_height=True, 355 ), 356 separator_padding, 357 ], 358 style='class:pane_separator', 359 ) 360 else: 361 resize_separator = Window( 362 content=WindowListResizeHandle(self, window_list, "════"), 363 char='─', 364 height=1, 365 align=WindowAlign.CENTER, 366 dont_extend_width=False, 367 style='class:pane_separator', 368 ) 369 window_containers.append(resize_separator) 370 371 if vertical_split: 372 split = WindowManagerVSplit(self, window_containers) 373 else: 374 split = WindowManagerHSplit(self, window_containers) 375 376 split_items = [] 377 split_items.extend(self.top_toolbars) 378 split_items.append(split) 379 split_items.extend(self.bottom_toolbars) 380 return HSplit(split_items) 381 382 def update_root_container_body(self): 383 # Replace the root MenuContainer body with the new split. 384 self.application.root_container.container.content.children[ 385 1] = self.create_root_container() 386 387 def _get_active_window_list_and_pane(self): 388 active_pane = None 389 active_window_list = None 390 for window_list in self.window_lists: 391 active_pane = window_list.get_current_active_pane() 392 if active_pane: 393 active_window_list = window_list 394 break 395 return active_window_list, active_pane 396 397 def window_list_index(self, window_list: WindowList) -> Optional[int]: 398 index = None 399 try: 400 index = self.window_lists.index(window_list) 401 except ValueError: 402 # Ignore ValueError which can be raised by the self.window_lists 403 # deque if the window_list can't be found. 404 pass 405 return index 406 407 def run_action_on_active_pane(self, function_name): 408 _active_window_list, active_pane = ( 409 self._get_active_window_list_and_pane()) 410 if not hasattr(active_pane, function_name): 411 return 412 method_to_call = getattr(active_pane, function_name) 413 method_to_call() 414 return 415 416 def focus_previous_pane(self) -> None: 417 """Focus on the previous visible window pane or tab.""" 418 self.focus_next_pane(reverse_order=True) 419 420 def focus_next_pane(self, reverse_order=False) -> None: 421 """Focus on the next visible window pane or tab.""" 422 active_window_list, active_pane = ( 423 self._get_active_window_list_and_pane()) 424 if active_window_list is None: 425 return 426 427 # Total count of window lists and panes 428 window_list_count = len(self.window_lists) 429 pane_count = len(active_window_list.active_panes) 430 431 # Get currently focused indices 432 active_window_list_index = self.window_list_index(active_window_list) 433 if active_window_list_index is None: 434 return 435 active_pane_index = active_window_list.pane_index(active_pane) 436 437 increment = -1 if reverse_order else 1 438 # Assume we can switch to the next pane in the current window_list 439 next_pane_index = active_pane_index + increment 440 441 # Case 1: next_pane_index does not exist in this window list. 442 # Action: Switch to the first pane of the next window list. 443 if next_pane_index >= pane_count or next_pane_index < 0: 444 # Get the next window_list 445 next_window_list_index = ((active_window_list_index + increment) % 446 window_list_count) 447 next_window_list = self.window_lists[next_window_list_index] 448 449 # If tabbed window mode is enabled, switch to the first tab. 450 if next_window_list.display_mode == DisplayMode.TABBED: 451 if reverse_order: 452 next_window_list.switch_to_tab( 453 len(next_window_list.active_panes) - 1) 454 else: 455 next_window_list.switch_to_tab(0) 456 return 457 458 # Otherwise switch to the first visible window pane. 459 pane_list = next_window_list.active_panes 460 if reverse_order: 461 pane_list = reversed(pane_list) 462 for pane in pane_list: 463 if pane.show_pane: 464 self.application.focus_on_container(pane) 465 return 466 467 # Case 2: next_pane_index does exist and display mode is tabs. 468 # Action: Switch to the next tab of the current window list. 469 if active_window_list.display_mode == DisplayMode.TABBED: 470 active_window_list.switch_to_tab(next_pane_index) 471 return 472 473 # Case 3: next_pane_index does exist and display mode is stacked. 474 # Action: Switch to the next visible window pane. 475 index_range = range(1, pane_count) 476 if reverse_order: 477 index_range = range(pane_count - 1, 0, -1) 478 for i in index_range: 479 next_pane_index = (active_pane_index + i) % pane_count 480 next_pane = active_window_list.active_panes[next_pane_index] 481 if next_pane.show_pane: 482 self.application.focus_on_container(next_pane) 483 return 484 return 485 486 def move_pane_left(self): 487 active_window_list, active_pane = ( 488 self._get_active_window_list_and_pane()) 489 if not active_window_list: 490 return 491 492 window_list_index = self.window_list_index(active_window_list) 493 # Move left should pick the previous window_list 494 target_window_list_index = window_list_index - 1 495 496 # Check if a new WindowList should be created on the left 497 if target_window_list_index == -1: 498 # Add the new WindowList 499 target_window_list = WindowList(self) 500 self.window_lists.appendleft(target_window_list) 501 self.reset_split_sizes() 502 # New index is 0 503 target_window_list_index = 0 504 505 # Get the destination window_list 506 target_window_list = self.window_lists[target_window_list_index] 507 508 # Move the pane 509 active_window_list.remove_pane_no_checks(active_pane) 510 target_window_list.add_pane(active_pane, add_at_beginning=True) 511 target_window_list.reset_pane_sizes() 512 self.delete_empty_window_lists() 513 514 def move_pane_right(self): 515 active_window_list, active_pane = ( 516 self._get_active_window_list_and_pane()) 517 if not active_window_list: 518 return 519 520 window_list_index = self.window_list_index(active_window_list) 521 # Move right should pick the next window_list 522 target_window_list_index = window_list_index + 1 523 524 # Check if a new WindowList should be created 525 if target_window_list_index == len(self.window_lists): 526 # Add a new WindowList 527 target_window_list = WindowList(self) 528 self.window_lists.append(target_window_list) 529 self.reset_split_sizes() 530 531 # Get the destination window_list 532 target_window_list = self.window_lists[target_window_list_index] 533 534 # Move the pane 535 active_window_list.remove_pane_no_checks(active_pane) 536 target_window_list.add_pane(active_pane, add_at_beginning=True) 537 target_window_list.reset_pane_sizes() 538 self.delete_empty_window_lists() 539 540 def move_pane_up(self): 541 active_window_list, _active_pane = ( 542 self._get_active_window_list_and_pane()) 543 if not active_window_list: 544 return 545 546 active_window_list.move_pane_up() 547 548 def move_pane_down(self): 549 active_window_list, _active_pane = ( 550 self._get_active_window_list_and_pane()) 551 if not active_window_list: 552 return 553 554 active_window_list.move_pane_down() 555 556 def shrink_pane(self): 557 active_window_list, _active_pane = ( 558 self._get_active_window_list_and_pane()) 559 if not active_window_list: 560 return 561 562 active_window_list.shrink_pane() 563 564 def enlarge_pane(self): 565 active_window_list, _active_pane = ( 566 self._get_active_window_list_and_pane()) 567 if not active_window_list: 568 return 569 570 active_window_list.enlarge_pane() 571 572 def shrink_split(self): 573 if len(self.window_lists) < 2: 574 return 575 576 active_window_list, _active_pane = ( 577 self._get_active_window_list_and_pane()) 578 if not active_window_list: 579 return 580 581 self.adjust_split_size(active_window_list, -_WINDOW_SPLIT_ADJUST) 582 583 def enlarge_split(self): 584 active_window_list, _active_pane = ( 585 self._get_active_window_list_and_pane()) 586 if not active_window_list: 587 return 588 589 self.adjust_split_size(active_window_list, _WINDOW_SPLIT_ADJUST) 590 591 def balance_window_sizes(self): 592 """Reset all splits and pane sizes.""" 593 self.reset_pane_sizes() 594 self.reset_split_sizes() 595 596 def reset_split_sizes(self): 597 """Reset all active pane width and height to defaults""" 598 available_height = self.current_window_manager_height 599 available_width = self.current_window_manager_width 600 old_heights = [w.height.preferred for w in self.window_lists] 601 old_widths = [w.width.preferred for w in self.window_lists] 602 new_heights = [int(available_height / len(old_heights)) 603 ] * len(old_heights) 604 new_widths = [int(available_width / len(old_widths))] * len(old_widths) 605 606 self._set_window_list_sizes(new_heights, new_widths) 607 608 def _get_next_window_list_for_resizing( 609 self, window_list: WindowList) -> Optional[WindowList]: 610 window_list_index = self.window_list_index(window_list) 611 if window_list_index is None: 612 return None 613 614 next_window_list_index = ((window_list_index + 1) % 615 len(self.window_lists)) 616 617 # Use the previous window if we are on the last split 618 if window_list_index == len(self.window_lists) - 1: 619 next_window_list_index = window_list_index - 1 620 621 next_window_list = self.window_lists[next_window_list_index] 622 return next_window_list 623 624 def adjust_split_size(self, 625 window_list: WindowList, 626 diff: int = _WINDOW_SPLIT_ADJUST) -> None: 627 """Increase or decrease a given window_list's vertical split width.""" 628 # No need to resize if only one split. 629 if len(self.window_lists) < 2: 630 return 631 632 # Get the next split to subtract from. 633 next_window_list = self._get_next_window_list_for_resizing(window_list) 634 if not next_window_list: 635 return 636 637 if self.vertical_window_list_spliting(): 638 # Get current width 639 old_value = window_list.width.preferred 640 next_old_value = next_window_list.width.preferred # type: ignore 641 else: 642 # Get current height 643 old_value = window_list.height.preferred 644 next_old_value = next_window_list.height.preferred # type: ignore 645 646 # Add to the current split 647 new_value = old_value + diff 648 if new_value <= 0: 649 new_value = old_value 650 651 # Subtract from the next split 652 next_new_value = next_old_value - diff 653 if next_new_value <= 0: 654 next_new_value = next_old_value 655 656 # If new height is too small or no change, make no adjustments. 657 if new_value < 3 or next_new_value < 3 or old_value == new_value: 658 return 659 660 if self.vertical_window_list_spliting(): 661 # Set new width 662 window_list.width.preferred = new_value 663 next_window_list.width.preferred = next_new_value # type: ignore 664 else: 665 # Set new height 666 window_list.height.preferred = new_value 667 next_window_list.height.preferred = next_new_value # type: ignore 668 window_list.rebalance_window_heights() 669 next_window_list.rebalance_window_heights() 670 671 def toggle_pane(self, pane): 672 """Toggle a pane on or off.""" 673 window_list, _pane_index = ( 674 self._find_window_list_and_pane_index(pane)) 675 676 # Don't hide the window if tabbed mode is enabled. Switching to a 677 # separate tab is preffered. 678 if window_list.display_mode == DisplayMode.TABBED: 679 return 680 pane.show_pane = not pane.show_pane 681 self.update_root_container_body() 682 self.application.update_menu_items() 683 684 # Set focus to the top level menu. This has the effect of keeping the 685 # menu open if it's already open. 686 self.application.focus_main_menu() 687 688 def focus_first_visible_pane(self): 689 """Focus on the first visible container.""" 690 for pane in self.active_panes(): 691 if pane.show_pane: 692 self.application.application.layout.focus(pane) 693 break 694 695 def check_for_all_hidden_panes_and_unhide(self) -> None: 696 """Scan for window_lists containing only hidden panes.""" 697 for window_list in self.window_lists: 698 all_hidden = all(not pane.show_pane 699 for pane in window_list.active_panes) 700 if all_hidden: 701 # Unhide the first pane 702 self.toggle_pane(window_list.active_panes[0]) 703 704 def add_pane_no_checks(self, pane: Any): 705 self.window_lists[0].add_pane_no_checks(pane) 706 707 def add_pane(self, pane: Any): 708 self.window_lists[0].add_pane(pane, add_at_beginning=True) 709 710 def first_window_list(self): 711 return self.window_lists[0] 712 713 def active_panes(self): 714 """Return all active panes from all window lists.""" 715 return chain.from_iterable( 716 map(operator.attrgetter('active_panes'), self.window_lists)) 717 718 def start_resize_pane(self, pane): 719 window_list, pane_index = self._find_window_list_and_pane_index(pane) 720 window_list.start_resize(pane, pane_index) 721 722 def mouse_resize(self, xpos, ypos): 723 if self.resize_target_window_list_index is None: 724 return 725 target_window_list = self.window_lists[ 726 self.resize_target_window_list_index] 727 728 diff = ypos - self.resize_current_row 729 if self.vertical_window_list_spliting(): 730 diff = xpos - self.resize_current_column 731 if diff == 0: 732 return 733 734 self.adjust_split_size(target_window_list, diff) 735 self._resize_update_current_row_column() 736 self.application.redraw_ui() 737 738 def mouse_handler(self, mouse_event: MouseEvent): 739 """MouseHandler used when resize_mode == True.""" 740 mouse_position = mouse_event.position 741 742 if (mouse_event.event_type == MouseEventType.MOUSE_MOVE 743 and mouse_event.button == MouseButton.LEFT): 744 self.mouse_resize(mouse_position.x, mouse_position.y) 745 elif mouse_event.event_type == MouseEventType.MOUSE_UP: 746 self.stop_resize() 747 # Mouse event handled, return None. 748 return None 749 else: 750 self.stop_resize() 751 752 # Mouse event not handled, return NotImplemented. 753 return NotImplemented 754 755 def _calculate_actual_widths(self) -> List[int]: 756 widths = [w.width.preferred for w in self.window_lists] 757 758 available_width = self.current_window_manager_width 759 # Subtract 1 for each separator 760 available_width -= len(self.window_lists) - 1 761 remaining_rows = available_width - sum(widths) 762 window_list_index = 0 763 # Distribute remaining unaccounted columns to each window in turn. 764 while remaining_rows > 0: 765 widths[window_list_index] += 1 766 remaining_rows -= 1 767 window_list_index = (window_list_index + 1) % len(widths) 768 769 return widths 770 771 def _calculate_actual_heights(self) -> List[int]: 772 heights = [w.height.preferred for w in self.window_lists] 773 774 available_height = self.current_window_manager_height 775 # Subtract 1 for each vertical separator 776 available_height -= len(self.window_lists) - 1 777 remaining_rows = available_height - sum(heights) 778 window_list_index = 0 779 # Distribute remaining unaccounted columns to each window in turn. 780 while remaining_rows > 0: 781 heights[window_list_index] += 1 782 remaining_rows -= 1 783 window_list_index = (window_list_index + 1) % len(heights) 784 785 return heights 786 787 def _resize_update_current_row_column(self) -> None: 788 if self.resize_target_window_list_index is None: 789 return 790 791 widths = self._calculate_actual_widths() 792 heights = self._calculate_actual_heights() 793 794 start_column = 0 795 start_row = 0 796 797 # Find the starting column 798 for i in range(self.resize_target_window_list_index + 1): 799 # If we are past the target window_list, exit the loop. 800 if i > self.resize_target_window_list_index: 801 break 802 start_column += widths[i] 803 start_row += heights[i] 804 if i < self.resize_target_window_list_index - 1: 805 start_column += 1 806 start_row += 1 807 808 self.resize_current_column = start_column 809 self.resize_current_row = start_row 810 811 def start_resize(self, window_list): 812 # Check the target window_list isn't the last one. 813 if window_list == self.window_lists[-1]: 814 return 815 816 list_index = self.window_list_index(window_list) 817 if list_index is None: 818 return 819 820 self.resize_mode = True 821 self.resize_target_window_list = window_list 822 self.resize_target_window_list_index = list_index 823 self._resize_update_current_row_column() 824 825 def stop_resize(self): 826 self.resize_mode = False 827 self.resize_target_window_list = None 828 self.resize_target_window_list_index = None 829 self.resize_current_row = 0 830 self.resize_current_column = 0 831 832 def _find_window_list_and_pane_index(self, pane: Any): 833 pane_index = None 834 parent_window_list = None 835 for window_list in self.window_lists: 836 pane_index = window_list.pane_index(pane) 837 if pane_index is not None: 838 parent_window_list = window_list 839 break 840 return parent_window_list, pane_index 841 842 def remove_pane(self, existing_pane: Any): 843 window_list, _pane_index = ( 844 self._find_window_list_and_pane_index(existing_pane)) 845 if window_list: 846 window_list.remove_pane(existing_pane) 847 # Reset focus if this list is empty 848 if len(window_list.active_panes) == 0: 849 self.application.focus_main_menu() 850 851 def reset_pane_sizes(self): 852 for window_list in self.window_lists: 853 window_list.reset_pane_sizes() 854 855 def _remove_panes_from_layout( 856 self, pane_titles: Iterable[str]) -> Dict[str, Any]: 857 # Gather pane objects and remove them from the window layout. 858 collected_panes = {} 859 860 for window_list in self.window_lists: 861 # Make a copy of active_panes to prevent mutating the while 862 # iterating. 863 for pane in copy.copy(window_list.active_panes): 864 if pane.pane_title() in pane_titles: 865 collected_panes[pane.pane_title()] = ( 866 window_list.remove_pane_no_checks(pane)) 867 return collected_panes 868 869 def _set_pane_options(self, pane, options: dict) -> None: # pylint: disable=no-self-use 870 if options.get('hidden', False): 871 # Hide this pane 872 pane.show_pane = False 873 if options.get('height', False): 874 # Apply new height 875 new_height = options['height'] 876 assert isinstance(new_height, int) 877 pane.height.preferred = new_height 878 879 def _set_window_list_display_modes(self, prefs: ConsolePrefs) -> None: 880 # Set column display modes 881 for column_index, column_type in enumerate(prefs.window_column_modes): 882 mode = DisplayMode.STACK 883 if 'tabbed' in column_type: 884 mode = DisplayMode.TABBED 885 self.window_lists[column_index].set_display_mode(mode) 886 887 def _create_new_log_pane_with_loggers(self, window_title, window_options, 888 existing_pane_titles) -> LogPane: 889 if 'loggers' not in window_options: 890 error_unknown_window(window_title, existing_pane_titles) 891 892 new_pane = LogPane(application=self.application, 893 pane_title=window_title) 894 # Add logger handlers 895 for logger_name, logger_options in window_options.get('loggers', 896 {}).items(): 897 898 log_level_name = logger_options.get('level', None) 899 new_pane.add_log_handler(logger_name, level_name=log_level_name) 900 return new_pane 901 902 # TODO(tonymd): Split this large function up. 903 def apply_config(self, prefs: ConsolePrefs) -> None: 904 """Apply window configuration from loaded ConsolePrefs.""" 905 if not prefs.windows: 906 return 907 908 unique_titles = prefs.unique_window_titles 909 collected_panes = self._remove_panes_from_layout(unique_titles) 910 existing_pane_titles = [ 911 p.pane_title() for p in collected_panes.values() 912 if isinstance(p, LogPane) 913 ] 914 915 # Keep track of original non-duplicated pane titles 916 already_added_panes = [] 917 918 for column_index, column in enumerate(prefs.windows.items()): # pylint: disable=too-many-nested-blocks 919 _column_type, windows = column 920 # Add a new window_list if needed 921 if column_index >= len(self.window_lists): 922 self.window_lists.append(WindowList(self)) 923 924 # Set column display mode to stacked by default. 925 self.window_lists[column_index].display_mode = DisplayMode.STACK 926 927 # Add windows to the this column (window_list) 928 for window_title, window_dict in windows.items(): 929 window_options = window_dict if window_dict else {} 930 new_pane = None 931 desired_window_title = window_title 932 # Check for duplicate_of: Title value 933 window_title = window_options.get('duplicate_of', window_title) 934 935 # Check if this pane is brand new, ready to be added, or should 936 # be duplicated. 937 if (window_title not in already_added_panes 938 and window_title not in collected_panes): 939 # New pane entirely 940 new_pane = self._create_new_log_pane_with_loggers( 941 window_title, window_options, existing_pane_titles) 942 943 elif window_title not in already_added_panes: 944 # First time adding this pane 945 already_added_panes.append(window_title) 946 new_pane = collected_panes[window_title] 947 948 elif window_title in collected_panes: 949 # Pane added once, duplicate it 950 new_pane = collected_panes[window_title].create_duplicate() 951 # Rename this duplicate pane 952 assert isinstance(new_pane, LogPane) 953 new_pane.set_pane_title(desired_window_title) 954 955 if new_pane: 956 # Set window size and visibility 957 self._set_pane_options(new_pane, window_options) 958 # Add the new pane 959 self.window_lists[column_index].add_pane_no_checks( 960 new_pane) 961 # Apply log filters 962 if isinstance(new_pane, LogPane): 963 new_pane.apply_filters_from_config(window_options) 964 965 # Update column display modes. 966 self._set_window_list_display_modes(prefs) 967 # Check for columns where all panes are hidden and unhide at least one. 968 self.check_for_all_hidden_panes_and_unhide() 969 970 # Update prompt_toolkit containers. 971 self.update_root_container_body() 972 self.application.update_menu_items() 973 974 # Focus on the first visible pane. 975 self.focus_first_visible_pane() 976 977 def create_window_menu(self): 978 """Build the [Window] menu for the current set of window lists.""" 979 root_menu_items = [] 980 for window_list_index, window_list in enumerate(self.window_lists): 981 menu_items = [] 982 menu_items.append( 983 MenuItem( 984 'Column {index} View Modes'.format( 985 index=window_list_index + 1), 986 children=[ 987 MenuItem( 988 '{check} {display_mode} Windows'.format( 989 display_mode=display_mode.value, 990 check=pw_console.widgets.checkbox. 991 to_checkbox_text( 992 window_list.display_mode == display_mode, 993 end='', 994 )), 995 handler=functools.partial( 996 window_list.set_display_mode, display_mode), 997 ) for display_mode in DisplayMode 998 ], 999 )) 1000 menu_items.extend( 1001 MenuItem( 1002 '{index}: {title}'.format( 1003 index=pane_index + 1, 1004 title=pane.menu_title(), 1005 ), 1006 children=[ 1007 MenuItem( 1008 '{check} Show/Hide Window'.format( 1009 check=pw_console.widgets.checkbox. 1010 to_checkbox_text(pane.show_pane, end='')), 1011 handler=functools.partial(self.toggle_pane, pane), 1012 ), 1013 ] + [ 1014 MenuItem(text, 1015 handler=functools.partial( 1016 self.application.run_pane_menu_option, 1017 handler)) 1018 for text, handler in pane.get_all_menu_options() 1019 ], 1020 ) for pane_index, pane in enumerate(window_list.active_panes)) 1021 if window_list_index + 1 < len(self.window_lists): 1022 menu_items.append(MenuItem('-')) 1023 root_menu_items.extend(menu_items) 1024 1025 menu = MenuItem( 1026 '[Windows]', 1027 children=root_menu_items, 1028 ) 1029 1030 return [menu] 1031