pw_console: Focus on next window/tab key binding
- Add keybindings to focus on the next+previous window pane or
tab. This loops through all visible windows.
Ctrl-Alt-n for next
Ctrl-Alt-p for previous
- Update user guide with new bindings plus some fixes.
- Add back in missing window management keys from the keyboard shortcut
help window.
- Update keyboard shortuct listing for better readability. Alternate
keys are shown on separate lines instead of on the same line with
comma separation. For example:
Old:
Execute code --------------------- Enter, Meta-Enter, Option-Enter
Reverse search history ----------- Ctrl-R
Erase input buffer. -------------- Ctrl-C
New:
Execute code --------------------- Enter
Meta-Enter
Option-Enter
Reverse search history ----------- Ctrl-R
Erase input buffer. -------------- Ctrl-C
Change-Id: I79f9b101f848d234f2d90f4335548c95ae0f0a2f
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/78500
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/pw_console/py/help_window_test.py b/pw_console/py/help_window_test.py
index 0df8695..14caa73 100644
--- a/pw_console/py/help_window_test.py
+++ b/pw_console/py/help_window_test.py
@@ -118,10 +118,6 @@
self.assertIn(
inspect.cleandoc("""
- Pigweed CLI v0.1
-
- ================================ Help ===============================
-
Welcome to the Pigweed Console!
Please enjoy this extra help text.
"""),
@@ -129,27 +125,31 @@
)
self.assertIn(
inspect.cleandoc("""
- ============================ Global Keys ============================
+ ==== Global Keys ====
"""),
help_window.help_text,
)
self.assertIn(
inspect.cleandoc("""
Toggle help window. ----------------- F1
- Quit the application. --------------- Ctrl-Q, Ctrl-W
+ Quit the application. --------------- Ctrl-Q
+ Ctrl-W
"""),
help_window.help_text,
)
self.assertIn(
inspect.cleandoc("""
- ============================= Focus Keys ============================
+ ==== Focus Keys ====
"""),
help_window.help_text,
)
self.assertIn(
inspect.cleandoc("""
- Move focus to the next widget. ------ BackTab, Ctrl-Down, Ctrl-Right
- Move focus to the previous widget. -- Ctrl-Left, Ctrl-Up
+ Move focus to the next widget. ------ Ctrl-Down
+ Ctrl-Right
+ Shift-Tab
+ Move focus to the previous widget. -- Ctrl-Left
+ Ctrl-Up
"""),
help_window.help_text,
)
diff --git a/pw_console/py/pw_console/console_app.py b/pw_console/py/pw_console/console_app.py
index 013822d..ab212f4 100644
--- a/pw_console/py/pw_console/console_app.py
+++ b/pw_console/py/pw_console/console_app.py
@@ -495,6 +495,14 @@
'[View]',
children=[
# [Menu Item ][Keybind ]
+ MenuItem('Focus Next Window/Tab Ctrl-Alt-n',
+ handler=self.window_manager.focus_next_pane),
+ # [Menu Item ][Keybind ]
+ MenuItem('Focus Prev Window/Tab Ctrl-Alt-p',
+ handler=self.window_manager.focus_previous_pane),
+ MenuItem('-'),
+
+ # [Menu Item ][Keybind ]
MenuItem('Move Window Up Ctrl-Alt-Up',
handler=functools.partial(
self.run_pane_menu_option,
@@ -675,6 +683,9 @@
self.keybind_help_window.add_keybind_help_text('Global',
self.key_bindings)
+ self.keybind_help_window.add_keybind_help_text(
+ 'Window Management', self.window_manager.key_bindings)
+
# Add activated plugin key bindings to the help text.
for pane in self.window_manager.active_panes():
for key_bindings in pane.get_all_key_bindings():
diff --git a/pw_console/py/pw_console/docs/user_guide.rst b/pw_console/py/pw_console/docs/user_guide.rst
index c6d3223..d92b602 100644
--- a/pw_console/py/pw_console/docs/user_guide.rst
+++ b/pw_console/py/pw_console/docs/user_guide.rst
@@ -33,21 +33,21 @@
::
- +-----------------------------------------------------+
- | [File] [View] [Window] [Help] Pigweed Console |
- +=====================================================+
- | |
- | |
- | |
- | Log Window |
- +=====================================================+
- | |
- | |
- | Python Results |
- +- - - - - - - - - - - - - - - - - - - - - - - - - - -+
- | |
- | Python Input |
- +-----------------------------------------------------+
+ +---------------------------------------------------------+
+ | [File] [Edit] [View] [Window] [Help] Pigweed Console |
+ +=========================================================+
+ | |
+ | |
+ | |
+ | Log Window |
+ +=========================================================+
+ | |
+ | |
+ | Python Results |
+ +- - - - - - - - - - - - - - - - - - - - - - - - - - - - -+
+ | |
+ | Python Input |
+ +---------------------------------------------------------+
Navigation
@@ -64,10 +64,12 @@
============================================ =====================
Function Keys
============================================ =====================
-Move focus between all active UI elements :kbd:`Shift-Tab`
+Switch focus to the next window or tab :kbd:`Ctrl-Alt-n`
+Switch focus to the previous window or tab :kbd:`Ctrl-Alt-p`
-Move focus between windows and the main menu :kbd:`Ctrl-Up`
- :kbd:`Ctrl-Down`
+Switch focus to the next UI element :kbd:`Shift-Tab`
+ :kbd:`Ctrl-Right`
+Switch focus to the previous UI element :kbd:`Ctrl-Left`
Move selection in the main menu :kbd:`Up`
:kbd:`Down`
@@ -413,7 +415,9 @@
under the :guilabel:`[Window]` menu.
The active window can be moved and resized with the following keys. There are
-also menu options under :guilabel:`[View]` for the same actions.
+also menu options under :guilabel:`[View]` for the same actions. Additionally,
+windows can be resized with the mouse by click dragging on the :guilabel:`====`
+text on the far right side of any toolbar.
============================================ =====================
Function Keys
diff --git a/pw_console/py/pw_console/help_window.py b/pw_console/py/pw_console/help_window.py
index 0e05b69..13c64b5 100644
--- a/pw_console/py/pw_console/help_window.py
+++ b/pw_console/py/pw_console/help_window.py
@@ -279,6 +279,7 @@
key_name = key_name.replace('Control', 'Ctrl-')
key_name = key_name.replace('Shift', 'Shift-')
key_name = key_name.replace('Escape-', 'Alt-')
+ key_name = key_name.replace('BackTab', 'Shift-Tab')
key_list.append(key_name)
key_list_width = len(', '.join(key_list))
diff --git a/pw_console/py/pw_console/templates/keybind_list.jinja b/pw_console/py/pw_console/templates/keybind_list.jinja
index 92acdb9..140d13b 100644
--- a/pw_console/py/pw_console/templates/keybind_list.jinja
+++ b/pw_console/py/pw_console/templates/keybind_list.jinja
@@ -14,6 +14,7 @@
the License.
#}
{% set total_width = [max_description_width + max_key_list_width + 5, max_additional_help_text_width]|max %}
+{% set key_whitespace_indentation = ' ' * (max_description_width + 5) %}
{% if preamble %}
{{ preamble }}
@@ -30,7 +31,7 @@
{{ ' {} Keys '.format(section).center(total_width, '=') }}
{% for description, key_list in key_dict.items() %}
-{{ (description+' ').ljust(max_description_width+3, '-') }} {{ key_list|sort|join(', ') }}
+{{ (description+' ').ljust(max_description_width + 3, '-') }} {{ key_list|sort|join('\n' + key_whitespace_indentation) }}
{% endfor %}
diff --git a/pw_console/py/pw_console/widgets/window_pane.py b/pw_console/py/pw_console/widgets/window_pane.py
index cf562fe..e6326cb 100644
--- a/pw_console/py/pw_console/widgets/window_pane.py
+++ b/pw_console/py/pw_console/widgets/window_pane.py
@@ -101,6 +101,14 @@
self.last_pane_width = 0
self.last_pane_height = 0
+ def __repr__(self) -> str:
+ """Create a repr with this pane's title and subtitle."""
+ repr_str = f'{type(self).__qualname__}(pane_title="{self.pane_title()}"'
+ if self.pane_subtitle():
+ repr_str += f', pane_subtitle="{self.pane_subtitle()}"'
+ repr_str += ')'
+ return repr_str
+
def pane_title(self) -> str:
return self._pane_title
diff --git a/pw_console/py/pw_console/window_list.py b/pw_console/py/pw_console/window_list.py
index ca83a3a..a38eb9a 100644
--- a/pw_console/py/pw_console/window_list.py
+++ b/pw_console/py/pw_console/window_list.py
@@ -249,8 +249,8 @@
def switch_to_tab(self, index: int):
self.focused_pane_index = index
+ # refresh_ui() will focus on the new tab container.
self.refresh_ui()
- self.application.focus_on_container(self.active_panes[index])
def set_display_mode(self, mode: DisplayMode):
self.display_mode = mode
diff --git a/pw_console/py/pw_console/window_manager.py b/pw_console/py/pw_console/window_manager.py
index b128f74..091e857 100644
--- a/pw_console/py/pw_console/window_manager.py
+++ b/pw_console/py/pw_console/window_manager.py
@@ -105,6 +105,16 @@
"""Enlarge the current window split."""
self.enlarge_split()
+ @bindings.add('escape', 'c-p') # Ctrl-Alt-p
+ def focus_prev_pane(_event):
+ """Switch focus to the previous window pane or tab."""
+ self.focus_previous_pane()
+
+ @bindings.add('escape', 'c-n') # Ctrl-Alt-n
+ def focus_next_pane(_event):
+ """Switch focus to the next window pane or tab."""
+ self.focus_next_pane()
+
@bindings.add('c-u')
def balance_window_panes(_event):
"""Balance all window sizes."""
@@ -193,6 +203,74 @@
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 not active_window_list:
+ 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)
+ 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())
diff --git a/pw_console/py/window_manager_test.py b/pw_console/py/window_manager_test.py
index 3ccb0b0..7fd76c8 100644
--- a/pw_console/py/window_manager_test.py
+++ b/pw_console/py/window_manager_test.py
@@ -24,11 +24,12 @@
from pw_console.console_app import ConsoleApp
from pw_console.window_manager import _WINDOW_SPLIT_ADJUST
-from pw_console.window_list import _WINDOW_HEIGHT_ADJUST
+from pw_console.window_list import _WINDOW_HEIGHT_ADJUST, DisplayMode
def _create_console_app(logger_count=2):
console_app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT)
+ console_app.focus_on_container = MagicMock()
loggers = {}
for i in range(logger_count):
@@ -412,6 +413,193 @@
],
)
+ def test_focus_next_and_previous_pane(self) -> None:
+ """Test getting the window list for a given pane."""
+ with create_app_session(output=FakeOutput()):
+ console_app = _create_console_app(logger_count=4)
+
+ window_manager = console_app.window_manager
+ self.assertEqual(
+ _window_pane_titles(window_manager),
+ [
+ [
+ 'Log3 - test_log3',
+ 'Log2 - test_log2',
+ 'Log1 - test_log1',
+ 'Log0 - test_log0',
+ 'Python Repl - ',
+ ],
+ ],
+ )
+
+ # Scenario: Move between panes with a single stacked window list.
+
+ # Set the first pane in focus.
+ _target_list_and_pane(window_manager, 0, 0)
+ # Switch focus to the next pane
+ window_manager.focus_next_pane()
+ # Pane index 1 should now be focused.
+ console_app.focus_on_container.assert_called_once_with(
+ window_manager.window_lists[0].active_panes[1])
+ console_app.focus_on_container.reset_mock()
+
+ # Set the first pane in focus.
+ _target_list_and_pane(window_manager, 0, 0)
+ # Switch focus to the previous pane
+ window_manager.focus_previous_pane()
+ # Previous pane should wrap around to the last pane in the first
+ # window_list.
+ console_app.focus_on_container.assert_called_once_with(
+ window_manager.window_lists[0].active_panes[-1])
+ console_app.focus_on_container.reset_mock()
+
+ # Set the last pane in focus.
+ _target_list_and_pane(window_manager, 0, 4)
+ # Switch focus to the next pane
+ window_manager.focus_next_pane()
+ # Next pane should wrap around to the first pane in the first
+ # window_list.
+ console_app.focus_on_container.assert_called_once_with(
+ window_manager.window_lists[0].active_panes[0])
+ console_app.focus_on_container.reset_mock()
+
+ # Scenario: Move between panes with a single tabbed window list.
+
+ # Switch to Tabbed view mode
+ window_manager.window_lists[0].set_display_mode(DisplayMode.TABBED)
+ # The set_display_mode call above will call focus_on_container once.
+ console_app.focus_on_container.reset_mock()
+
+ # Setup the switch_to_tab mock
+ window_manager.window_lists[0].switch_to_tab = MagicMock(
+ wraps=window_manager.window_lists[0].switch_to_tab)
+
+ # Set the first pane/tab in focus.
+ _target_list_and_pane(window_manager, 0, 0)
+ # Switch focus to the next pane/tab
+ window_manager.focus_next_pane()
+ # Check switch_to_tab is called
+ window_manager.window_lists[
+ 0].switch_to_tab.assert_called_once_with(1)
+ # And that focus_on_container is called only once
+ console_app.focus_on_container.assert_called_once_with(
+ window_manager.window_lists[0].active_panes[1])
+ console_app.focus_on_container.reset_mock()
+ window_manager.window_lists[0].switch_to_tab.reset_mock()
+
+ # Set the last pane/tab in focus.
+ _target_list_and_pane(window_manager, 0, 4)
+ # Switch focus to the next pane/tab
+ window_manager.focus_next_pane()
+ # Check switch_to_tab is called
+ window_manager.window_lists[
+ 0].switch_to_tab.assert_called_once_with(0)
+ # And that focus_on_container is called only once
+ console_app.focus_on_container.assert_called_once_with(
+ window_manager.window_lists[0].active_panes[0])
+ console_app.focus_on_container.reset_mock()
+ window_manager.window_lists[0].switch_to_tab.reset_mock()
+
+ # Set the first pane/tab in focus.
+ _target_list_and_pane(window_manager, 0, 0)
+ # Switch focus to the prev pane/tab
+ window_manager.focus_previous_pane()
+ # Check switch_to_tab is called
+ window_manager.window_lists[
+ 0].switch_to_tab.assert_called_once_with(4)
+ # And that focus_on_container is called only once
+ console_app.focus_on_container.assert_called_once_with(
+ window_manager.window_lists[0].active_panes[4])
+ console_app.focus_on_container.reset_mock()
+ window_manager.window_lists[0].switch_to_tab.reset_mock()
+
+ # Scenario: Move between multiple window lists with mixed stacked
+ # and tabbed view modes.
+
+ # Setup: Move two panes to the right into their own stacked
+ # window_list.
+ _target_list_and_pane(window_manager, 0, 4)
+ window_manager.move_pane_right()
+ _target_list_and_pane(window_manager, 0, 3)
+ window_manager.move_pane_right()
+ self.assertEqual(
+ _window_pane_titles(window_manager),
+ [
+ [
+ 'Log3 - test_log3',
+ 'Log2 - test_log2',
+ 'Log1 - test_log1',
+ ],
+ [
+ 'Log0 - test_log0',
+ 'Python Repl - ',
+ ],
+ ],
+ )
+
+ # Setup the switch_to_tab mock on the second window_list
+ window_manager.window_lists[1].switch_to_tab = MagicMock(
+ wraps=window_manager.window_lists[1].switch_to_tab)
+
+ # Set Log1 in focus
+ _target_list_and_pane(window_manager, 0, 2)
+ window_manager.focus_next_pane()
+ # Log0 should now have focus
+ console_app.focus_on_container.assert_called_once_with(
+ window_manager.window_lists[1].active_panes[0])
+ console_app.focus_on_container.reset_mock()
+
+ # Set Log0 in focus
+ _target_list_and_pane(window_manager, 1, 0)
+ window_manager.focus_previous_pane()
+ # Log1 should now have focus
+ console_app.focus_on_container.assert_called_once_with(
+ window_manager.window_lists[0].active_panes[2])
+ # The first window list is in tabbed mode so switch_to_tab should be
+ # called once.
+ window_manager.window_lists[
+ 0].switch_to_tab.assert_called_once_with(2)
+ # Reset
+ window_manager.window_lists[0].switch_to_tab.reset_mock()
+ console_app.focus_on_container.reset_mock()
+
+ # Set Python Repl in focus
+ _target_list_and_pane(window_manager, 1, 1)
+ window_manager.focus_next_pane()
+ # Log3 should now have focus
+ console_app.focus_on_container.assert_called_once_with(
+ window_manager.window_lists[0].active_panes[0])
+ window_manager.window_lists[
+ 0].switch_to_tab.assert_called_once_with(0)
+ # Reset
+ window_manager.window_lists[0].switch_to_tab.reset_mock()
+ console_app.focus_on_container.reset_mock()
+
+ # Set Log3 in focus
+ _target_list_and_pane(window_manager, 0, 0)
+ window_manager.focus_next_pane()
+ # Log2 should now have focus
+ console_app.focus_on_container.assert_called_once_with(
+ window_manager.window_lists[0].active_panes[1])
+ window_manager.window_lists[
+ 0].switch_to_tab.assert_called_once_with(1)
+ # Reset
+ window_manager.window_lists[0].switch_to_tab.reset_mock()
+ console_app.focus_on_container.reset_mock()
+
+ # Set Python Repl in focus
+ _target_list_and_pane(window_manager, 1, 1)
+ window_manager.focus_previous_pane()
+ # Log0 should now have focus
+ console_app.focus_on_container.assert_called_once_with(
+ window_manager.window_lists[1].active_panes[0])
+ # The second window list is in stacked mode so switch_to_tab should
+ # not be called.
+ window_manager.window_lists[1].switch_to_tab.assert_not_called()
+ # Reset
+ window_manager.window_lists[1].switch_to_tab.reset_mock()
+ console_app.focus_on_container.reset_mock()
+
if __name__ == '__main__':
unittest.main()