xref: /aosp_15_r20/external/pigweed/pw_console/py/pw_console/quit_dialog.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2022 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""LogPane Info Toolbar classes."""
15
16from __future__ import annotations
17import functools
18import logging
19import sys
20from typing import Callable, TYPE_CHECKING
21
22from prompt_toolkit.data_structures import Point
23from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
24from prompt_toolkit.filters import Condition
25from prompt_toolkit.layout import (
26    ConditionalContainer,
27    FormattedTextControl,
28    HSplit,
29    Window,
30    WindowAlign,
31)
32
33from pw_console.widgets import (
34    create_border,
35    mouse_handlers,
36    to_keybind_indicator,
37)
38
39if TYPE_CHECKING:
40    from pw_console.console_app import ConsoleApp
41
42_LOG = logging.getLogger(__package__)
43
44
45class QuitDialog(ConditionalContainer):
46    """Confirmation quit dialog box."""
47
48    DIALOG_HEIGHT = 2
49
50    def __init__(
51        self, application: ConsoleApp, on_quit: Callable | None = None
52    ):
53        self.application = application
54        self.show_dialog = False
55        # Tracks the last focused container, to enable restoring focus after
56        # closing the dialog.
57        self.last_focused_pane = None
58
59        self.on_quit_function = (
60            on_quit if on_quit else self._default_on_quit_function
61        )
62
63        # Quit keybindings are active when this dialog is in focus
64        key_bindings = KeyBindings()
65        register = self.application.prefs.register_keybinding
66
67        @register('quit-dialog.yes', key_bindings)
68        def _quit(_event: KeyPressEvent) -> None:
69            """Close save as bar."""
70            self.quit_action()
71
72        @register('quit-dialog.no', key_bindings)
73        def _cancel(_event: KeyPressEvent) -> None:
74            """Close save as bar."""
75            self.close_dialog()
76
77        self.exit_message = 'Quit? y/n '
78
79        action_bar_control = FormattedTextControl(
80            self.get_action_fragments,
81            show_cursor=True,
82            focusable=True,
83            key_bindings=key_bindings,
84            # Cursor will appear after the exit_message
85            get_cursor_position=lambda: Point(len(self.exit_message), 0),
86        )
87
88        action_bar_window = Window(
89            content=action_bar_control,
90            height=QuitDialog.DIALOG_HEIGHT,
91            align=WindowAlign.LEFT,
92            dont_extend_width=False,
93        )
94
95        super().__init__(
96            create_border(
97                HSplit(
98                    [action_bar_window],
99                    height=QuitDialog.DIALOG_HEIGHT,
100                    style='class:quit-dialog',
101                ),
102                QuitDialog.DIALOG_HEIGHT,
103                border_style='class:quit-dialog-border',
104                left_margin_columns=1,
105            ),
106            filter=Condition(lambda: self.show_dialog),
107        )
108
109    def focus_self(self):
110        self.application.layout.focus(self)
111
112    def close_dialog(self):
113        """Close this dialog box."""
114        self.show_dialog = False
115        # Restore original focus if possible.
116        if self.last_focused_pane:
117            self.application.layout.focus(self.last_focused_pane)
118        else:
119            # Fallback to focusing on the main menu.
120            self.application.focus_main_menu()
121
122    def open_dialog(self):
123        self.show_dialog = True
124        self.last_focused_pane = self.application.focused_window()
125        self.focus_self()
126        self.application.redraw_ui()
127
128    def _default_on_quit_function(self):
129        if hasattr(self.application, 'application'):
130            self.application.application.exit()
131        else:
132            sys.exit()
133
134    def quit_action(self):
135        self.on_quit_function()
136
137    def get_action_fragments(self):
138        """Return FormattedText with action buttons."""
139
140        # Mouse handlers
141        focus = functools.partial(mouse_handlers.on_click, self.focus_self)
142        cancel = functools.partial(mouse_handlers.on_click, self.close_dialog)
143        quit_action = functools.partial(
144            mouse_handlers.on_click, self.quit_action
145        )
146
147        # Separator should have the focus mouse handler so clicking on any
148        # whitespace focuses the input field.
149        separator_text = ('', '  ', focus)
150
151        # Default button style
152        button_style = 'class:toolbar-button-inactive'
153
154        fragments = [('', self.exit_message), separator_text]
155        fragments.append(('', '\n'))
156
157        # Cancel button
158        fragments.extend(
159            to_keybind_indicator(
160                key='n / Ctrl-c',
161                description='Cancel',
162                mouse_handler=cancel,
163                base_style=button_style,
164            )
165        )
166
167        # Two space separator
168        fragments.append(separator_text)
169
170        # Save button
171        fragments.extend(
172            to_keybind_indicator(
173                key='y / Ctrl-d',
174                description='Quit',
175                mouse_handler=quit_action,
176                base_style=button_style,
177            )
178        )
179
180        # One space separator
181        fragments.append(('', ' ', focus))
182
183        return fragments
184