| # Copyright 2021 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """WindowManager""" |
| |
| import collections |
| import copy |
| import functools |
| from itertools import chain |
| import logging |
| import operator |
| from typing import Any, Dict, Iterable, List, Optional |
| |
| from prompt_toolkit.key_binding import KeyBindings |
| from prompt_toolkit.layout import ( |
| Dimension, |
| HSplit, |
| VSplit, |
| FormattedTextControl, |
| Window, |
| WindowAlign, |
| ) |
| from prompt_toolkit.mouse_events import MouseEvent, MouseEventType, MouseButton |
| from prompt_toolkit.widgets import MenuItem |
| |
| from pw_console.console_prefs import ConsolePrefs, error_unknown_window |
| from pw_console.log_pane import LogPane |
| import pw_console.widgets.checkbox |
| from pw_console.widgets import WindowPaneToolbar |
| import pw_console.widgets.mouse_handlers |
| from pw_console.window_list import WindowList, DisplayMode |
| |
| _LOG = logging.getLogger(__package__) |
| |
| # Amount for adjusting window dimensions when enlarging and shrinking. |
| _WINDOW_SPLIT_ADJUST = 1 |
| |
| |
| class WindowListResizeHandle(FormattedTextControl): |
| """Button to initiate window list resize drag events.""" |
| def __init__(self, window_manager, window_list: Any, *args, |
| **kwargs) -> None: |
| self.window_manager = window_manager |
| self.window_list = window_list |
| super().__init__(*args, **kwargs) |
| |
| def mouse_handler(self, mouse_event: MouseEvent): |
| """Mouse handler for this control.""" |
| # Start resize mouse drag event |
| if mouse_event.event_type == MouseEventType.MOUSE_DOWN: |
| self.window_manager.start_resize(self.window_list) |
| # Mouse event handled, return None. |
| return None |
| |
| # Mouse event not handled, return NotImplemented. |
| return NotImplemented |
| |
| |
| class WindowManagerVSplit(VSplit): |
| """PromptToolkit VSplit class with some additions for size and mouse resize. |
| |
| This VSplit has a write_to_screen function that saves the width and height |
| of the container for the current render pass. It also handles overriding |
| mouse handlers for triggering window resize adjustments. |
| """ |
| def __init__(self, parent_window_manager, *args, **kwargs): |
| # Save a reference to the parent window pane. |
| self.parent_window_manager = parent_window_manager |
| super().__init__(*args, **kwargs) |
| |
| def write_to_screen( |
| self, |
| screen, |
| mouse_handlers, |
| write_position, |
| parent_style: str, |
| erase_bg: bool, |
| z_index: Optional[int], |
| ) -> None: |
| new_mouse_handlers = mouse_handlers |
| # Is resize mode active? |
| if self.parent_window_manager.resize_mode: |
| # Ignore future mouse_handler updates. |
| new_mouse_handlers = ( |
| pw_console.widgets.mouse_handlers.EmptyMouseHandler()) |
| # Set existing mouse_handlers to the parent_window_managers's |
| # mouse_handler. This will handle triggering resize events. |
| mouse_handlers.set_mouse_handler_for_range( |
| write_position.xpos, |
| write_position.xpos + write_position.width, |
| write_position.ypos, |
| write_position.ypos + write_position.height, |
| self.parent_window_manager.mouse_handler) |
| |
| # Save the width and height for the current render pass. |
| self.parent_window_manager.update_window_manager_size( |
| write_position.width, write_position.height) |
| # Continue writing content to the screen. |
| super().write_to_screen(screen, new_mouse_handlers, write_position, |
| parent_style, erase_bg, z_index) |
| |
| |
| class WindowManagerHSplit(HSplit): |
| """PromptToolkit HSplit class with some additions for size and mouse resize. |
| |
| This HSplit has a write_to_screen function that saves the width and height |
| of the container for the current render pass. It also handles overriding |
| mouse handlers for triggering window resize adjustments. |
| """ |
| def __init__(self, parent_window_manager, *args, **kwargs): |
| # Save a reference to the parent window pane. |
| self.parent_window_manager = parent_window_manager |
| super().__init__(*args, **kwargs) |
| |
| def write_to_screen( |
| self, |
| screen, |
| mouse_handlers, |
| write_position, |
| parent_style: str, |
| erase_bg: bool, |
| z_index: Optional[int], |
| ) -> None: |
| new_mouse_handlers = mouse_handlers |
| # Is resize mode active? |
| if self.parent_window_manager.resize_mode: |
| # Ignore future mouse_handler updates. |
| new_mouse_handlers = ( |
| pw_console.widgets.mouse_handlers.EmptyMouseHandler()) |
| # Set existing mouse_handlers to the parent_window_managers's |
| # mouse_handler. This will handle triggering resize events. |
| mouse_handlers.set_mouse_handler_for_range( |
| write_position.xpos, |
| write_position.xpos + write_position.width, |
| write_position.ypos, |
| write_position.ypos + write_position.height, |
| self.parent_window_manager.mouse_handler) |
| |
| # Save the width and height for the current render pass. |
| self.parent_window_manager.update_window_manager_size( |
| write_position.width, write_position.height) |
| # Continue writing content to the screen. |
| super().write_to_screen(screen, new_mouse_handlers, write_position, |
| parent_style, erase_bg, z_index) |
| |
| |
| class WindowManager: |
| """WindowManager class |
| |
| This class handles adding/removing/resizing windows and rendering the |
| prompt_toolkit split layout.""" |
| |
| # pylint: disable=too-many-public-methods,too-many-instance-attributes |
| |
| def __init__( |
| self, |
| application: Any, |
| ): |
| self.application = application |
| self.window_lists: collections.deque = collections.deque() |
| self.window_lists.append(WindowList(self)) |
| self.key_bindings = self._create_key_bindings() |
| self.top_toolbars: List[WindowPaneToolbar] = [] |
| self.bottom_toolbars: List[WindowPaneToolbar] = [] |
| |
| self.resize_mode: bool = False |
| self.resize_target_window_list_index: Optional[int] = None |
| self.resize_target_window_list: Optional[int] = None |
| self.resize_current_row: int = 0 |
| self.resize_current_column: int = 0 |
| |
| self.current_window_manager_width: int = 0 |
| self.current_window_manager_height: int = 0 |
| self.last_window_manager_width: int = 0 |
| self.last_window_manager_height: int = 0 |
| |
| def update_window_manager_size(self, width, height): |
| """Save width and height for the current UI render pass.""" |
| if width: |
| self.last_window_manager_width = self.current_window_manager_width |
| self.current_window_manager_width = width |
| if height: |
| self.last_window_manager_height = self.current_window_manager_height |
| self.current_window_manager_height = height |
| |
| if (self.current_window_manager_width != self.last_window_manager_width |
| or self.current_window_manager_height != |
| self.last_window_manager_height): |
| self.rebalance_window_list_sizes() |
| |
| def _set_window_list_sizes(self, new_heights: List[int], |
| new_widths: List[int]) -> None: |
| for window_list in self.window_lists: |
| window_list.height = Dimension(preferred=new_heights[0]) |
| new_heights = new_heights[1:] |
| window_list.width = Dimension(preferred=new_widths[0]) |
| new_widths = new_widths[1:] |
| |
| def vertical_window_list_spliting(self) -> bool: |
| return self.application.prefs.window_column_split_method == 'vertical' |
| |
| def rebalance_window_list_sizes(self) -> None: |
| """Adjust relative split sizes to fill available space.""" |
| available_height = self.current_window_manager_height |
| available_width = self.current_window_manager_width |
| |
| old_heights = [w.height.preferred for w in self.window_lists] |
| old_widths = [w.width.preferred for w in self.window_lists] |
| |
| # Make sure the old totals are not zero. |
| old_height_total = max(sum(old_heights), 1) |
| old_width_total = max(sum(old_widths), 1) |
| |
| height_percentages = [ |
| value / old_height_total for value in old_heights |
| ] |
| width_percentages = [value / old_width_total for value in old_widths] |
| |
| new_heights = [ |
| int(available_height * percentage) |
| for percentage in height_percentages |
| ] |
| new_widths = [ |
| int(available_width * percentage) |
| for percentage in width_percentages |
| ] |
| |
| if self.vertical_window_list_spliting(): |
| new_heights = [ |
| self.current_window_manager_height for h in new_heights |
| ] |
| else: |
| new_widths = [ |
| self.current_window_manager_width for h in new_widths |
| ] |
| |
| self._set_window_list_sizes(new_heights, new_widths) |
| |
| def _create_key_bindings(self) -> KeyBindings: |
| key_bindings = KeyBindings() |
| register = self.application.prefs.register_keybinding |
| |
| @register('window-manager.move-pane-left', key_bindings) |
| def move_pane_left(_event): |
| """Move window pane left.""" |
| self.move_pane_left() |
| |
| @register('window-manager.move-pane-right', key_bindings) |
| def move_pane_right(_event): |
| """Move window pane right.""" |
| self.move_pane_right() |
| |
| @register('window-manager.move-pane-down', key_bindings) |
| def move_pane_down(_event): |
| """Move window pane down.""" |
| self.move_pane_down() |
| |
| @register('window-manager.move-pane-up', key_bindings) |
| def move_pane_up(_event): |
| """Move window pane up.""" |
| self.move_pane_up() |
| |
| @register('window-manager.enlarge-pane', key_bindings) |
| def enlarge_pane(_event): |
| """Enlarge the active window pane.""" |
| self.enlarge_pane() |
| |
| @register('window-manager.shrink-pane', key_bindings) |
| def shrink_pane(_event): |
| """Shrink the active window pane.""" |
| self.shrink_pane() |
| |
| @register('window-manager.shrink-split', key_bindings) |
| def shrink_split(_event): |
| """Shrink the current window split.""" |
| self.shrink_split() |
| |
| @register('window-manager.enlarge-split', key_bindings) |
| def enlarge_split(_event): |
| """Enlarge the current window split.""" |
| self.enlarge_split() |
| |
| @register('window-manager.focus-prev-pane', key_bindings) |
| def focus_prev_pane(_event): |
| """Switch focus to the previous window pane or tab.""" |
| self.focus_previous_pane() |
| |
| @register('window-manager.focus-next-pane', key_bindings) |
| def focus_next_pane(_event): |
| """Switch focus to the next window pane or tab.""" |
| self.focus_next_pane() |
| |
| @register('window-manager.balance-window-panes', key_bindings) |
| def balance_window_panes(_event): |
| """Balance all window sizes.""" |
| self.balance_window_sizes() |
| |
| return key_bindings |
| |
| def delete_empty_window_lists(self): |
| empty_lists = [ |
| window_list for window_list in self.window_lists |
| if window_list.empty() |
| ] |
| for empty_list in empty_lists: |
| self.window_lists.remove(empty_list) |
| |
| def add_top_toolbar(self, toolbar: WindowPaneToolbar) -> None: |
| self.top_toolbars.append(toolbar) |
| |
| def add_bottom_toolbar(self, toolbar: WindowPaneToolbar) -> None: |
| self.bottom_toolbars.append(toolbar) |
| |
| def create_root_container(self): |
| """Create vertical or horizontal splits for all active panes.""" |
| self.delete_empty_window_lists() |
| |
| for window_list in self.window_lists: |
| window_list.update_container() |
| |
| vertical_split = self.vertical_window_list_spliting() |
| |
| window_containers = [] |
| for i, window_list in enumerate(self.window_lists): |
| window_containers.append(window_list.container) |
| if (i + 1) >= len(self.window_lists): |
| continue |
| |
| if vertical_split: |
| separator_padding = Window( |
| content=WindowListResizeHandle(self, window_list, "│"), |
| char='│', |
| width=1, |
| dont_extend_height=False, |
| ) |
| resize_separator = HSplit( |
| [ |
| separator_padding, |
| Window( |
| content=WindowListResizeHandle( |
| self, window_list, "║\n║\n║"), |
| char='│', |
| width=1, |
| dont_extend_height=True, |
| ), |
| separator_padding, |
| ], |
| style='class:pane_separator', |
| ) |
| else: |
| resize_separator = Window( |
| content=WindowListResizeHandle(self, window_list, "════"), |
| char='─', |
| height=1, |
| align=WindowAlign.CENTER, |
| dont_extend_width=False, |
| style='class:pane_separator', |
| ) |
| window_containers.append(resize_separator) |
| |
| if vertical_split: |
| split = WindowManagerVSplit(self, window_containers) |
| else: |
| split = WindowManagerHSplit(self, window_containers) |
| |
| split_items = [] |
| split_items.extend(self.top_toolbars) |
| split_items.append(split) |
| split_items.extend(self.bottom_toolbars) |
| return HSplit(split_items) |
| |
| def update_root_container_body(self): |
| # Replace the root MenuContainer body with the new split. |
| self.application.root_container.container.content.children[ |
| 1] = self.create_root_container() |
| |
| def _get_active_window_list_and_pane(self): |
| active_pane = None |
| active_window_list = None |
| for window_list in self.window_lists: |
| active_pane = window_list.get_current_active_pane() |
| if active_pane: |
| active_window_list = window_list |
| break |
| return active_window_list, active_pane |
| |
| def window_list_index(self, window_list: WindowList) -> Optional[int]: |
| index = None |
| try: |
| index = self.window_lists.index(window_list) |
| except ValueError: |
| # Ignore ValueError which can be raised by the self.window_lists |
| # deque if the window_list can't be found. |
| pass |
| return index |
| |
| def run_action_on_active_pane(self, function_name): |
| _active_window_list, active_pane = ( |
| self._get_active_window_list_and_pane()) |
| if not hasattr(active_pane, function_name): |
| return |
| method_to_call = getattr(active_pane, function_name) |
| method_to_call() |
| return |
| |
| def focus_previous_pane(self) -> None: |
| """Focus on the previous visible window pane or tab.""" |
| self.focus_next_pane(reverse_order=True) |
| |
| def focus_next_pane(self, reverse_order=False) -> None: |
| """Focus on the next visible window pane or tab.""" |
| active_window_list, active_pane = ( |
| self._get_active_window_list_and_pane()) |
| if active_window_list is None: |
| return |
| |
| # Total count of window lists and panes |
| window_list_count = len(self.window_lists) |
| pane_count = len(active_window_list.active_panes) |
| |
| # Get currently focused indices |
| active_window_list_index = self.window_list_index(active_window_list) |
| if active_window_list_index is None: |
| return |
| active_pane_index = active_window_list.pane_index(active_pane) |
| |
| increment = -1 if reverse_order else 1 |
| # Assume we can switch to the next pane in the current window_list |
| next_pane_index = active_pane_index + increment |
| |
| # Case 1: next_pane_index does not exist in this window list. |
| # Action: Switch to the first pane of the next window list. |
| if next_pane_index >= pane_count or next_pane_index < 0: |
| # Get the next window_list |
| next_window_list_index = ((active_window_list_index + increment) % |
| window_list_count) |
| next_window_list = self.window_lists[next_window_list_index] |
| |
| # If tabbed window mode is enabled, switch to the first tab. |
| if next_window_list.display_mode == DisplayMode.TABBED: |
| if reverse_order: |
| next_window_list.switch_to_tab( |
| len(next_window_list.active_panes) - 1) |
| else: |
| next_window_list.switch_to_tab(0) |
| return |
| |
| # Otherwise switch to the first visible window pane. |
| pane_list = next_window_list.active_panes |
| if reverse_order: |
| pane_list = reversed(pane_list) |
| for pane in pane_list: |
| if pane.show_pane: |
| self.application.focus_on_container(pane) |
| return |
| |
| # Case 2: next_pane_index does exist and display mode is tabs. |
| # Action: Switch to the next tab of the current window list. |
| if active_window_list.display_mode == DisplayMode.TABBED: |
| active_window_list.switch_to_tab(next_pane_index) |
| return |
| |
| # Case 3: next_pane_index does exist and display mode is stacked. |
| # Action: Switch to the next visible window pane. |
| index_range = range(1, pane_count) |
| if reverse_order: |
| index_range = range(pane_count - 1, 0, -1) |
| for i in index_range: |
| next_pane_index = (active_pane_index + i) % pane_count |
| next_pane = active_window_list.active_panes[next_pane_index] |
| if next_pane.show_pane: |
| self.application.focus_on_container(next_pane) |
| return |
| return |
| |
| def move_pane_left(self): |
| active_window_list, active_pane = ( |
| self._get_active_window_list_and_pane()) |
| if not active_window_list: |
| return |
| |
| window_list_index = self.window_list_index(active_window_list) |
| # Move left should pick the previous window_list |
| target_window_list_index = window_list_index - 1 |
| |
| # Check if a new WindowList should be created on the left |
| if target_window_list_index == -1: |
| # Add the new WindowList |
| target_window_list = WindowList(self) |
| self.window_lists.appendleft(target_window_list) |
| self.reset_split_sizes() |
| # New index is 0 |
| target_window_list_index = 0 |
| |
| # Get the destination window_list |
| target_window_list = self.window_lists[target_window_list_index] |
| |
| # Move the pane |
| active_window_list.remove_pane_no_checks(active_pane) |
| target_window_list.add_pane(active_pane, add_at_beginning=True) |
| target_window_list.reset_pane_sizes() |
| self.delete_empty_window_lists() |
| |
| def move_pane_right(self): |
| active_window_list, active_pane = ( |
| self._get_active_window_list_and_pane()) |
| if not active_window_list: |
| return |
| |
| window_list_index = self.window_list_index(active_window_list) |
| # Move right should pick the next window_list |
| target_window_list_index = window_list_index + 1 |
| |
| # Check if a new WindowList should be created |
| if target_window_list_index == len(self.window_lists): |
| # Add a new WindowList |
| target_window_list = WindowList(self) |
| self.window_lists.append(target_window_list) |
| self.reset_split_sizes() |
| |
| # Get the destination window_list |
| target_window_list = self.window_lists[target_window_list_index] |
| |
| # Move the pane |
| active_window_list.remove_pane_no_checks(active_pane) |
| target_window_list.add_pane(active_pane, add_at_beginning=True) |
| target_window_list.reset_pane_sizes() |
| self.delete_empty_window_lists() |
| |
| def move_pane_up(self): |
| active_window_list, _active_pane = ( |
| self._get_active_window_list_and_pane()) |
| if not active_window_list: |
| return |
| |
| active_window_list.move_pane_up() |
| |
| def move_pane_down(self): |
| active_window_list, _active_pane = ( |
| self._get_active_window_list_and_pane()) |
| if not active_window_list: |
| return |
| |
| active_window_list.move_pane_down() |
| |
| def shrink_pane(self): |
| active_window_list, _active_pane = ( |
| self._get_active_window_list_and_pane()) |
| if not active_window_list: |
| return |
| |
| active_window_list.shrink_pane() |
| |
| def enlarge_pane(self): |
| active_window_list, _active_pane = ( |
| self._get_active_window_list_and_pane()) |
| if not active_window_list: |
| return |
| |
| active_window_list.enlarge_pane() |
| |
| def shrink_split(self): |
| if len(self.window_lists) < 2: |
| return |
| |
| active_window_list, _active_pane = ( |
| self._get_active_window_list_and_pane()) |
| if not active_window_list: |
| return |
| |
| self.adjust_split_size(active_window_list, -_WINDOW_SPLIT_ADJUST) |
| |
| def enlarge_split(self): |
| active_window_list, _active_pane = ( |
| self._get_active_window_list_and_pane()) |
| if not active_window_list: |
| return |
| |
| self.adjust_split_size(active_window_list, _WINDOW_SPLIT_ADJUST) |
| |
| def balance_window_sizes(self): |
| """Reset all splits and pane sizes.""" |
| self.reset_pane_sizes() |
| self.reset_split_sizes() |
| |
| def reset_split_sizes(self): |
| """Reset all active pane width and height to defaults""" |
| available_height = self.current_window_manager_height |
| available_width = self.current_window_manager_width |
| old_heights = [w.height.preferred for w in self.window_lists] |
| old_widths = [w.width.preferred for w in self.window_lists] |
| new_heights = [int(available_height / len(old_heights)) |
| ] * len(old_heights) |
| new_widths = [int(available_width / len(old_widths))] * len(old_widths) |
| |
| self._set_window_list_sizes(new_heights, new_widths) |
| |
| def _get_next_window_list_for_resizing( |
| self, window_list: WindowList) -> Optional[WindowList]: |
| window_list_index = self.window_list_index(window_list) |
| if window_list_index is None: |
| return None |
| |
| next_window_list_index = ((window_list_index + 1) % |
| len(self.window_lists)) |
| |
| # Use the previous window if we are on the last split |
| if window_list_index == len(self.window_lists) - 1: |
| next_window_list_index = window_list_index - 1 |
| |
| next_window_list = self.window_lists[next_window_list_index] |
| return next_window_list |
| |
| def adjust_split_size(self, |
| window_list: WindowList, |
| diff: int = _WINDOW_SPLIT_ADJUST) -> None: |
| """Increase or decrease a given window_list's vertical split width.""" |
| # No need to resize if only one split. |
| if len(self.window_lists) < 2: |
| return |
| |
| # Get the next split to subtract from. |
| next_window_list = self._get_next_window_list_for_resizing(window_list) |
| if not next_window_list: |
| return |
| |
| if self.vertical_window_list_spliting(): |
| # Get current width |
| old_value = window_list.width.preferred |
| next_old_value = next_window_list.width.preferred # type: ignore |
| else: |
| # Get current height |
| old_value = window_list.height.preferred |
| next_old_value = next_window_list.height.preferred # type: ignore |
| |
| # Add to the current split |
| new_value = old_value + diff |
| if new_value <= 0: |
| new_value = old_value |
| |
| # Subtract from the next split |
| next_new_value = next_old_value - diff |
| if next_new_value <= 0: |
| next_new_value = next_old_value |
| |
| # If new height is too small or no change, make no adjustments. |
| if new_value < 3 or next_new_value < 3 or old_value == new_value: |
| return |
| |
| if self.vertical_window_list_spliting(): |
| # Set new width |
| window_list.width.preferred = new_value |
| next_window_list.width.preferred = next_new_value # type: ignore |
| else: |
| # Set new height |
| window_list.height.preferred = new_value |
| next_window_list.height.preferred = next_new_value # type: ignore |
| window_list.rebalance_window_heights() |
| next_window_list.rebalance_window_heights() |
| |
| def toggle_pane(self, pane): |
| """Toggle a pane on or off.""" |
| window_list, _pane_index = ( |
| self._find_window_list_and_pane_index(pane)) |
| |
| # Don't hide the window if tabbed mode is enabled. Switching to a |
| # separate tab is preffered. |
| if window_list.display_mode == DisplayMode.TABBED: |
| return |
| pane.show_pane = not pane.show_pane |
| self.update_root_container_body() |
| self.application.update_menu_items() |
| |
| # Set focus to the top level menu. This has the effect of keeping the |
| # menu open if it's already open. |
| self.application.focus_main_menu() |
| |
| def focus_first_visible_pane(self): |
| """Focus on the first visible container.""" |
| for pane in self.active_panes(): |
| if pane.show_pane: |
| self.application.application.layout.focus(pane) |
| break |
| |
| def check_for_all_hidden_panes_and_unhide(self) -> None: |
| """Scan for window_lists containing only hidden panes.""" |
| for window_list in self.window_lists: |
| all_hidden = all(not pane.show_pane |
| for pane in window_list.active_panes) |
| if all_hidden: |
| # Unhide the first pane |
| self.toggle_pane(window_list.active_panes[0]) |
| |
| def add_pane_no_checks(self, pane: Any): |
| self.window_lists[0].add_pane_no_checks(pane) |
| |
| def add_pane(self, pane: Any): |
| self.window_lists[0].add_pane(pane, add_at_beginning=True) |
| |
| def first_window_list(self): |
| return self.window_lists[0] |
| |
| def active_panes(self): |
| """Return all active panes from all window lists.""" |
| return chain.from_iterable( |
| map(operator.attrgetter('active_panes'), self.window_lists)) |
| |
| def start_resize_pane(self, pane): |
| window_list, pane_index = self._find_window_list_and_pane_index(pane) |
| window_list.start_resize(pane, pane_index) |
| |
| def mouse_resize(self, xpos, ypos): |
| if self.resize_target_window_list_index is None: |
| return |
| target_window_list = self.window_lists[ |
| self.resize_target_window_list_index] |
| |
| diff = ypos - self.resize_current_row |
| if self.vertical_window_list_spliting(): |
| diff = xpos - self.resize_current_column |
| if diff == 0: |
| return |
| |
| self.adjust_split_size(target_window_list, diff) |
| self._resize_update_current_row_column() |
| self.application.redraw_ui() |
| |
| def mouse_handler(self, mouse_event: MouseEvent): |
| """MouseHandler used when resize_mode == True.""" |
| mouse_position = mouse_event.position |
| |
| if (mouse_event.event_type == MouseEventType.MOUSE_MOVE |
| and mouse_event.button == MouseButton.LEFT): |
| self.mouse_resize(mouse_position.x, mouse_position.y) |
| elif mouse_event.event_type == MouseEventType.MOUSE_UP: |
| self.stop_resize() |
| # Mouse event handled, return None. |
| return None |
| else: |
| self.stop_resize() |
| |
| # Mouse event not handled, return NotImplemented. |
| return NotImplemented |
| |
| def _calculate_actual_widths(self) -> List[int]: |
| widths = [w.width.preferred for w in self.window_lists] |
| |
| available_width = self.current_window_manager_width |
| # Subtract 1 for each separator |
| available_width -= len(self.window_lists) - 1 |
| remaining_rows = available_width - sum(widths) |
| window_list_index = 0 |
| # Distribute remaining unaccounted columns to each window in turn. |
| while remaining_rows > 0: |
| widths[window_list_index] += 1 |
| remaining_rows -= 1 |
| window_list_index = (window_list_index + 1) % len(widths) |
| |
| return widths |
| |
| def _calculate_actual_heights(self) -> List[int]: |
| heights = [w.height.preferred for w in self.window_lists] |
| |
| available_height = self.current_window_manager_height |
| # Subtract 1 for each vertical separator |
| available_height -= len(self.window_lists) - 1 |
| remaining_rows = available_height - sum(heights) |
| window_list_index = 0 |
| # Distribute remaining unaccounted columns to each window in turn. |
| while remaining_rows > 0: |
| heights[window_list_index] += 1 |
| remaining_rows -= 1 |
| window_list_index = (window_list_index + 1) % len(heights) |
| |
| return heights |
| |
| def _resize_update_current_row_column(self) -> None: |
| if self.resize_target_window_list_index is None: |
| return |
| |
| widths = self._calculate_actual_widths() |
| heights = self._calculate_actual_heights() |
| |
| start_column = 0 |
| start_row = 0 |
| |
| # Find the starting column |
| for i in range(self.resize_target_window_list_index + 1): |
| # If we are past the target window_list, exit the loop. |
| if i > self.resize_target_window_list_index: |
| break |
| start_column += widths[i] |
| start_row += heights[i] |
| if i < self.resize_target_window_list_index - 1: |
| start_column += 1 |
| start_row += 1 |
| |
| self.resize_current_column = start_column |
| self.resize_current_row = start_row |
| |
| def start_resize(self, window_list): |
| # Check the target window_list isn't the last one. |
| if window_list == self.window_lists[-1]: |
| return |
| |
| list_index = self.window_list_index(window_list) |
| if list_index is None: |
| return |
| |
| self.resize_mode = True |
| self.resize_target_window_list = window_list |
| self.resize_target_window_list_index = list_index |
| self._resize_update_current_row_column() |
| |
| def stop_resize(self): |
| self.resize_mode = False |
| self.resize_target_window_list = None |
| self.resize_target_window_list_index = None |
| self.resize_current_row = 0 |
| self.resize_current_column = 0 |
| |
| def _find_window_list_and_pane_index(self, pane: Any): |
| pane_index = None |
| parent_window_list = None |
| for window_list in self.window_lists: |
| pane_index = window_list.pane_index(pane) |
| if pane_index is not None: |
| parent_window_list = window_list |
| break |
| return parent_window_list, pane_index |
| |
| def remove_pane(self, existing_pane: Any): |
| window_list, _pane_index = ( |
| self._find_window_list_and_pane_index(existing_pane)) |
| if window_list: |
| window_list.remove_pane(existing_pane) |
| # Reset focus if this list is empty |
| if len(window_list.active_panes) == 0: |
| self.application.focus_main_menu() |
| |
| def reset_pane_sizes(self): |
| for window_list in self.window_lists: |
| window_list.reset_pane_sizes() |
| |
| def _remove_panes_from_layout( |
| self, pane_titles: Iterable[str]) -> Dict[str, Any]: |
| # Gather pane objects and remove them from the window layout. |
| collected_panes = {} |
| |
| for window_list in self.window_lists: |
| # Make a copy of active_panes to prevent mutating the while |
| # iterating. |
| for pane in copy.copy(window_list.active_panes): |
| if pane.pane_title() in pane_titles: |
| collected_panes[pane.pane_title()] = ( |
| window_list.remove_pane_no_checks(pane)) |
| return collected_panes |
| |
| def _set_pane_options(self, pane, options: dict) -> None: # pylint: disable=no-self-use |
| if options.get('hidden', False): |
| # Hide this pane |
| pane.show_pane = False |
| if options.get('height', False): |
| # Apply new height |
| new_height = options['height'] |
| assert isinstance(new_height, int) |
| pane.height.preferred = new_height |
| |
| def _set_window_list_display_modes(self, prefs: ConsolePrefs) -> None: |
| # Set column display modes |
| for column_index, column_type in enumerate(prefs.window_column_modes): |
| mode = DisplayMode.STACK |
| if 'tabbed' in column_type: |
| mode = DisplayMode.TABBED |
| self.window_lists[column_index].set_display_mode(mode) |
| |
| def _create_new_log_pane_with_loggers(self, window_title, window_options, |
| existing_pane_titles) -> LogPane: |
| if 'loggers' not in window_options: |
| error_unknown_window(window_title, existing_pane_titles) |
| |
| new_pane = LogPane(application=self.application, |
| pane_title=window_title) |
| # Add logger handlers |
| for logger_name, logger_options in window_options.get('loggers', |
| {}).items(): |
| |
| log_level_name = logger_options.get('level', None) |
| new_pane.add_log_handler(logger_name, level_name=log_level_name) |
| return new_pane |
| |
| # TODO(tonymd): Split this large function up. |
| def apply_config(self, prefs: ConsolePrefs) -> None: |
| """Apply window configuration from loaded ConsolePrefs.""" |
| if not prefs.windows: |
| return |
| |
| unique_titles = prefs.unique_window_titles |
| collected_panes = self._remove_panes_from_layout(unique_titles) |
| existing_pane_titles = [ |
| p.pane_title() for p in collected_panes.values() |
| if isinstance(p, LogPane) |
| ] |
| |
| # Keep track of original non-duplicated pane titles |
| already_added_panes = [] |
| |
| for column_index, column in enumerate(prefs.windows.items()): # pylint: disable=too-many-nested-blocks |
| _column_type, windows = column |
| # Add a new window_list if needed |
| if column_index >= len(self.window_lists): |
| self.window_lists.append(WindowList(self)) |
| |
| # Set column display mode to stacked by default. |
| self.window_lists[column_index].display_mode = DisplayMode.STACK |
| |
| # Add windows to the this column (window_list) |
| for window_title, window_dict in windows.items(): |
| window_options = window_dict if window_dict else {} |
| new_pane = None |
| desired_window_title = window_title |
| # Check for duplicate_of: Title value |
| window_title = window_options.get('duplicate_of', window_title) |
| |
| # Check if this pane is brand new, ready to be added, or should |
| # be duplicated. |
| if (window_title not in already_added_panes |
| and window_title not in collected_panes): |
| # New pane entirely |
| new_pane = self._create_new_log_pane_with_loggers( |
| window_title, window_options, existing_pane_titles) |
| |
| elif window_title not in already_added_panes: |
| # First time adding this pane |
| already_added_panes.append(window_title) |
| new_pane = collected_panes[window_title] |
| |
| elif window_title in collected_panes: |
| # Pane added once, duplicate it |
| new_pane = collected_panes[window_title].create_duplicate() |
| # Rename this duplicate pane |
| assert isinstance(new_pane, LogPane) |
| new_pane.set_pane_title(desired_window_title) |
| |
| if new_pane: |
| # Set window size and visibility |
| self._set_pane_options(new_pane, window_options) |
| # Add the new pane |
| self.window_lists[column_index].add_pane_no_checks( |
| new_pane) |
| # Apply log filters |
| if isinstance(new_pane, LogPane): |
| new_pane.apply_filters_from_config(window_options) |
| |
| # Update column display modes. |
| self._set_window_list_display_modes(prefs) |
| # Check for columns where all panes are hidden and unhide at least one. |
| self.check_for_all_hidden_panes_and_unhide() |
| |
| # Update prompt_toolkit containers. |
| self.update_root_container_body() |
| self.application.update_menu_items() |
| |
| # Focus on the first visible pane. |
| self.focus_first_visible_pane() |
| |
| def create_window_menu(self): |
| """Build the [Window] menu for the current set of window lists.""" |
| root_menu_items = [] |
| for window_list_index, window_list in enumerate(self.window_lists): |
| menu_items = [] |
| menu_items.append( |
| MenuItem( |
| 'Column {index} View Modes'.format( |
| index=window_list_index + 1), |
| children=[ |
| MenuItem( |
| '{check} {display_mode} Windows'.format( |
| display_mode=display_mode.value, |
| check=pw_console.widgets.checkbox. |
| to_checkbox_text( |
| window_list.display_mode == display_mode, |
| end='', |
| )), |
| handler=functools.partial( |
| window_list.set_display_mode, display_mode), |
| ) for display_mode in DisplayMode |
| ], |
| )) |
| menu_items.extend( |
| MenuItem( |
| '{index}: {title}'.format( |
| index=pane_index + 1, |
| title=pane.menu_title(), |
| ), |
| children=[ |
| MenuItem( |
| '{check} Show/Hide Window'.format( |
| check=pw_console.widgets.checkbox. |
| to_checkbox_text(pane.show_pane, end='')), |
| handler=functools.partial(self.toggle_pane, pane), |
| ), |
| ] + [ |
| MenuItem(text, |
| handler=functools.partial( |
| self.application.run_pane_menu_option, |
| handler)) |
| for text, handler in pane.get_all_menu_options() |
| ], |
| ) for pane_index, pane in enumerate(window_list.active_panes)) |
| if window_list_index + 1 < len(self.window_lists): |
| menu_items.append(MenuItem('-')) |
| root_menu_items.extend(menu_items) |
| |
| menu = MenuItem( |
| '[Windows]', |
| children=root_menu_items, |
| ) |
| |
| return [menu] |