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