blob: f142a8af8d135d254293da603ca9b1b739ea69d4 [file] [log] [blame]
# Copyright 2022 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.
"""LogPane Save As Dialog."""
from __future__ import annotations
import functools
from pathlib import Path
from typing import Optional, TYPE_CHECKING
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
from prompt_toolkit.completion import PathCompleter
from prompt_toolkit.filters import Condition
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.layout import (
ConditionalContainer,
FormattedTextControl,
HSplit,
Window,
WindowAlign,
)
from prompt_toolkit.widgets import TextArea
from prompt_toolkit.validation import (
ValidationError,
Validator,
)
import pw_console.widgets.checkbox
import pw_console.widgets.border
import pw_console.widgets.mouse_handlers
import pw_console.style
if TYPE_CHECKING:
from pw_console.log_pane import LogPane
class PathValidator(Validator):
"""Validation of file path input."""
def validate(self, document):
"""Check input path leads to a valid parent directory."""
target_path = Path(document.text).expanduser()
if not target_path.parent.exists():
raise ValidationError(
# Set cursor position to the end
len(document.text),
"Directory doesn't exist: %s" % document.text)
if target_path.is_dir():
raise ValidationError(
# Set cursor position to the end
len(document.text),
"File input is an existing directory: %s" % document.text)
class LogPaneSaveAsDialog(ConditionalContainer):
"""Dialog box for saving logs to a file."""
# Height of the dialog box contens in lines of text.
DIALOG_HEIGHT = 3
def __init__(self, log_pane: 'LogPane'):
self.log_pane = log_pane
self.path_validator = PathValidator()
self._export_with_table_formatting: bool = True
self._export_with_selected_lines_only: bool = False
self.starting_file_path: str = str(Path.cwd())
self.input_field = TextArea(
prompt=[
('class:saveas-dialog-setting', 'File: ',
functools.partial(pw_console.widgets.mouse_handlers.on_click,
self.focus_self))
],
# Pre-fill the current working directory.
text=self.starting_file_path,
focusable=True,
focus_on_click=True,
scrollbar=False,
multiline=False,
height=1,
dont_extend_height=True,
dont_extend_width=False,
accept_handler=self._saveas_accept_handler,
validator=self.path_validator,
history=InMemoryHistory(),
completer=PathCompleter(expanduser=True),
)
self.input_field.buffer.cursor_position = len(self.starting_file_path)
settings_bar_control = FormattedTextControl(
self.get_settings_fragments)
settings_bar_window = Window(content=settings_bar_control,
height=1,
align=WindowAlign.LEFT,
dont_extend_width=False)
action_bar_control = FormattedTextControl(self.get_action_fragments)
action_bar_window = Window(content=action_bar_control,
height=1,
align=WindowAlign.RIGHT,
dont_extend_width=False)
# Add additional keybindings for the input_field text area.
key_bindings = KeyBindings()
register = self.log_pane.application.prefs.register_keybinding
@register('save-as-dialog.cancel', key_bindings)
def _close_saveas_dialog(_event: KeyPressEvent) -> None:
"""Close save as dialog."""
self.close_dialog()
self.input_field.control.key_bindings = key_bindings
super().__init__(
pw_console.widgets.border.create_border(
HSplit(
[
settings_bar_window,
self.input_field,
action_bar_window,
],
height=LogPaneSaveAsDialog.DIALOG_HEIGHT,
style='class:saveas-dialog',
),
LogPaneSaveAsDialog.DIALOG_HEIGHT,
border_style='class:saveas-dialog-border',
left_margin_columns=1,
),
filter=Condition(lambda: self.log_pane.saveas_dialog_active),
)
def focus_self(self):
self.log_pane.application.application.layout.focus(self)
def close_dialog(self):
"""Close this dialog."""
self.log_pane.saveas_dialog_active = False
self.log_pane.application.focus_on_container(self.log_pane)
self.log_pane.redraw_ui()
def _toggle_table_formatting(self):
self._export_with_table_formatting = (
not self._export_with_table_formatting)
def _toggle_selected_lines(self):
self._export_with_selected_lines_only = (
not self._export_with_selected_lines_only)
def set_export_options(self,
table_format: Optional[bool] = None,
selected_lines_only: Optional[bool] = None) -> None:
# Allows external callers such as the line selection dialog to set
# export format options.
if table_format is not None:
self._export_with_table_formatting = table_format
if selected_lines_only is not None:
self._export_with_selected_lines_only = selected_lines_only
def save_action(self):
"""Trigger save file execution on mouse click.
This ultimately runs LogPaneSaveAsDialog._saveas_accept_handler()."""
self.input_field.buffer.validate_and_handle()
def _saveas_accept_handler(self, buff: Buffer) -> bool:
"""Function run when hitting Enter in the input_field."""
input_text = buff.text
if len(input_text) == 0:
self.close_dialog()
# Don't save anything if empty input.
return False
if self.log_pane.log_view.export_logs(
file_name=input_text,
use_table_formatting=self._export_with_table_formatting,
selected_lines_only=self._export_with_selected_lines_only):
self.close_dialog()
# Reset selected_lines_only
self.set_export_options(selected_lines_only=False)
# Erase existing input text.
return False
# Keep existing text if error
return True
def get_settings_fragments(self):
"""Return FormattedText with current save settings."""
# Mouse handlers
focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
self.focus_self)
toggle_table_formatting = functools.partial(
pw_console.widgets.mouse_handlers.on_click,
self._toggle_table_formatting)
toggle_selected_lines = functools.partial(
pw_console.widgets.mouse_handlers.on_click,
self._toggle_selected_lines)
# Separator should have the focus mouse handler so clicking on any
# whitespace focuses the input field.
separator_text = ('', ' ', focus)
# Default button style
button_style = 'class:toolbar-button-inactive'
fragments = [('class:saveas-dialog-title', 'Save as File', focus)]
fragments.append(separator_text)
# Table checkbox
fragments.extend(
pw_console.widgets.checkbox.to_checkbox_with_keybind_indicator(
checked=self._export_with_table_formatting,
key='', # No key shortcut help text
description='Table Formatting',
mouse_handler=toggle_table_formatting,
base_style=button_style))
# Two space separator
fragments.append(separator_text)
# Selected lines checkbox
fragments.extend(
pw_console.widgets.checkbox.to_checkbox_with_keybind_indicator(
checked=self._export_with_selected_lines_only,
key='', # No key shortcut help text
description='Selected Lines Only',
mouse_handler=toggle_selected_lines,
base_style=button_style))
# Two space separator
fragments.append(separator_text)
return fragments
def get_action_fragments(self):
"""Return FormattedText with the save action buttons."""
# Mouse handlers
focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
self.focus_self)
cancel = functools.partial(pw_console.widgets.mouse_handlers.on_click,
self.close_dialog)
save = functools.partial(pw_console.widgets.mouse_handlers.on_click,
self.save_action)
# Separator should have the focus mouse handler so clicking on any
# whitespace focuses the input field.
separator_text = ('', ' ', focus)
# Default button style
button_style = 'class:toolbar-button-inactive'
fragments = [separator_text]
# Cancel button
fragments.extend(
pw_console.widgets.checkbox.to_keybind_indicator(
key='Ctrl-c',
description='Cancel',
mouse_handler=cancel,
base_style=button_style,
))
# Two space separator
fragments.append(separator_text)
# Save button
fragments.extend(
pw_console.widgets.checkbox.to_keybind_indicator(
key='Enter',
description='Save',
mouse_handler=save,
base_style=button_style,
))
# One space separator
fragments.append(('', ' ', focus))
return fragments