blob: 17a0dfe18b474831a438af667c96f27894a707a9 [file] [log] [blame]
# Copyright 2021 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Window pane base class."""
from abc import ABC
from typing import Any, Callable, List, Optional, Tuple, TYPE_CHECKING, Union
import functools
from prompt_toolkit.layout.dimension import AnyDimension
from prompt_toolkit.filters import Condition
from prompt_toolkit.layout import (
AnyContainer,
ConditionalContainer,
Dimension,
HSplit,
walk,
)
from prompt_toolkit.widgets import MenuItem
from pw_console.get_pw_console_app import get_pw_console_app
import pw_console.widgets.checkbox
import pw_console.widgets.mouse_handlers
import pw_console.style
if TYPE_CHECKING:
from pw_console.console_app import ConsoleApp
class WindowPaneHSplit(HSplit):
"""PromptToolkit HSplit that saves the current width and height.
This overrides the write_to_screen function to save the width and height of
the container to be rendered.
"""
def __init__(self, parent_window_pane, *args, **kwargs):
# Save a reference to the parent window pane.
self.parent_window_pane = parent_window_pane
super().__init__(*args, **kwargs)
def write_to_screen(
self,
screen,
mouse_handlers,
write_position,
parent_style: str,
erase_bg: bool,
z_index: Optional[int],
) -> None:
# Save the width and height for the current render pass. This will be
# used by the log pane to render the correct amount of log lines.
self.parent_window_pane.update_pane_size(write_position.width,
write_position.height)
# Continue writing content to the screen.
super().write_to_screen(screen, mouse_handlers, write_position,
parent_style, erase_bg, z_index)
class WindowPane(ABC):
"""The Pigweed Console Window Pane parent class."""
# pylint: disable=too-many-instance-attributes
def __init__(
self,
application: Union['ConsoleApp', Any] = None,
pane_title: str = 'Window',
height: Optional[AnyDimension] = None,
width: Optional[AnyDimension] = None,
):
if application:
self.application = application
else:
self.application = get_pw_console_app()
self._pane_title = pane_title
self._pane_subtitle: str = ''
# Default width and height to 10 lines each. They will be resized by the
# WindowManager later.
self.height = height if height else Dimension(preferred=10)
self.width = width if width else Dimension(preferred=10)
# Boolean to show or hide this window pane
self.show_pane = True
# Booleans for toggling top and bottom toolbars
self.show_top_toolbar = True
self.show_bottom_toolbar = True
# Height and width values for the current rendering pass.
self.current_pane_width = 0
self.current_pane_height = 0
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
def set_pane_title(self, title: str) -> None:
self._pane_title = title
def menu_title(self) -> str:
"""Return a title to display in the Window menu."""
return self.pane_title()
def pane_subtitle(self) -> str: # pylint: disable=no-self-use
"""Further title info for display in the Window menu."""
return ''
def redraw_ui(self) -> None:
"""Redraw the prompt_toolkit UI."""
if not hasattr(self, 'application'):
return
# Thread safe way of sending a repaint trigger to the input event loop.
self.application.redraw_ui()
def focus_self(self) -> None:
"""Switch prompt_toolkit focus to this window pane."""
if not hasattr(self, 'application'):
return
self.application.focus_on_container(self)
def __pt_container__(self):
"""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 # pylint: disable=no-member
def get_all_key_bindings(self) -> List:
"""Return keybinds for display in the help window.
For example:
Using a prompt_toolkit control:
return [self.some_content_control_instance.get_key_bindings()]
Hand-crafted bindings for display in the HelpWindow:
return [{
'Execute code': ['Enter', 'Option-Enter', 'Meta-Enter'],
'Reverse search history': ['Ctrl-R'],
'Erase input buffer.': ['Ctrl-C'],
'Show settings.': ['F2'],
'Show history.': ['F3'],
}]
"""
# pylint: disable=no-self-use
return []
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
callable to invoke on click.
"""
# 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
or self.last_pane_height != self.current_pane_height)
def update_pane_size(self, width, height) -> None:
"""Save pane width and height for the current UI render pass."""
if width:
self.last_pane_width = self.current_pane_width
self.current_pane_width = width
if height:
self.last_pane_height = self.current_pane_height
self.current_pane_height = height
def _create_pane_container(self, *content) -> ConditionalContainer:
return ConditionalContainer(
WindowPaneHSplit(
self,
content,
# Window pane dimensions
height=lambda: self.height,
width=lambda: self.width,
style=functools.partial(pw_console.style.get_pane_style, self),
),
filter=Condition(lambda: self.show_pane))
def has_child_container(self, child_container: AnyContainer) -> bool:
if not child_container:
return False
for container in walk(self.__pt_container__()):
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