pw_console: WindowManager class
Refactor window management functions out of ConsoleApp.
This CL is just a move of the existing fuctions, no new
features or additional refactoring.
Test: Manual Window Management 1-9
Change-Id: I8f133e0c7d18921b54b88c6e8d2e51d195f41829
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/54960
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
Pigweed-Auto-Submit: Anthony DiGirolamo <tonymd@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_console/py/BUILD.gn b/pw_console/py/BUILD.gn
index b971a29..c1c0372 100644
--- a/pw_console/py/BUILD.gn
+++ b/pw_console/py/BUILD.gn
@@ -43,6 +43,7 @@
"pw_console/widgets/focus_on_click_overlay.py",
"pw_console/widgets/mouse_handlers.py",
"pw_console/widgets/table.py",
+ "pw_console/window_manager.py",
]
tests = [
"console_app_test.py",
diff --git a/pw_console/py/console_app_test.py b/pw_console/py/console_app_test.py
index 7c4384b..2a76853 100644
--- a/pw_console/py/console_app_test.py
+++ b/pw_console/py/console_app_test.py
@@ -48,40 +48,47 @@
console_app.add_log_handler(window_title, logger_instances)
# 4 panes, 3 for the loggers and 1 for the repl.
- self.assertEqual(len(console_app.active_panes), 4)
+ self.assertEqual(len(console_app.window_manager.active_panes), 4)
# Bypass prompt_toolkit has_focus()
- console_app._get_current_active_pane = MagicMock( # type: ignore
- return_value=console_app.active_panes[0])
+ console_app.window_manager._get_current_active_pane = (
+ MagicMock( # type: ignore
+ return_value=console_app.window_manager.active_panes[0]))
# Shrink the first pane
- console_app.shrink_pane()
- self.assertEqual(
- [pane.height.weight for pane in console_app.active_panes],
- [48, 52, 50, 50])
+ console_app.window_manager.shrink_pane()
+ self.assertEqual([
+ pane.height.weight
+ for pane in console_app.window_manager.active_panes
+ ], [48, 52, 50, 50])
# Reset pane sizes
- console_app.reset_pane_sizes()
- self.assertEqual(
- [pane.height.weight for pane in console_app.active_panes],
- [50, 50, 50, 50])
+ console_app.window_manager.reset_pane_sizes()
+ self.assertEqual([
+ pane.height.weight
+ for pane in console_app.window_manager.active_panes
+ ], [50, 50, 50, 50])
# Shrink last pane
- console_app._get_current_active_pane = MagicMock( # type: ignore
- return_value=console_app.active_panes[3])
- console_app.shrink_pane()
- self.assertEqual(
- [pane.height.weight for pane in console_app.active_panes],
- [50, 50, 52, 48])
+ console_app.window_manager._get_current_active_pane = (
+ MagicMock( # type: ignore
+ return_value=console_app.window_manager.active_panes[3]))
+ console_app.window_manager.shrink_pane()
+ self.assertEqual([
+ pane.height.weight
+ for pane in console_app.window_manager.active_panes
+ ], [50, 50, 52, 48])
# Enlarge second pane
- console_app._get_current_active_pane = MagicMock( # type: ignore
- return_value=console_app.active_panes[1])
- console_app.enlarge_pane()
- console_app.enlarge_pane()
- self.assertEqual(
- [pane.height.weight for pane in console_app.active_panes],
- [50, 54, 48, 48])
+ console_app.window_manager._get_current_active_pane = (
+ MagicMock( # type: ignore
+ return_value=console_app.window_manager.active_panes[1]))
+ console_app.window_manager.enlarge_pane()
+ console_app.window_manager.enlarge_pane()
+ self.assertEqual([
+ pane.height.weight
+ for pane in console_app.window_manager.active_panes
+ ], [50, 54, 48, 48])
def test_multiple_loggers_in_one_pane(self) -> None:
"""Test window resizing."""
@@ -100,13 +107,17 @@
console_app.add_log_handler(window_title, logger_instances)
# Two panes, one for the loggers and one for the repl.
- self.assertEqual(len(console_app.active_panes), 2)
+ self.assertEqual(len(console_app.window_manager.active_panes), 2)
- self.assertEqual(console_app.active_panes[0].pane_title(), 'Logs')
- self.assertEqual(console_app.active_panes[0]._pane_subtitle,
- 'test_log1, test_log2, test_log3')
- self.assertEqual(console_app.active_panes[0].pane_subtitle(),
- 'test_log1 + 3 more')
+ self.assertEqual(
+ console_app.window_manager.active_panes[0].pane_title(),
+ 'Logs')
+ self.assertEqual(
+ console_app.window_manager.active_panes[0]._pane_subtitle,
+ 'test_log1, test_log2, test_log3')
+ self.assertEqual(
+ console_app.window_manager.active_panes[0].pane_subtitle(),
+ 'test_log1 + 3 more')
if __name__ == '__main__':
diff --git a/pw_console/py/pw_console/console_app.py b/pw_console/py/pw_console/console_app.py
index abf6073..16a9c2f 100644
--- a/pw_console/py/pw_console/console_app.py
+++ b/pw_console/py/pw_console/console_app.py
@@ -13,8 +13,6 @@
# the License.
"""ConsoleApp control class."""
-import collections
-import collections.abc
import builtins
import asyncio
import logging
@@ -25,18 +23,15 @@
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.application import Application
-from prompt_toolkit.filters import Condition, has_focus
+from prompt_toolkit.filters import Condition
from prompt_toolkit.styles import (
DynamicStyle,
merge_styles,
)
from prompt_toolkit.layout import (
ConditionalContainer,
- Dimension,
Float,
- HSplit,
Layout,
- VSplit,
)
from prompt_toolkit.widgets import FormattedTextToolbar
from prompt_toolkit.widgets import (
@@ -61,6 +56,7 @@
from pw_console.log_pane import LogPane
from pw_console.pw_ptpython_repl import PwPtPythonRepl
from pw_console.repl_pane import ReplPane
+from pw_console.window_manager import WindowManager
_LOG = logging.getLogger(__package__)
@@ -98,10 +94,6 @@
logger_instance.name)
-# Weighted amount for adjusting window dimensions when enlarging and shrinking.
-_WINDOW_SIZE_ADJUST = 2
-
-
class ConsoleApp:
"""The main ConsoleApp class that glues everything together."""
@@ -153,7 +145,6 @@
'Ctrl-W', 'Quit '))
# Top level UI state toggles.
- self.vertical_split = False
self.load_theme()
# Pigweed upstream RST user guide
@@ -187,13 +178,9 @@
startup_message=repl_startup_message,
)
- # List of enabled panes.
- self.active_panes: collections.deque = collections.deque()
- self.active_panes.append(self.repl_pane)
-
- # Reference to the current prompt_toolkit window split for the current
- # set of active_panes.
- self.active_pane_split = None
+ # Window panes are added via the window_manager
+ self.window_manager = WindowManager(self)
+ self.window_manager.add_window(self.repl_pane)
# Top of screen menu items
self.menu_items = self._create_menu_items()
@@ -206,7 +193,7 @@
# prompt_toolkit root container.
self.root_container = MenuContainer(
- body=self._create_root_split(),
+ body=self.window_manager.create_root_split(),
menu_items=self.menu_items,
floats=[
# Top message bar
@@ -321,11 +308,13 @@
MenuItem(
'[View]',
children=[
- MenuItem('{check} Vertical Window Spliting'.format(
- check=pw_console.widgets.checkbox.to_checkbox_text(
- self.vertical_split)),
- handler=self.toggle_vertical_split),
- MenuItem('Rotate Window Order', handler=self.rotate_panes),
+ MenuItem(
+ '{check} Vertical Window Spliting'.format(
+ check=pw_console.widgets.checkbox.to_checkbox_text(
+ self.window_manager.vertical_split)),
+ handler=self.window_manager.toggle_vertical_split),
+ MenuItem('Rotate Window Order',
+ handler=self.window_manager.rotate_panes),
MenuItem('-'),
MenuItem(
'Themes',
@@ -393,7 +382,7 @@
check=pw_console.widgets.checkbox.
to_checkbox_text(pane.show_pane, end='')),
handler=functools.partial(
- self.toggle_pane, pane),
+ self.window_manager.toggle_pane, pane),
),
] + [
MenuItem(text,
@@ -401,7 +390,8 @@
self._run_pane_menu_option, handler))
for text, handler in pane.get_all_menu_options()
],
- ) for index, pane in enumerate(self.active_panes)
+ ) for index, pane in enumerate(
+ self.window_manager.active_panes)
],
)
]
@@ -430,142 +420,6 @@
return file_and_view_menu + window_menu + help_menu
- def _get_current_active_pane(self):
- """Return the current active window pane."""
- focused_pane = None
- for pane in self.active_panes:
- if has_focus(pane)():
- focused_pane = pane
- break
- return focused_pane
-
- def add_pane(self, new_pane, existing_pane=None):
- existing_pane_index = None
- if existing_pane:
- try:
- existing_pane_index = self.active_panes.index(existing_pane)
- except ValueError:
- # Ignore ValueError which can be raised by the self.active_panes
- # deque if existing_pane can't be found.
- pass
- if existing_pane_index:
- self.active_panes.insert(new_pane, existing_pane_index + 1)
- else:
- self.active_panes.append(new_pane)
-
- self.update_menu_items()
- self._update_root_container_body()
-
- self.redraw_ui()
-
- def remove_pane(self, existing_pane):
- existing_pane_index = 0
- if not existing_pane:
- return
- try:
- existing_pane_index = self.active_panes.index(existing_pane)
- self.active_panes.remove(existing_pane)
- except ValueError:
- # Ignore ValueError which can be raised by the self.active_panes
- # deque if existing_pane can't be found.
- pass
-
- self.update_menu_items()
- self._update_root_container_body()
- if len(self.active_panes) > 0:
- existing_pane_index -= 1
- try:
- self.focus_on_container(self.active_panes[existing_pane_index])
- except ValueError:
- # ValueError will be raised if the the pane at
- # existing_pane_index can't be accessed.
- # Focus on the main menu if the existing pane is hidden.
- self.focus_main_menu()
-
- self.redraw_ui()
-
- def enlarge_pane(self):
- """Enlarge the currently focused window pane."""
- pane = self._get_current_active_pane()
- if pane:
- self.adjust_pane_size(pane, _WINDOW_SIZE_ADJUST)
-
- def shrink_pane(self):
- """Shrink the currently focused window pane."""
- pane = self._get_current_active_pane()
- if pane:
- self.adjust_pane_size(pane, -_WINDOW_SIZE_ADJUST)
-
- def adjust_pane_size(self, pane, diff: int = _WINDOW_SIZE_ADJUST):
- """Increase or decrease a given pane's width or height weight."""
- # Placeholder next_pane value to allow setting width and height without
- # any consequences if there is no next visible pane.
- next_pane = HSplit([],
- height=Dimension(weight=50),
- width=Dimension(weight=50)) # type: ignore
- # Try to get the next visible pane to subtract a weight value from.
- next_visible_pane = self._get_next_visible_pane_after(pane)
- if next_visible_pane:
- next_pane = next_visible_pane
-
- # If the last pane is selected, and there are at least 2 panes, make
- # next_pane the previous pane.
- try:
- if len(self.active_panes) >= 2 and (self.active_panes.index(pane)
- == len(self.active_panes) - 1):
- next_pane = self.active_panes[-2]
- except ValueError:
- # Ignore ValueError raised if self.active_panes[-2] doesn't exist.
- pass
-
- # Get current weight values
- if self.vertical_split:
- old_weight = pane.width.weight
- next_old_weight = next_pane.width.weight # type: ignore
- else: # Horizontal split
- old_weight = pane.height.weight
- next_old_weight = next_pane.height.weight # type: ignore
-
- # Add to the current pane
- new_weight = old_weight + diff
- if new_weight <= 0:
- new_weight = old_weight
-
- # Subtract from the next pane
- next_new_weight = next_old_weight - diff
- if next_new_weight <= 0:
- next_new_weight = next_old_weight
-
- # Set new weight values
- if self.vertical_split:
- pane.width.weight = new_weight
- next_pane.width.weight = next_new_weight # type: ignore
- else: # Horizontal split
- pane.height.weight = new_weight
- next_pane.height.weight = next_new_weight # type: ignore
-
- def reset_pane_sizes(self):
- """Reset all active pane width and height to 50%"""
- for pane in self.active_panes:
- pane.height = Dimension(weight=50)
- pane.width = Dimension(weight=50)
-
- def rotate_panes(self, steps=1):
- """Rotate the order of all active window panes."""
- self.active_panes.rotate(steps)
- self.update_menu_items()
- self._update_root_container_body()
-
- def toggle_pane(self, pane):
- """Toggle a pane on or off."""
- pane.show_pane = not pane.show_pane
- self.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.focus_main_menu()
-
def focus_main_menu(self):
"""Set application focus to the main menu."""
self.application.layout.focus(self.root_container.window)
@@ -574,30 +428,6 @@
"""Set application focus to a specific container."""
self.application.layout.focus(pane)
- def _get_next_visible_pane_after(self, target_pane):
- """Return the next visible pane that appears after the target pane."""
- try:
- target_pane_index = self.active_panes.index(target_pane)
- except ValueError:
- # If pane can't be found, focus on the main menu.
- return None
-
- # Loop through active panes (not including the target_pane).
- for i in range(1, len(self.active_panes)):
- next_pane_index = (target_pane_index + i) % len(self.active_panes)
- next_pane = self.active_panes[next_pane_index]
- if next_pane.show_pane:
- return next_pane
- return None
-
- def focus_next_visible_pane(self, pane):
- """Focus on the next visible window pane if possible."""
- next_visible_pane = self._get_next_visible_pane_after(pane)
- if next_visible_pane:
- self.application.layout.focus(next_visible_pane)
- return
- self.focus_main_menu()
-
def toggle_light_theme(self):
"""Toggle light and dark theme colors."""
# Use ptpython's style_transformation to swap dark and light colors.
@@ -610,9 +440,9 @@
def _create_log_pane(self, title=None) -> 'LogPane':
# Create one log pane.
- self.active_panes.appendleft(
- LogPane(application=self, pane_title=title))
- return self.active_panes[0]
+ log_pane = LogPane(application=self, pane_title=title)
+ self.window_manager.add_pane(log_pane, add_at_beginning=True)
+ return log_pane
def add_log_handler(self,
window_title: str,
@@ -622,7 +452,7 @@
existing_log_pane = None
# Find an existing LogPane with the same window_title.
- for pane in self.active_panes:
+ for pane in self.window_manager.active_panes:
if isinstance(pane, LogPane) and pane.pane_title() == window_title:
existing_log_pane = pane
break
@@ -633,7 +463,7 @@
for logger in logger_instances:
_add_log_handler_to_pane(logger, existing_log_pane)
- self._update_root_container_body()
+ self.window_manager.update_root_container_body()
self.update_menu_items()
self._update_help_window()
@@ -644,7 +474,7 @@
def run_after_render_hooks(self, *unused_args, **unused_kwargs):
"""Run each active pane's `after_render_hook` if defined."""
- for pane in self.active_panes:
+ for pane in self.window_manager.active_panes:
if hasattr(pane, 'after_render_hook'):
pane.after_render_hook()
@@ -671,7 +501,7 @@
self.key_bindings)
# Add activated plugin key bindings to the help text.
- for pane in self.active_panes:
+ for pane in self.window_manager.active_panes:
for key_bindings in pane.get_all_key_bindings():
help_section_title = pane.__class__.__name__
if isinstance(key_bindings, KeyBindings):
@@ -683,46 +513,12 @@
self.keybind_help_window.generate_help_text()
- def _update_split_orientation(self):
- if self.vertical_split:
- self.active_pane_split = VSplit(
- list(pane for pane in self.active_panes if pane.show_pane),
- # Add a vertical separator between each active window pane.
- padding=1,
- padding_char='│',
- padding_style='class:pane_separator',
- )
- else:
- self.active_pane_split = HSplit(self.active_panes)
-
- def _create_root_split(self):
- """Create a vertical or horizontal split container for all active
- panes."""
- self._update_split_orientation()
- return HSplit([
- self.active_pane_split,
- ])
-
- def _update_root_container_body(self):
- # Replace the root MenuContainer body with the new split.
- self.root_container.container.content.children[
- 1] = self._create_root_split()
-
def toggle_log_line_wrapping(self):
"""Menu item handler to toggle line wrapping of all log panes."""
- for pane in self.active_panes:
+ for pane in self.window_manager.active_panes:
if isinstance(pane, LogPane):
pane.toggle_wrap_lines()
- def toggle_vertical_split(self):
- """Toggle visibility of the help window."""
- self.vertical_split = not self.vertical_split
-
- self.update_menu_items()
- self._update_root_container_body()
-
- self.redraw_ui()
-
def focused_window(self):
"""Return the currently focused window."""
return self.application.layout.current_window
@@ -748,7 +544,7 @@
async def run(self, test_mode=False):
"""Start the prompt_toolkit UI."""
- self.reset_pane_sizes()
+ self.window_manager.reset_pane_sizes()
if test_mode:
background_log_task = asyncio.create_task(self.log_forever())
diff --git a/pw_console/py/pw_console/key_bindings.py b/pw_console/py/pw_console/key_bindings.py
index f141920..a7a4de6 100644
--- a/pw_console/py/pw_console/key_bindings.py
+++ b/pw_console/py/pw_console/key_bindings.py
@@ -51,32 +51,32 @@
@bindings.add('f4')
def toggle_vertical_split(event):
"""Toggle horizontal and vertical window splitting."""
- console_app.toggle_vertical_split()
+ console_app.window_manager.toggle_vertical_split()
@bindings.add('c-s-left')
def rotate_panes(event):
"""Rotate window positions backward."""
- console_app.rotate_panes(-1)
+ console_app.window_manager.rotate_panes(-1)
@bindings.add('c-s-right')
def rotate_panes(event):
"""Rotate window positions forward."""
- console_app.rotate_panes()
+ console_app.window_manager.rotate_panes()
@bindings.add('c-j')
def enlarge_pane(event):
"""Enlarge the active window pane."""
- console_app.enlarge_pane()
+ console_app.window_manager.enlarge_pane()
@bindings.add('c-k')
def shrink_pane(event):
"""Shrink the active window pane."""
- console_app.shrink_pane()
+ console_app.window_manager.shrink_pane()
@bindings.add('c-u')
def balance_window_panes(event):
"""Balance all window pane sizes."""
- console_app.reset_pane_sizes()
+ console_app.window_manager.reset_pane_sizes()
@bindings.add('c-w')
@bindings.add('c-q')
diff --git a/pw_console/py/pw_console/log_pane.py b/pw_console/py/pw_console/log_pane.py
index ba43d92..c8c96da 100644
--- a/pw_console/py/pw_console/log_pane.py
+++ b/pw_console/py/pw_console/log_pane.py
@@ -98,18 +98,17 @@
"""Toggle table view."""
self.log_pane.toggle_table_view()
- # TODO(tonymd): Make this a menu option instead of a keybind.
@key_bindings.add('insert')
def _duplicate(_event: KeyPressEvent) -> None:
"""Duplicate this log pane."""
self.log_pane.duplicate()
- # TODO(tonymd): Make this a menu option instead of a keybind.
@key_bindings.add('delete')
def _delete(_event: KeyPressEvent) -> None:
"""Remove log pane."""
if self.log_pane.is_a_duplicate:
- self.log_pane.application.remove_pane(self.log_pane)
+ self.log_pane.application.window_manager.remove_pane(
+ self.log_pane)
@key_bindings.add('C')
def _clear_history(_event: KeyPressEvent) -> None:
@@ -538,7 +537,8 @@
if self.is_a_duplicate:
options += [(
'Remove pane',
- functools.partial(self.application.remove_pane, self),
+ functools.partial(self.application.window_manager.remove_pane,
+ self),
)]
# Search / Filter section
@@ -595,5 +595,5 @@
new_pane.is_a_duplicate = True
# Add the new pane.
- self.application.add_pane(new_pane)
+ self.application.window_manager.add_pane(new_pane)
return new_pane
diff --git a/pw_console/py/pw_console/window_manager.py b/pw_console/py/pw_console/window_manager.py
new file mode 100644
index 0000000..1020584
--- /dev/null
+++ b/pw_console/py/pw_console/window_manager.py
@@ -0,0 +1,248 @@
+# 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
+from typing import Any
+
+from prompt_toolkit.filters import has_focus
+from prompt_toolkit.layout import (
+ Dimension,
+ HSplit,
+ VSplit,
+)
+
+# Weighted amount for adjusting window dimensions when enlarging and shrinking.
+_WINDOW_SIZE_ADJUST = 2
+
+
+class WindowManager:
+ """The main ConsoleApp class containing the whole console."""
+
+ # pylint: disable=too-many-instance-attributes
+ def __init__(
+ self,
+ application: Any,
+ ):
+ self.application = application
+ self.active_panes: collections.deque = collections.deque()
+ self.vertical_split = False
+
+ # Reference to the current prompt_toolkit window split for the current
+ # set of active_panes.
+ self.active_pane_split = None
+
+ def add_window(self, pane: Any):
+ self.active_panes.append(pane)
+
+ def active_split(self):
+ return self.active_panes
+
+ def _get_current_active_pane(self):
+ """Return the current active window pane."""
+ focused_pane = None
+ for pane in self.active_panes:
+ if has_focus(pane)():
+ focused_pane = pane
+ break
+ return focused_pane
+
+ def toggle_vertical_split(self):
+ """Toggle visibility of the help window."""
+ self.vertical_split = not self.vertical_split
+
+ self.application.update_menu_items()
+ self.update_root_container_body()
+
+ self.application.redraw_ui()
+
+ def _update_split_orientation(self):
+ if self.vertical_split:
+ self.active_pane_split = VSplit(
+ list(pane for pane in self.active_panes if pane.show_pane),
+ # Add a vertical separator between each active window pane.
+ padding=1,
+ padding_char='│',
+ padding_style='class:pane_separator',
+ )
+ else:
+ self.active_pane_split = HSplit(self.active_panes)
+
+ def create_root_split(self):
+ """Create a vertical or horizontal split container for all active
+ panes."""
+ self._update_split_orientation()
+ return HSplit([
+ self.active_pane_split,
+ ])
+
+ 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_split()
+
+ def add_pane(self, new_pane, existing_pane=None, add_at_beginning=False):
+ existing_pane_index = None
+ if existing_pane:
+ try:
+ existing_pane_index = self.active_panes.index(existing_pane)
+ except ValueError:
+ # Ignore ValueError which can be raised by the self.active_panes
+ # deque if existing_pane can't be found.
+ pass
+ if existing_pane_index:
+ self.active_panes.insert(new_pane, existing_pane_index + 1)
+ else:
+ if add_at_beginning:
+ self.active_panes.appendleft(new_pane)
+ else:
+ self.active_panes.append(new_pane)
+
+ self.application.update_menu_items()
+ self.update_root_container_body()
+
+ self.application.redraw_ui()
+
+ def remove_pane(self, existing_pane):
+ existing_pane_index = 0
+ if not existing_pane:
+ return
+ try:
+ existing_pane_index = self.active_panes.index(existing_pane)
+ self.active_panes.remove(existing_pane)
+ except ValueError:
+ # Ignore ValueError which can be raised by the self.active_panes
+ # deque if existing_pane can't be found.
+ pass
+
+ self.application.update_menu_items()
+ self.update_root_container_body()
+ if len(self.active_panes) > 0:
+ existing_pane_index -= 1
+ try:
+ self.application.focus_on_container(
+ self.active_panes[existing_pane_index])
+ except ValueError:
+ # ValueError will be raised if the the pane at
+ # existing_pane_index can't be accessed.
+ # Focus on the main menu if the existing pane is hidden.
+ self.application.focus_main_menu()
+
+ self.application.redraw_ui()
+
+ def enlarge_pane(self):
+ """Enlarge the currently focused window pane."""
+ pane = self._get_current_active_pane()
+ if pane:
+ self.adjust_pane_size(pane, _WINDOW_SIZE_ADJUST)
+
+ def shrink_pane(self):
+ """Shrink the currently focused window pane."""
+ pane = self._get_current_active_pane()
+ if pane:
+ self.adjust_pane_size(pane, -_WINDOW_SIZE_ADJUST)
+
+ def adjust_pane_size(self, pane, diff: int = _WINDOW_SIZE_ADJUST):
+ """Increase or decrease a given pane's width or height weight."""
+ # Placeholder next_pane value to allow setting width and height without
+ # any consequences if there is no next visible pane.
+ next_pane = HSplit([],
+ height=Dimension(weight=50),
+ width=Dimension(weight=50)) # type: ignore
+ # Try to get the next visible pane to subtract a weight value from.
+ next_visible_pane = self._get_next_visible_pane_after(pane)
+ if next_visible_pane:
+ next_pane = next_visible_pane
+
+ # If the last pane is selected, and there are at least 2 panes, make
+ # next_pane the previous pane.
+ try:
+ if len(self.active_panes) >= 2 and (self.active_panes.index(pane)
+ == len(self.active_panes) - 1):
+ next_pane = self.active_panes[-2]
+ except ValueError:
+ # Ignore ValueError raised if self.active_panes[-2] doesn't exist.
+ pass
+
+ # Get current weight values
+ if self.vertical_split:
+ old_weight = pane.width.weight
+ next_old_weight = next_pane.width.weight # type: ignore
+ else: # Horizontal split
+ old_weight = pane.height.weight
+ next_old_weight = next_pane.height.weight # type: ignore
+
+ # Add to the current pane
+ new_weight = old_weight + diff
+ if new_weight <= 0:
+ new_weight = old_weight
+
+ # Subtract from the next pane
+ next_new_weight = next_old_weight - diff
+ if next_new_weight <= 0:
+ next_new_weight = next_old_weight
+
+ # Set new weight values
+ if self.vertical_split:
+ pane.width.weight = new_weight
+ next_pane.width.weight = next_new_weight # type: ignore
+ else: # Horizontal split
+ pane.height.weight = new_weight
+ next_pane.height.weight = next_new_weight # type: ignore
+
+ def reset_pane_sizes(self):
+ """Reset all active pane width and height to 50%"""
+ for pane in self.active_panes:
+ pane.height = Dimension(weight=50)
+ pane.width = Dimension(weight=50)
+
+ def rotate_panes(self, steps=1):
+ """Rotate the order of all active window panes."""
+ self.active_panes.rotate(steps)
+ self.application.update_menu_items()
+ self.update_root_container_body()
+
+ def toggle_pane(self, pane):
+ """Toggle a pane on or off."""
+ 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 _get_next_visible_pane_after(self, target_pane):
+ """Return the next visible pane that appears after the target pane."""
+ try:
+ target_pane_index = self.active_panes.index(target_pane)
+ except ValueError:
+ # If pane can't be found, focus on the main menu.
+ return None
+
+ # Loop through active panes (not including the target_pane).
+ for i in range(1, len(self.active_panes)):
+ next_pane_index = (target_pane_index + i) % len(self.active_panes)
+ next_pane = self.active_panes[next_pane_index]
+ if next_pane.show_pane:
+ return next_pane
+ return None
+
+ def focus_next_visible_pane(self, pane):
+ """Focus on the next visible window pane if possible."""
+ next_visible_pane = self._get_next_visible_pane_after(pane)
+ if next_visible_pane:
+ self.application.layout.focus(next_visible_pane)
+ return
+ self.application.focus_main_menu()
diff --git a/pw_console/testing.rst b/pw_console/testing.rst
index 585e983..fd69c47 100644
--- a/pw_console/testing.rst
+++ b/pw_console/testing.rst
@@ -229,7 +229,78 @@
- Window is hidden
- |checkbox|
-4. Add note to the commit message
+4. Test Window Management
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. list-table::
+ :widths: 5 45 45 5
+ :header-rows: 1
+
+ * - #
+ - Test Action
+ - Expected Result
+ - ✅
+
+ * - 1
+ - Click the :guilabel:`View > [ ] Vertical Window Spliting`
+ - | Log pane appears on the left
+ | Repl pane appears on the right
+ - |checkbox|
+
+ * - 2
+ - Click the :guilabel:`View > [x] Vertical Window Spliting`
+ - | Log pane appears on the top
+ | Repl pane appears on the bottom
+ - |checkbox|
+
+ * - 3
+ - | Click the :guilabel:`Logs`
+ | window title
+ - Log pane is focused
+ - |checkbox|
+
+ * - 4
+ - | Click the menu :guilabel:`Windows > 1: Logs fake_device.1`
+ | Click :guilabel:`Duplicate pane`
+ - | 3 panes are visible:
+ | Log pane on top
+ | Repl pane in the middle
+ | Log pane on the bottom
+ - |checkbox|
+
+ * - 5
+ - Click the :guilabel:`View > Rotate Window Order`
+ - | 3 panes are visible:
+ | Log pane on top
+ | Log pane in the middle
+ | Repl pane on the bottom
+ - |checkbox|
+
+ * - 6
+ - | Click the menu :guilabel:`Windows > 1: Logs fake_device.1`
+ | Click :guilabel:`Remove pane`
+ - | 2 panes are visible:
+ | Log pane on top
+ | Repl pane in the middle
+ - |checkbox|
+
+ * - 7
+ - | Click the :guilabel:`Logs`
+ | window title
+ - Log pane is focused
+ - |checkbox|
+
+ * - 8
+ - Hold the keys :guilabel:`Ctrl-j`
+ - Log pane is enlarged
+ - |checkbox|
+
+ * - 9
+ - Hold the keys :guilabel:`Ctrl-k`
+ - Log pane shrinks
+ - |checkbox|
+
+5. Add note to the commit message
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Add a ``Testing:`` line to your commit message and mention the steps