blob: cbcd8bae7ebb2fc2173e0ba92b40789f3c6581d4 [file] [log] [blame]
# 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 functools
import operator
from itertools import chain
from typing import Any
from prompt_toolkit.layout import (
Dimension,
HSplit,
VSplit,
)
from prompt_toolkit.widgets import MenuItem
import pw_console.widgets.checkbox
from pw_console.window_list import WindowList, DisplayMode
# Weighted amount for adjusting window dimensions when enlarging and shrinking.
_WINDOW_SPLIT_ADJUST = 2
class WindowManager:
"""WindowManager class
This class handles adding/removing/resizing windows and rendering the
prompt_toolkit split layout."""
def __init__(
self,
application: Any,
):
self.application = application
self.window_lists: collections.deque = collections.deque()
self.window_lists.append(WindowList(self))
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 create_root_container(self):
"""Create a vertical or horizontal split container for all active
panes."""
self.delete_empty_window_lists()
for window_list in self.window_lists:
window_list.update_container()
return HSplit([
VSplit(
[window_list.container for window_list in self.window_lists],
padding=1,
padding_char='│',
padding_style='class:pane_separator',
# height=Dimension(weight=50),
# width=Dimension(weight=50),
)
])
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):
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 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)
# 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)
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)
# 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)
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 50%"""
for window_list in self.window_lists:
window_list.height = Dimension(weight=50)
window_list.width = Dimension(weight=50)
def adjust_split_size(self,
window_list: WindowList,
diff: int = _WINDOW_SPLIT_ADJUST):
"""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 a weight value from.
window_list_index = self.window_list_index(window_list)
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]
# Get current weight values
old_weight = window_list.width.weight
next_old_weight = next_window_list.width.weight # type: ignore
# Add to the current split
new_weight = old_weight + diff
if new_weight <= 0:
new_weight = old_weight
# Subtract from the next split
next_new_weight = next_old_weight - diff
if next_new_weight <= 0:
next_new_weight = next_old_weight
# Set new weight values
window_list.width.weight = new_weight
next_window_list.width.weight = next_new_weight # type: ignore
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 if tabbed mode is enabled, the container can't be rendered.
if window_list.display_mode == DisplayMode.TABBED:
return
pane.show_pane = not pane.show_pane
self.application.update_menu_items()
self.update_root_container_body()
# 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 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 _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)
def reset_pane_sizes(self):
for window_list in self.window_lists:
window_list.reset_pane_sizes()
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} {subtitle}'.format(
index=pane_index + 1,
title=pane.menu_title(),
subtitle=pane.pane_subtitle()),
children=[
MenuItem(
'{check} Show 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]