pw_console: Floating window pane plugin support
- Create the FloatingWindowPane class and add_floating_window_plugin()
embed function.
- All WindowPane classes can implement a get_top_level_menus() fuction
to return MenuItems for display in pw_consoles main menu bar.
- Convert 2048 to a proper floating window plugin with a
get_top_level_menus() impl. Fixup watch_app.py to accomodate this
change.
Minor changes:
- Populate the repl_pane's get_window_menu_options
- Replace some 'list' types with 'List'
- Switch jinja2 loader from Filesystem to DictLoader
Change-Id: I6247b39dd792edc5469b78dbfbe627b6e62fb71d
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/104464
Reviewed-by: Carlos Chinchilla <cachinchilla@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/pw_console/embedding.rst b/pw_console/embedding.rst
index 249ab84..07a9443 100644
--- a/pw_console/embedding.rst
+++ b/pw_console/embedding.rst
@@ -31,8 +31,8 @@
Console embed instance. Typically, a console is started by creating a
``PwConsoleEmbed()`` instance, calling customization functions, then calling
``.embed()`` as shown in `Using embed()`_. Adding plugins functions similarly by
-calling ``add_top_toolbar``, ``add_bottom_toolbar`` or
-``add_window_plugin``. For example:
+calling ``add_top_toolbar``, ``add_bottom_toolbar``,
+``add_floating_window_plugin`` or ``add_window_plugin``. For example:
.. code-block:: python
diff --git a/pw_console/plugins.rst b/pw_console/plugins.rst
index 2258e2d..e4e5cba 100644
--- a/pw_console/plugins.rst
+++ b/pw_console/plugins.rst
@@ -19,7 +19,8 @@
background tasks.
2. Enable the plugin before pw_console startup by calling ``add_window_plugin``,
- ``add_top_toolbar`` or ``add_bottom_toolbar``. See the
+ ``add_floating_window_plugin``, ``add_top_toolbar`` or
+ ``add_bottom_toolbar``. See the
:ref:`module-pw_console-embedding-plugins` section of the
:ref:`module-pw_console-embedding` for an example.
@@ -127,9 +128,15 @@
game of 2048.
Similar to the ``ClockPane`` the ``Twenty48Pane`` class inherits from
-``WindowPane`` and ``PluginMixin``. Game keybindings are set within the
-``Twenty48Control`` class which is the ``FormattedTextControl`` widget that is
-in focus while playing.
+``PluginMixin`` to manage background tasks. With a few differences:
+
+- Uses ``FloatingWindowPane`` to create a floating window instead of a
+ standard tiled window.
+- Implements the ``get_top_level_menus`` function to create a new ``[2048]``
+ menu in Pigweed Console's own main menu bar.
+- Adds custom game keybindings which are set within the ``Twenty48Control``
+ class. That is the prompt_toolkit ``FormattedTextControl`` widget which
+ receives keyboard input when the game is in focus.
The ``Twenty48Game`` class is separate from the user interface and handles
managing the game state as well as printing the game board. The
diff --git a/pw_console/py/pw_console/__main__.py b/pw_console/py/pw_console/__main__.py
index 7c36cf9..97c4e77 100644
--- a/pw_console/py/pw_console/__main__.py
+++ b/pw_console/py/pw_console/__main__.py
@@ -27,6 +27,7 @@
from pw_console.log_store import LogStore
from pw_console.plugins.calc_pane import CalcPane
from pw_console.plugins.clock_pane import ClockPane
+from pw_console.plugins.twenty48_pane import Twenty48Pane
_LOG = logging.getLogger(__package__)
_ROOT_LOG = logging.getLogger('')
@@ -135,6 +136,8 @@
_ROOT_LOG.debug('Adding plugins...')
console.add_window_plugin(ClockPane())
console.add_window_plugin(CalcPane())
+ console.add_floating_window_plugin(
+ Twenty48Pane(include_resize_handle=False), left=4)
_ROOT_LOG.debug('Starting prompt_toolkit full-screen application...')
console.embed()
diff --git a/pw_console/py/pw_console/command_runner.py b/pw_console/py/pw_console/command_runner.py
index cc697db..4f4c98a 100644
--- a/pw_console/py/pw_console/command_runner.py
+++ b/pw_console/py/pw_console/command_runner.py
@@ -491,6 +491,7 @@
'[Help] > ',
# This focuses on a save dialog bor.
'Save/Export a copy',
+ '[Windows] > Floating ',
]:
if command_text in self.selected_item_text:
close_dialog_first = True
diff --git a/pw_console/py/pw_console/console_app.py b/pw_console/py/pw_console/console_app.py
index 05d2af7..10c0b7e 100644
--- a/pw_console/py/pw_console/console_app.py
+++ b/pw_console/py/pw_console/console_app.py
@@ -22,9 +22,9 @@
from pathlib import Path
import sys
from threading import Thread
-from typing import Any, Callable, Iterable, List, Optional, Tuple, Union
+from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
-from jinja2 import Environment, FileSystemLoader, make_logging_undefined
+from jinja2 import Environment, DictLoader, make_logging_undefined
from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.output import ColorDepth
@@ -61,13 +61,13 @@
import pw_console.key_bindings
from pw_console.log_pane import LogPane
from pw_console.log_store import LogStore
-from pw_console.plugins.twenty48_pane import Twenty48Pane
from pw_console.pw_ptpython_repl import PwPtPythonRepl
from pw_console.python_logging import all_loggers
from pw_console.quit_dialog import QuitDialog
from pw_console.repl_pane import ReplPane
import pw_console.style
import pw_console.widgets.checkbox
+from pw_console.widgets import FloatingWindowPane
import pw_console.widgets.mouse_handlers
from pw_console.window_manager import WindowManager
@@ -137,6 +137,8 @@
color_depth=None,
extra_completers=None,
prefs=None,
+ floating_window_plugins: Optional[List[Tuple[FloatingWindowPane,
+ Dict]]] = None,
):
self.prefs = prefs if prefs else ConsolePrefs()
self.color_depth = get_default_colordepth(color_depth)
@@ -154,11 +156,16 @@
local_vars = local_vars or global_vars
+ jinja_templates = {
+ t: importlib.resources.read_text('pw_console.templates', t)
+ for t in importlib.resources.contents('pw_console.templates')
+ if t.endswith('.jinja')
+ }
+
# Setup the Jinja environment
self.jinja_env = Environment(
# Load templates automatically from pw_console/templates
- loader=FileSystemLoader(
- importlib.resources.files('pw_console.templates')),
+ loader=DictLoader(jinja_templates),
# Raise errors if variables are undefined in templates
undefined=make_logging_undefined(
logger=logging.getLogger(__package__), ),
@@ -214,8 +221,11 @@
self.prefs_file_window.load_yaml_text(
self.prefs.current_config_as_yaml())
- self.game_2048 = Twenty48Pane(self, include_resize_handle=False)
- self.game_2048.show_pane = False
+ self.floating_window_plugins = None
+ if floating_window_plugins:
+ self.floating_window_plugins = [
+ plugin for plugin, _ in floating_window_plugins
+ ]
# Used for tracking which pane was in focus before showing help window.
self.last_focused_pane = None
@@ -298,11 +308,15 @@
# Callable to get width
width=self.keybind_help_window.content_width,
),
- Float(
- content=self.game_2048,
- top=3,
- left=4,
- ),
+ ]
+
+ if floating_window_plugins:
+ self.floats.extend([
+ Float(content=plugin_container, **float_args)
+ for plugin_container, float_args in floating_window_plugins
+ ])
+
+ self.floats.extend([
# Completion menu that can overlap other panes since it lives in
# the top level Float container.
Float(
@@ -331,7 +345,7 @@
top=2,
left=2,
),
- ]
+ ])
# prompt_toolkit root container.
self.root_container = MenuContainer(
@@ -392,9 +406,9 @@
# Run the function for a particular menu item.
return_value = function_to_run()
# It's return value dictates if the main menu should close or not.
- # - True: The main menu stays open. This is the default prompt_toolkit
+ # - False: The main menu stays open. This is the default prompt_toolkit
# menu behavior.
- # - False: The main menu closes.
+ # - True: The main menu closes.
# Update menu content. This will refresh checkboxes and add/remove
# items.
@@ -599,13 +613,6 @@
'Themes',
children=themes_submenu,
),
- MenuItem('Games',
- children=[
- MenuItem(
- '2048',
- handler=self.game_2048.open_dialog,
- ),
- ]),
MenuItem('-'),
MenuItem('Exit', handler=self.exit_console),
],
@@ -700,7 +707,43 @@
),
]
- window_menu = self.window_manager.create_window_menu()
+ window_menu_items = self.window_manager.create_window_menu_items()
+
+ floating_window_items = []
+ if self.floating_window_plugins:
+ floating_window_items.append(MenuItem('-', None))
+ floating_window_items.extend(
+ MenuItem(
+ 'Floating Window {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.run_pane_menu_option, pane.toggle_dialog),
+ ),
+ ] + [
+ MenuItem(text,
+ handler=functools.partial(
+ self.run_pane_menu_option, handler))
+ for text, handler in pane.get_window_menu_options()
+ ],
+ ) for pane_index, pane in enumerate(
+ self.floating_window_plugins))
+ window_menu_items.extend(floating_window_items)
+
+ window_menu = [MenuItem('[Windows]', children=window_menu_items)]
+
+ top_level_plugin_menus = []
+ for pane in self.window_manager.active_panes():
+ top_level_plugin_menus.extend(pane.get_top_level_menus())
+ if self.floating_window_plugins:
+ for pane in self.floating_window_plugins:
+ top_level_plugin_menus.extend(pane.get_top_level_menus())
help_menu_items = [
MenuItem(self.user_guide_window.menu_title(),
@@ -727,7 +770,8 @@
),
]
- return file_menu + edit_menu + view_menu + window_menu + help_menu
+ return (file_menu + edit_menu + view_menu + top_level_plugin_menus +
+ window_menu + help_menu)
def focus_main_menu(self):
"""Set application focus to the main menu."""
@@ -874,18 +918,20 @@
def modal_window_is_open(self):
"""Return true if any modal window or dialog is open."""
+ floating_window_is_open = (self.keybind_help_window.show_window
+ or self.prefs_file_window.show_window
+ or self.user_guide_window.show_window
+ or self.quit_dialog.show_dialog
+ or self.command_runner.show_dialog)
+
if self.app_help_text:
- return (self.app_help_window.show_window
- or self.keybind_help_window.show_window
- or self.prefs_file_window.show_window
- or self.user_guide_window.show_window
- or self.quit_dialog.show_dialog or self.game_2048.show_pane
- or self.command_runner.show_dialog)
- return (self.keybind_help_window.show_window
- or self.prefs_file_window.show_window
- or self.user_guide_window.show_window
- or self.quit_dialog.show_dialog or self.game_2048.show_pane
- or self.command_runner.show_dialog)
+ floating_window_is_open = (self.app_help_window.show_window
+ or floating_window_is_open)
+
+ floating_plugin_is_open = any(
+ plugin.show_pane for plugin in self.floating_window_plugins)
+
+ return floating_window_is_open or floating_plugin_is_open
def exit_console(self):
"""Quit the console prompt_toolkit application UI."""
diff --git a/pw_console/py/pw_console/console_prefs.py b/pw_console/py/pw_console/console_prefs.py
index e8b891a..f5621f6 100644
--- a/pw_console/py/pw_console/console_prefs.py
+++ b/pw_console/py/pw_console/console_prefs.py
@@ -193,7 +193,7 @@
self._config[name] = not existing_setting
@property
- def column_order(self) -> list:
+ def column_order(self) -> List:
return self._config.get('column_order', [])
def column_style(self,
@@ -223,7 +223,7 @@
return self._config.get('windows', {})
@property
- def window_column_modes(self) -> list:
+ def window_column_modes(self) -> List:
return list(column_type for column_type in self.windows.keys())
@property
diff --git a/pw_console/py/pw_console/embed.py b/pw_console/py/pw_console/embed.py
index 60ef80a..e6dcddd 100644
--- a/pw_console/py/pw_console/embed.py
+++ b/pw_console/py/pw_console/embed.py
@@ -16,7 +16,7 @@
import asyncio
import logging
from pathlib import Path
-from typing import Any, Dict, List, Iterable, Optional, Union
+from typing import Any, Dict, List, Iterable, Optional, Tuple, Union
from prompt_toolkit.completion import WordCompleter
@@ -24,7 +24,11 @@
from pw_console.get_pw_console_app import PW_CONSOLE_APP_CONTEXTVAR
from pw_console.plugin_mixin import PluginMixin
import pw_console.python_logging
-from pw_console.widgets import WindowPane, WindowPaneToolbar
+from pw_console.widgets import (
+ FloatingWindowPane,
+ WindowPane,
+ WindowPaneToolbar,
+)
def _set_console_app_instance(plugin: Any, console_app: ConsoleApp) -> None:
@@ -124,6 +128,8 @@
self.setup_python_logging_called = False
self.hidden_by_default_windows: List[str] = []
self.window_plugins: List[WindowPane] = []
+ self.floating_window_plugins: List[Tuple[FloatingWindowPane,
+ Dict]] = []
self.top_toolbar_plugins: List[WindowPaneToolbar] = []
self.bottom_toolbar_plugins: List[WindowPaneToolbar] = []
@@ -135,6 +141,41 @@
"""
self.window_plugins.append(window_pane)
+ def add_floating_window_plugin(self, window_pane: FloatingWindowPane,
+ **float_args) -> None:
+ """Include a custom floating window pane plugin.
+
+ This adds a FloatingWindowPane class to the pw_console UI. The first
+ argument should be the window to add and the remaining keyword arguments
+ are passed to the prompt_toolkit Float() class. This allows positioning
+ of the floating window. By default the floating window will be
+ centered. To anchor the window to a side or corner of the screen set the
+ ``left``, ``right``, ``top``, or ``bottom`` keyword args.
+
+ For example:
+
+ .. code-block:: python
+
+ from pw_console import PwConsoleEmbed
+
+ console = PwConsoleEmbed(...)
+ my_plugin = MyPlugin()
+ # Anchor this floating window 2 rows away from the top and 4 columns
+ # away from the left edge of the screen.
+ console.add_floating_window_plugin(my_plugin, top=2, left=4)
+
+ See all possible keyword args in the prompt_toolkit documentation:
+ https://python-prompt-toolkit.readthedocs.io/en/stable/pages/reference.html#prompt_toolkit.layout.Float
+
+ Args:
+ window_pane: Any instance of the FloatingWindowPane class.
+ left: Distance to the left edge of the screen
+ right: Distance to the right edge of the screen
+ top: Distance to the top edge of the screen
+ bottom: Distance to the bottom edge of the screen
+ """
+ self.floating_window_plugins.append((window_pane, float_args))
+
def add_top_toolbar(self, toolbar: WindowPaneToolbar) -> None:
"""Include a toolbar plugin to display on the top of the screen.
@@ -251,6 +292,7 @@
help_text=self.help_text,
app_title=self.app_title,
extra_completers=self.extra_completers,
+ floating_window_plugins=self.floating_window_plugins,
)
PW_CONSOLE_APP_CONTEXTVAR.set(self.console_app) # type: ignore
# Setup Python logging and log panes.
@@ -274,6 +316,10 @@
_set_console_app_instance(toolbar, self.console_app)
self.console_app.window_manager.add_bottom_toolbar(toolbar)
+ # Init floating window plugins.
+ for floating_window, _ in self.floating_window_plugins:
+ _set_console_app_instance(floating_window, self.console_app)
+
# Rebuild prompt_toolkit containers, menu items, and help content with
# any new plugins added above.
self.console_app.refresh_layout()
diff --git a/pw_console/py/pw_console/log_pane.py b/pw_console/py/pw_console/log_pane.py
index eb4fbfd..7557a4b 100644
--- a/pw_console/py/pw_console/log_pane.py
+++ b/pw_console/py/pw_console/log_pane.py
@@ -16,7 +16,15 @@
import functools
import logging
import re
-from typing import Any, List, Optional, Union, TYPE_CHECKING
+from typing import (
+ Any,
+ Callable,
+ List,
+ Optional,
+ TYPE_CHECKING,
+ Tuple,
+ Union,
+)
from prompt_toolkit.application.current import get_app
from prompt_toolkit.filters import (
@@ -548,7 +556,8 @@
# Return log content control keybindings
return [self.log_content_control.get_key_bindings()]
- def get_all_menu_options(self) -> List:
+ def get_window_menu_options(
+ self) -> List[Tuple[str, Union[Callable, None]]]:
"""Return all menu options for the log pane."""
options = [
diff --git a/pw_console/py/pw_console/log_view.py b/pw_console/py/pw_console/log_view.py
index 5ba56eb..31b7db8 100644
--- a/pw_console/py/pw_console/log_view.py
+++ b/pw_console/py/pw_console/log_view.py
@@ -738,7 +738,7 @@
"""Get pre-formatted table header."""
return self.log_store.render_table_header()
- def render_content(self) -> list:
+ def render_content(self) -> List:
"""Return logs to display on screen as a list of FormattedText tuples.
This function determines when the log screen requires re-rendeing based
diff --git a/pw_console/py/pw_console/plugins/twenty48_pane.py b/pw_console/py/pw_console/plugins/twenty48_pane.py
index 872f856..832ce3e 100644
--- a/pw_console/py/pw_console/plugins/twenty48_pane.py
+++ b/pw_console/py/pw_console/plugins/twenty48_pane.py
@@ -14,25 +14,36 @@
"""Example Plugin that displays some dynamic content: a game of 2048."""
from random import choice
-from typing import Any, Iterable, List, Tuple
+from typing import Iterable, List, Tuple, TYPE_CHECKING
import time
from prompt_toolkit.filters import has_focus
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
from prompt_toolkit.layout import (
+ AnyContainer,
Dimension,
FormattedTextControl,
+ HSplit,
Window,
WindowAlign,
VSplit,
)
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
+from prompt_toolkit.widgets import MenuItem
-from pw_console.widgets import ToolbarButton, WindowPane, WindowPaneToolbar
+import pw_console.widgets.border
+from pw_console.widgets import (
+ FloatingWindowPane,
+ ToolbarButton,
+ WindowPaneToolbar,
+)
from pw_console.plugin_mixin import PluginMixin
from pw_console.get_pw_console_app import get_pw_console_app
+if TYPE_CHECKING:
+ from pw_console.console_app import ConsoleApp
+
Twenty48Cell = Tuple[int, int, int]
@@ -347,7 +358,7 @@
return NotImplemented
-class Twenty48Pane(WindowPane, PluginMixin):
+class Twenty48Pane(FloatingWindowPane, PluginMixin):
"""Example Pigweed Console plugin to play 2048.
The Twenty48Pane is a WindowPane based plugin that displays an interactive
@@ -359,22 +370,16 @@
For an example see:
https://pigweed.dev/pw_console/embedding.html#adding-plugins
"""
- def __init__(self,
- application: Any,
- include_resize_handle: bool = True,
- **kwargs):
+ def __init__(self, include_resize_handle: bool = True, **kwargs):
super().__init__(pane_title='2048',
- height=Dimension(preferred=15),
- width=Dimension(preferred=48),
+ height=Dimension(preferred=17),
+ width=Dimension(preferred=50),
**kwargs)
- # Reference to the parent pw_console app.
- self.application = application
self.game = Twenty48Game()
- # Tracks the last focused container, to enable restoring focus after
- # closing the dialog.
- self.last_focused_pane = None
+ # Hide by default.
+ self.show_pane = False
# Create a toolbar for display at the bottom of the 2048 window. It
# will show the window title and buttons.
@@ -440,6 +445,25 @@
# self.container is the root container that contains objects to be
# rendered in the UI, one on top of the other.
self.container = self._create_pane_container(
+ pw_console.widgets.border.create_border(
+ HSplit([
+ # Vertical split content
+ VSplit([
+ # Left side will show the game board.
+ self.twenty48_game_window,
+ # Stats will be shown on the right.
+ self.twenty48_stats_window,
+ ]),
+ # The bottom_toolbar is shown below the VSplit.
+ self.bottom_toolbar,
+ ]),
+ title='2048',
+ border_style='class:command-runner-border',
+ # left_margin_columns=1,
+ # right_margin_columns=1,
+ ))
+
+ self.dialog_content: List[AnyContainer] = [
# Vertical split content
VSplit([
# Left side will show the game board.
@@ -449,7 +473,20 @@
]),
# The bottom_toolbar is shown below the VSplit.
self.bottom_toolbar,
+ ]
+ # Wrap the dialog content in a border
+ self.bordered_dialog_content = pw_console.widgets.border.create_border(
+ HSplit(self.dialog_content),
+ title='2048',
+ border_style='class:command-runner-border',
)
+ # self.container is the root container that contains objects to be
+ # rendered in the UI, one on top of the other.
+ if include_resize_handle:
+ self.container = self._create_pane_container(*self.dialog_content)
+ else:
+ self.container = self._create_pane_container(
+ self.bordered_dialog_content)
# This plugin needs to run a task in the background periodically and
# uses self.plugin_init() to set which function to run, and how often.
@@ -462,25 +499,28 @@
plugin_logger_name='pw_console_example_2048_plugin',
)
- def focus_self(self) -> None:
- self.application.focus_on_container(self)
+ def get_top_level_menus(self) -> List[MenuItem]:
+ return [
+ MenuItem(
+ '[2048]',
+ children=[
+ MenuItem('Example Top Level Menu',
+ handler=None,
+ disabled=True),
+ # Menu separator
+ MenuItem('-', None),
+ MenuItem('Restart', handler=self.game.reset_game),
+ ],
+ ),
+ ]
- def close_dialog(self) -> None:
- """Close runner dialog box."""
- self.show_pane = False
+ def pw_console_init(self, app: 'ConsoleApp') -> None:
+ """Set the Pigweed Console application instance.
- # Restore original focus if possible.
- if self.last_focused_pane:
- self.application.focus_on_container(self.last_focused_pane)
- else:
- # Fallback to focusing on the main menu.
- self.application.focus_main_menu()
-
- def open_dialog(self) -> None:
- self.show_pane = True
- self.last_focused_pane = self.application.focused_window()
- self.focus_self()
- self.application.redraw_ui()
+ This function is called after the Pigweed Console starts up and allows
+ access to the user preferences. Prefs is required for creating new
+ user-remappable keybinds."""
+ self.application = app
def _background_task(self) -> bool:
"""Function run in the background for the ClockPane plugin."""
diff --git a/pw_console/py/pw_console/repl_pane.py b/pw_console/py/pw_console/repl_pane.py
index 14ea92d..56aec25 100644
--- a/pw_console/py/pw_console/repl_pane.py
+++ b/pw_console/py/pw_console/repl_pane.py
@@ -27,6 +27,7 @@
Optional,
Tuple,
TYPE_CHECKING,
+ Union,
)
from prompt_toolkit.filters import (
@@ -328,8 +329,21 @@
'Show history.': ['F3'],
}]
- def get_all_menu_options(self):
- return []
+ def get_window_menu_options(
+ self) -> List[Tuple[str, Union[Callable, None]]]:
+ return [
+ ('Python Input > Paste',
+ self.paste_system_clipboard_to_input_buffer),
+ ('Python Input > Copy or Clear', self.copy_or_clear_input_buffer),
+ ('Python Input > Run', self.run_code),
+ # Menu separator
+ ('-', None),
+ ('Python Output > Toggle Wrap lines',
+ self.toggle_wrap_output_lines),
+ ('Python Output > Copy All', self.copy_all_output_text),
+ ('Python Output > Copy Selection', self.copy_output_selection),
+ ('Python Output > Clear', self.clear_output_buffer),
+ ]
def run_code(self):
"""Trigger a repl code execution on mouse click."""
diff --git a/pw_console/py/pw_console/widgets/__init__.py b/pw_console/py/pw_console/widgets/__init__.py
index 946e3af..71ec193 100644
--- a/pw_console/py/pw_console/widgets/__init__.py
+++ b/pw_console/py/pw_console/widgets/__init__.py
@@ -23,5 +23,9 @@
to_checkbox_text,
)
from pw_console.widgets.mouse_handlers import on_click
-from pw_console.widgets.window_pane import WindowPane, WindowPaneHSplit
+from pw_console.widgets.window_pane import (
+ FloatingWindowPane,
+ WindowPane,
+ WindowPaneHSplit,
+)
from pw_console.widgets.window_pane_toolbar import WindowPaneToolbar
diff --git a/pw_console/py/pw_console/widgets/window_pane.py b/pw_console/py/pw_console/widgets/window_pane.py
index ab2484a..17a0dfe 100644
--- a/pw_console/py/pw_console/widgets/window_pane.py
+++ b/pw_console/py/pw_console/widgets/window_pane.py
@@ -14,7 +14,7 @@
"""Window pane base class."""
from abc import ABC
-from typing import Any, Optional, TYPE_CHECKING, Union
+from typing import Any, Callable, List, Optional, Tuple, TYPE_CHECKING, Union
import functools
from prompt_toolkit.layout.dimension import AnyDimension
@@ -27,6 +27,7 @@
HSplit,
walk,
)
+from prompt_toolkit.widgets import MenuItem
from pw_console.get_pw_console_app import get_pw_console_app
@@ -145,7 +146,7 @@
object."""
return self.container # pylint: disable=no-member
- def get_all_key_bindings(self) -> list:
+ def get_all_key_bindings(self) -> List:
"""Return keybinds for display in the help window.
For example:
@@ -167,7 +168,8 @@
# pylint: disable=no-self-use
return []
- def get_all_menu_options(self) -> list:
+ def get_window_menu_options(
+ self) -> List[Tuple[str, Union[Callable, None]]]:
"""Return menu options for the window pane.
Should return a list of tuples containing with the display text and
@@ -176,6 +178,11 @@
# pylint: disable=no-self-use
return []
+ def get_top_level_menus(self) -> List[MenuItem]:
+ """Return MenuItems to be displayed on the main pw_console menu bar."""
+ # pylint: disable=no-self-use
+ return []
+
def pane_resized(self) -> bool:
"""Return True if the current window size has changed."""
return (self.last_pane_width != self.current_pane_width
@@ -209,3 +216,44 @@
if container == child_container:
return True
return False
+
+
+class FloatingWindowPane(WindowPane):
+ """The Pigweed Console FloatingWindowPane class."""
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Tracks the last focused container, to enable restoring focus after
+ # closing the dialog.
+ self.last_focused_pane = None
+
+ def close_dialog(self) -> None:
+ """Close runner dialog box."""
+ self.show_pane = False
+
+ # Restore original focus if possible.
+ if self.last_focused_pane:
+ self.application.focus_on_container(self.last_focused_pane)
+ else:
+ # Fallback to focusing on the main menu.
+ self.application.focus_main_menu()
+
+ self.application.update_menu_items()
+
+ def open_dialog(self) -> None:
+ self.show_pane = True
+ self.last_focused_pane = self.application.focused_window()
+ self.focus_self()
+ self.application.redraw_ui()
+
+ self.application.update_menu_items()
+
+ def toggle_dialog(self) -> bool:
+ if self.show_pane:
+ self.close_dialog()
+ else:
+ self.open_dialog()
+ # The focused window has changed. Return true so
+ # ConsoleApp.run_pane_menu_option does not set the focus to the main
+ # menu.
+ return True
diff --git a/pw_console/py/pw_console/window_manager.py b/pw_console/py/pw_console/window_manager.py
index 044751b..1098d7f 100644
--- a/pw_console/py/pw_console/window_manager.py
+++ b/pw_console/py/pw_console/window_manager.py
@@ -974,7 +974,7 @@
# Focus on the first visible pane.
self.focus_first_visible_pane()
- def create_window_menu(self):
+ def create_window_menu_items(self) -> List[MenuItem]:
"""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):
@@ -1015,16 +1015,11 @@
handler=functools.partial(
self.application.run_pane_menu_option,
handler))
- for text, handler in pane.get_all_menu_options()
+ for text, handler in pane.get_window_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]
+ return root_menu_items
diff --git a/pw_console/py/repl_pane_test.py b/pw_console/py/repl_pane_test.py
index f6623b7..cc49972 100644
--- a/pw_console/py/repl_pane_test.py
+++ b/pw_console/py/repl_pane_test.py
@@ -127,12 +127,13 @@
repl_pane = app.repl_pane
# Mock update_output_buffer to track number of update calls
- repl_pane.update_output_buffer = MagicMock(
+ repl_pane.update_output_buffer = MagicMock( # type: ignore
wraps=repl_pane.update_output_buffer)
# Mock complete callback
- pw_ptpython_repl.user_code_complete_callback = MagicMock(
- wraps=pw_ptpython_repl.user_code_complete_callback)
+ pw_ptpython_repl.user_code_complete_callback = ( # type: ignore
+ MagicMock(
+ wraps=pw_ptpython_repl.user_code_complete_callback))
# Repl done flag for tests
user_code_done = threading.Event()
diff --git a/pw_watch/py/pw_watch/watch_app.py b/pw_watch/py/pw_watch/watch_app.py
index 98432ec..b654f77 100644
--- a/pw_watch/py/pw_watch/watch_app.py
+++ b/pw_watch/py/pw_watch/watch_app.py
@@ -118,8 +118,9 @@
self.ninja_log_pane.log_view.log_store.formatter = logging.Formatter(
'%(message)s')
self.ninja_log_pane.table_view = False
- # Enable line wrapping
- self.ninja_log_pane.toggle_wrap_lines()
+ # Disable line wrapping for improved error visibility.
+ if self.ninja_log_pane.wrap_lines:
+ self.ninja_log_pane.toggle_wrap_lines()
# Blank right side toolbar text
self.ninja_log_pane._pane_subtitle = ' '
self.ninja_log_view = self.ninja_log_pane.log_view
@@ -147,7 +148,8 @@
self.window_manager.add_pane(self.ninja_log_pane)
- self.time_waster = Twenty48Pane(self)
+ self.time_waster = Twenty48Pane(include_resize_handle=True)
+ self.time_waster.application = self
self.time_waster.show_pane = False
self.window_manager.add_pane(self.time_waster)
@@ -200,7 +202,7 @@
"Rebuild."
self.run_build()
- @key_bindings.add('c-g', filter=self.input_box_not_focused())
+ @key_bindings.add('c-t', filter=self.input_box_not_focused())
def _pass_time(_event):
"Rebuild."
self.time_waster.show_pane = not self.time_waster.show_pane
@@ -306,6 +308,9 @@
self.ninja_log_view.log_store.clear_logs()
self.ninja_log_view._restart_filtering() # pylint: disable=protected-access
self.ninja_log_view.view_mode_changed()
+ # Re-enable follow if needed
+ if not self.ninja_log_view.follow:
+ self.ninja_log_view.toggle_follow()
def run_build(self):
"""Manually trigger a rebuild."""