pw_console: Re-style buttons
Change keybind -> Function toolbar text to look more like a button
by surrounding them with styled brackets. E.g. [Function Keybind]
Add a Window title and close button to the top of modal help windows.
Make F1 close help window if it's already open.
Remove repl minimum window height restriction.
Bug: 474
Bug: 475
Change-Id: If7aae0f4045e8125317c1fff51d756f2bb8bd926
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/56787
Reviewed-by: Joe Ethier <jethier@google.com>
Pigweed-Auto-Submit: Anthony DiGirolamo <tonymd@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_console/py/pw_console/console_app.py b/pw_console/py/pw_console/console_app.py
index 752c306..3a2e722 100644
--- a/pw_console/py/pw_console/console_app.py
+++ b/pw_console/py/pw_console/console_app.py
@@ -56,6 +56,7 @@
import pw_console.key_bindings
import pw_console.widgets.checkbox
+import pw_console.widgets.mouse_handlers
import pw_console.style
from pw_console.help_window import HelpWindow
from pw_console.log_pane import LogPane
@@ -164,30 +165,36 @@
self.app_title = app_title if app_title else 'Pigweed Console'
- # Top title message
- self.message = [
- ('class:logo', self.app_title),
- ('class:menu-bar', ' '),
- ]
- self.message.extend(
- pw_console.widgets.checkbox.to_keybind_indicator('F1', 'Help '))
- self.message.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
- 'Ctrl-W', 'Quit '))
-
# Top level UI state toggles.
self.load_theme()
# Pigweed upstream RST user guide
- self.user_guide_window = HelpWindow(self)
+ self.user_guide_window = HelpWindow(self, title='User Guide')
self.user_guide_window.load_user_guide()
+ # Top title message
+ self.message = [('class:logo', self.app_title), ('', ' ')]
+
+ self.message.extend(
+ pw_console.widgets.checkbox.to_keybind_indicator(
+ 'F1', 'Help',
+ functools.partial(pw_console.widgets.mouse_handlers.on_click,
+ self.user_guide_window.toggle_display)))
+
+ # Two space separator
+ self.message.append(('', ' '))
+
+ self.message.extend(
+ pw_console.widgets.checkbox.to_keybind_indicator('Ctrl-W', 'Quit'))
+
# Auto-generated keybindings list for all active panes
- self.keybind_help_window = HelpWindow(self)
+ self.keybind_help_window = HelpWindow(self, title='Keyboard Shortcuts')
# Downstream project specific help text
self.app_help_text = help_text if help_text else None
- self.app_help_window = HelpWindow(self, additional_help_text=help_text)
+ self.app_help_window = HelpWindow(self,
+ additional_help_text=help_text,
+ title=(self.app_title + ' Help'))
self.app_help_window.generate_help_text()
# Used for tracking which pane was in focus before showing help window.
@@ -475,16 +482,16 @@
window_menu = self.window_manager.create_window_menu()
help_menu_items = [
- MenuItem('User Guide',
+ MenuItem(self.user_guide_window.menu_title(),
handler=self.user_guide_window.toggle_display),
- MenuItem('Keyboard Shortcuts',
+ MenuItem(self.keybind_help_window.menu_title(),
handler=self.keybind_help_window.toggle_display),
]
if self.app_help_text:
help_menu_items.extend([
MenuItem('-'),
- MenuItem(self.app_title + ' Help',
+ MenuItem(self.app_help_window.menu_title(),
handler=self.app_help_window.toggle_display)
])
diff --git a/pw_console/py/pw_console/help_window.py b/pw_console/py/pw_console/help_window.py
index 9d3867a..6784b72 100644
--- a/pw_console/py/pw_console/help_window.py
+++ b/pw_console/py/pw_console/help_window.py
@@ -13,10 +13,11 @@
# the License.
"""Help window container class."""
-import logging
+import functools
import inspect
+import logging
from pathlib import Path
-from typing import Dict
+from typing import Dict, TYPE_CHECKING
from prompt_toolkit.document import Document
from prompt_toolkit.filters import Condition
@@ -24,13 +25,21 @@
from prompt_toolkit.layout import (
ConditionalContainer,
DynamicContainer,
+ FormattedTextControl,
HSplit,
+ VSplit,
+ Window,
+ WindowAlign,
)
from prompt_toolkit.layout.dimension import Dimension
from prompt_toolkit.lexers import PygmentsLexer
-from prompt_toolkit.widgets import Box, Frame, TextArea
+from prompt_toolkit.widgets import Box, TextArea
from pygments.lexers.markup import RstLexer # type: ignore
+import pw_console.widgets.mouse_handlers
+
+if TYPE_CHECKING:
+ from pw_console.console_app import ConsoleApp
_LOG = logging.getLogger(__package__)
@@ -46,6 +55,9 @@
class HelpWindow(ConditionalContainer):
"""Help window container for displaying keybindings."""
+
+ # pylint: disable=too-many-instance-attributes
+
def _create_help_text_area(self, **kwargs):
help_text_area = TextArea(
focusable=True,
@@ -59,6 +71,7 @@
key_bindings = KeyBindings()
@key_bindings.add('q')
+ @key_bindings.add('f1')
def _close_window(_event: KeyPressEvent) -> None:
"""Close the current dialog window."""
self.toggle_display()
@@ -66,40 +79,95 @@
help_text_area.control.key_bindings = key_bindings
return help_text_area
- def __init__(self, application, preamble='', additional_help_text=''):
+ def __init__(self,
+ application: 'ConsoleApp',
+ preamble: str = '',
+ additional_help_text: str = '',
+ title: str = '') -> None:
# Dict containing key = section title and value = list of key bindings.
- self.application = application
- self.show_window = False
- self.help_text_sections = {}
+ self.application: 'ConsoleApp' = application
+ self.show_window: bool = False
+ self.help_text_sections: Dict[str, Dict] = {}
+ self._pane_title: str = title
# Generated keybinding text
- self.preamble = preamble
- self.additional_help_text = additional_help_text
- self.help_text = ''
+ self.preamble: str = preamble
+ self.additional_help_text: str = additional_help_text
+ self.help_text: str = ''
- self.max_additional_help_text_width = (_longest_line_length(
+ self.max_additional_help_text_width: int = (_longest_line_length(
self.additional_help_text) if additional_help_text else 0)
- self.max_description_width = 0
- self.max_key_list_width = 0
- self.max_line_length = 0
+ self.max_description_width: int = 0
+ self.max_key_list_width: int = 0
+ self.max_line_length: int = 0
- self.help_text_area = self._create_help_text_area()
+ self.help_text_area: TextArea = self._create_help_text_area()
- frame = Frame(
- body=Box(
+ close_mouse_handler = functools.partial(
+ pw_console.widgets.mouse_handlers.on_click, self.toggle_display)
+
+ toolbar_padding = 1
+ toolbar_title = ' ' * toolbar_padding
+ toolbar_title += self.pane_title()
+
+ top_toolbar = VSplit(
+ [
+ Window(
+ content=FormattedTextControl(
+ # [('', toolbar_title)]
+ functools.partial(pw_console.style.get_pane_indicator,
+ self, toolbar_title)),
+ align=WindowAlign.LEFT,
+ dont_extend_width=True,
+ ),
+ Window(
+ content=FormattedTextControl([]),
+ align=WindowAlign.LEFT,
+ dont_extend_width=False,
+ ),
+ Window(
+ content=FormattedTextControl(
+ pw_console.widgets.checkbox.to_keybind_indicator(
+ 'q', 'Close', close_mouse_handler)),
+ align=WindowAlign.RIGHT,
+ dont_extend_width=True,
+ ),
+ ],
+ height=1,
+ style='class:toolbar_active',
+ )
+
+ self.container = HSplit([
+ top_toolbar,
+ Box(
body=DynamicContainer(lambda: self.help_text_area),
padding=Dimension(preferred=1, max=1),
padding_bottom=0,
padding_top=0,
char=' ',
style='class:frame.border', # Same style used for Frame.
- ), )
+ ),
+ ])
super().__init__(
- HSplit([frame]),
+ self.container,
filter=Condition(lambda: self.show_window),
)
+ def pane_title(self):
+ return self._pane_title
+
+ def menu_title(self):
+ """Return the title to display in the Window menu."""
+ return self.pane_title()
+
+ def __pt_container__(self):
+ """Return the prompt_toolkit container for displaying this HelpWindow.
+
+ This allows self to be used wherever prompt_toolkit expects a container
+ object."""
+ return self.container
+
def toggle_display(self):
"""Toggle visibility of this help window."""
# Toggle state variable.
diff --git a/pw_console/py/pw_console/log_pane.py b/pw_console/py/pw_console/log_pane.py
index 6bdcbff..97928e1 100644
--- a/pw_console/py/pw_console/log_pane.py
+++ b/pw_console/py/pw_console/log_pane.py
@@ -500,7 +500,10 @@
self.redraw_ui()
def __pt_container__(self):
- """Return the prompt_toolkit root container for this log pane."""
+ """Return the prompt_toolkit root container for this log pane.
+
+ This allows self to be used wherever prompt_toolkit expects a container
+ object."""
return self.container
def get_all_key_bindings(self) -> List:
diff --git a/pw_console/py/pw_console/log_pane_toolbars.py b/pw_console/py/pw_console/log_pane_toolbars.py
index dc8413d..28d7766 100644
--- a/pw_console/py/pw_console/log_pane_toolbars.py
+++ b/pw_console/py/pw_console/log_pane_toolbars.py
@@ -183,7 +183,9 @@
self.log_pane.focus_self)
fragments = []
if not has_focus(self.log_pane.__pt_container__())():
- fragments.append(('class:keyhelp', '[click to focus] ', focus))
+ fragments.append(('class:toolbar-button-decoration', '[', focus))
+ fragments.append(('class:keyhelp', 'click to focus', focus))
+ fragments.append(('class:toolbar-button-decoration', '] ', focus))
fragments.append(
('', ' {} '.format(self.log_pane.pane_subtitle()), focus))
return fragments
diff --git a/pw_console/py/pw_console/pw_ptpython_repl.py b/pw_console/py/pw_console/pw_ptpython_repl.py
index 07e47a9..5d0bf79 100644
--- a/pw_console/py/pw_console/pw_ptpython_repl.py
+++ b/pw_console/py/pw_console/pw_ptpython_repl.py
@@ -73,7 +73,8 @@
super().__init__(
*args,
create_app=False,
- _input_buffer_height=Dimension(min=5, weight=30),
+ # Absolute minimum height of 1
+ _input_buffer_height=Dimension(min=1),
_completer=completer,
**ptpython_kwargs,
)
@@ -107,7 +108,10 @@
self._last_exception = None
def __pt_container__(self):
- """Return the prompt_toolkit root container for class."""
+ """Return the prompt_toolkit root container for class.
+
+ This allows self to be used wherever prompt_toolkit expects a container
+ object."""
return self.ptpython_layout.root_container
def set_repl_pane(self, repl_pane):
@@ -268,6 +272,9 @@
# Don't keep input for now. Return True to keep input text.
return False
+ def line_break_count(self) -> int:
+ return self.default_buffer.text.count('\n')
+
def has_focus_and_input_empty_condition(self) -> Condition:
@Condition
def test() -> bool:
diff --git a/pw_console/py/pw_console/repl_pane.py b/pw_console/py/pw_console/repl_pane.py
index 1058cf7..982e39b 100644
--- a/pw_console/py/pw_console/repl_pane.py
+++ b/pw_console/py/pw_console/repl_pane.py
@@ -130,14 +130,18 @@
pw_console.widgets.checkbox.to_keybind_indicator(
'F3', 'History'))
else:
+ focus = functools.partial(pw_console.mouse.focus_handler,
+ repl_pane)
+ fragments.append(('class:toolbar-button-decoration', '[', focus))
fragments.append((
# Style
'class:keyhelp',
# Text
- '[click to focus] ',
+ 'click to focus',
# Mouse handler
- functools.partial(pw_console.mouse.focus_handler, repl_pane),
+ focus,
))
+ fragments.append(('class:toolbar-button-decoration', '] ', focus))
return fragments
def __init__(self, repl_pane):
@@ -253,16 +257,11 @@
self,
application: Any,
python_repl: PwPtPythonRepl,
- # TODO(tonymd): Make the height of input+output windows match the log
- # pane height. (Using minimum output height of 5 for now).
- output_height: Optional[AnyDimension] = Dimension(min=5, weight=70),
- # TODO(tonymd): Figure out how to resize ptpython input field.
- _input_height: Optional[AnyDimension] = None,
- # Default width and height to 50% of the screen
height: Optional[AnyDimension] = None,
width: Optional[AnyDimension] = None,
startup_message: Optional[str] = None,
) -> None:
+ # Default width and height to 50% of the screen
self.height = height if height else Dimension(weight=50)
self.width = width if width else Dimension(weight=50)
self.show_pane = True
@@ -283,7 +282,6 @@
self.startup_message = startup_message if startup_message else ''
self.output_field = TextArea(
- height=output_height,
text=self.startup_message,
focusable=True,
focus_on_click=True,
@@ -312,19 +310,28 @@
ReplHSplit(
self,
[
- HSplit([
- # 1. Repl Output
- self.output_field,
- # 2. Static separator toolbar.
- self.results_toolbar,
- ]),
- HSplit([
- # 3. Repl Input
- self.pw_ptpython_repl,
- # 4. Bottom toolbar
- self.bottom_toolbar,
- ]),
+ HSplit(
+ [
+ # 1. Repl Output
+ self.output_field,
+ # 2. Static separator toolbar.
+ self.results_toolbar,
+ ],
+ # Output area only dimensions
+ height=self.get_output_height,
+ ),
+ HSplit(
+ [
+ # 3. Repl Input
+ self.pw_ptpython_repl,
+ # 4. Bottom toolbar
+ self.bottom_toolbar,
+ ],
+ # Input area only dimensions
+ height=self.get_input_height,
+ ),
],
+ # Repl pane dimensions
height=lambda: self.height,
width=lambda: self.width,
style=functools.partial(pw_console.style.get_pane_style,
@@ -340,6 +347,29 @@
]),
filter=Condition(lambda: self.show_pane))
+ def get_output_height(self) -> AnyDimension:
+ # pylint: disable=no-self-use
+ return Dimension(min=1)
+
+ def get_input_height(self) -> AnyDimension:
+ desired_max_height = 10
+ # Check number of line breaks in the input buffer.
+ input_line_count = self.pw_ptpython_repl.line_break_count()
+ if input_line_count > desired_max_height:
+ desired_max_height = input_line_count
+ # Check if it's taller than the available space
+ if desired_max_height > self.current_pane_height:
+ # Leave space for minimum of
+ # 1 line of content in the output
+ # + 1 for output toolbar
+ # + 1 for input toolbar
+ desired_max_height = self.current_pane_height - 3
+
+ if desired_max_height > 1:
+ return Dimension(min=1, max=desired_max_height)
+ # Fall back to at least a height of 1
+ return Dimension(min=1)
+
def update_pane_size(self, width, height):
"""Save width and height of the repl pane for the current UI render
pass."""
@@ -427,7 +457,10 @@
return ''
def __pt_container__(self):
- """Return the prompt_toolkit container for this ReplPane."""
+ """Return the prompt_toolkit container for this ReplPane.
+
+ This allows self to be used wherever prompt_toolkit expects a container
+ object."""
return self.container
def copy_output_selection(self):
diff --git a/pw_console/py/pw_console/search_toolbar.py b/pw_console/py/pw_console/search_toolbar.py
index 84691d9..0d63c20 100644
--- a/pw_console/py/pw_console/search_toolbar.py
+++ b/pw_console/py/pw_console/search_toolbar.py
@@ -139,14 +139,17 @@
]
fragments.extend(separator_text)
- fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
- 'Ctrl-t', 'Column:', next_field))
- fragments.extend([
+ selected_column_text = [
('class:search-bar-setting',
(self._search_field.title() if self._search_field else 'All'),
next_field),
- ])
+ ]
+ fragments.extend(
+ pw_console.widgets.checkbox.to_keybind_indicator(
+ 'Ctrl-t',
+ 'Column:',
+ next_field,
+ middle_fragments=selected_column_text))
fragments.extend(separator_text)
fragments.extend(
@@ -155,13 +158,16 @@
fragments.extend(separator_text)
# Matching Method
+ current_matcher_text = [
+ ('class:search-bar-setting',
+ str(self.log_pane.log_view.search_matcher.name), next_matcher)
+ ]
fragments.extend(
pw_console.widgets.checkbox.to_keybind_indicator(
- 'Ctrl-n', 'Matcher:', next_matcher))
- fragments.extend([
- ('class:search-bar-setting',
- str(self.log_pane.log_view.search_matcher.name), next_matcher),
- ])
+ 'Ctrl-n',
+ 'Matcher:',
+ next_matcher,
+ middle_fragments=current_matcher_text))
fragments.extend(separator_text)
return fragments
diff --git a/pw_console/py/pw_console/style.py b/pw_console/py/pw_console/style.py
index 4539866..1ee85eb 100644
--- a/pw_console/py/pw_console/style.py
+++ b/pw_console/py/pw_console/style.py
@@ -108,6 +108,7 @@
# Used for pane titles
'toolbar_accent': theme.cyan_accent,
+ 'toolbar-button-decoration': '{}'.format(theme.cyan_accent),
'toolbar-setting-active': 'bg:{} {}'.format(
theme.green_accent,
theme.active_bg,
diff --git a/pw_console/py/pw_console/widgets/checkbox.py b/pw_console/py/pw_console/widgets/checkbox.py
index eb669a8..71e01ff 100644
--- a/pw_console/py/pw_console/widgets/checkbox.py
+++ b/pw_console/py/pw_console/widgets/checkbox.py
@@ -20,11 +20,10 @@
from prompt_toolkit.formatted_text import StyleAndTextTuples
-_KEY_SEPARATOR = ' → '
+_KEY_SEPARATOR = ' '
_CHECKED_BOX = '[✓]'
if sys.platform in ['win32']:
- _KEY_SEPARATOR = ' : '
_CHECKED_BOX = '[x]'
@@ -67,32 +66,45 @@
key,
description,
mouse_handler,
- extra_fragments=[to_checkbox(checked, mouse_handler)])
+ leading_fragments=[to_checkbox(checked, mouse_handler)])
return to_keybind_indicator(key,
description,
- extra_fragments=[to_checkbox(checked)])
+ leading_fragments=[to_checkbox(checked)])
-def to_keybind_indicator(key: str,
- description: str,
- mouse_handler=None,
- extra_fragments: Optional[Iterable] = None):
+def to_keybind_indicator(
+ key: str,
+ description: str,
+ mouse_handler=None,
+ leading_fragments: Optional[Iterable] = None,
+ middle_fragments: Optional[Iterable] = None,
+):
"""Create a clickable keybind indicator for toolbars."""
fragments: StyleAndTextTuples = []
+ fragments.append(('class:toolbar-button-decoration', '['))
- if mouse_handler:
- fragments.append(('class:keybind', key, mouse_handler))
- fragments.append(('class:keyhelp', _KEY_SEPARATOR, mouse_handler))
- else:
- fragments.append(('class:keybind', key))
- fragments.append(('class:keyhelp', _KEY_SEPARATOR))
-
- if extra_fragments:
- for fragment in extra_fragments:
+ # Add any starting fragments first
+ if leading_fragments:
+ for fragment in leading_fragments:
fragments.append(fragment)
+ # Function name
if mouse_handler:
fragments.append(('class:keyhelp', description, mouse_handler))
else:
fragments.append(('class:keyhelp', description))
+
+ if middle_fragments:
+ for fragment in middle_fragments:
+ fragments.append(fragment)
+
+ # Separator and keybind
+ if mouse_handler:
+ fragments.append(('class:keyhelp', _KEY_SEPARATOR, mouse_handler))
+ fragments.append(('class:keybind', key, mouse_handler))
+ else:
+ fragments.append(('class:keyhelp', _KEY_SEPARATOR))
+ fragments.append(('class:keybind', key))
+
+ fragments.append(('class:toolbar-button-decoration', ']'))
return fragments
diff --git a/pw_console/testing.rst b/pw_console/testing.rst
index e4a959b..1e35ebd 100644
--- a/pw_console/testing.rst
+++ b/pw_console/testing.rst
@@ -221,7 +221,7 @@
- |checkbox|
* - 4
- - Press :guilabel:`q`
+ - Press :guilabel:`f1`
- Window is hidden
- |checkbox|
@@ -232,7 +232,7 @@
- |checkbox|
* - 6
- - Press :guilabel:`q`
+ - Click the :guilabel:`[Close q]` button.
- Window is hidden
- |checkbox|