diff --git a/subiquitycore/ui/confirmation.py b/subiquitycore/ui/confirmation.py new file mode 100644 index 00000000..34e53608 --- /dev/null +++ b/subiquitycore/ui/confirmation.py @@ -0,0 +1,65 @@ +# Copyright 2023 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from typing import Callable + +import urwid + +from subiquitycore.ui.buttons import ( + back_btn, + done_btn, + ) +from subiquitycore.ui.utils import button_pile +from subiquitycore.ui.stretchy import Stretchy + + +class ConfirmationOverlay(Stretchy): + """ An overlay widget that asks the user to confirm or cancel an action. + """ + def __init__(self, title: str, question: str, + confirm_label: str, cancel_label: str, + on_confirm: Callable[[], None], + on_cancel: Callable[[], None]) -> None: + + self.on_cancel_cb = on_cancel + self.on_confirm_cb = on_confirm + self.choice_made = False + + widgets = [ + urwid.Text(question), + urwid.Text(""), + button_pile([ + back_btn( + label=cancel_label, on_press=lambda u: self.on_cancel()), + done_btn( + label=confirm_label, on_press=lambda u: self.on_confirm()), + ]), + ] + + super().__init__(title, widgets, 0, 2) + + def on_cancel(self) -> None: + self.choice_made = True + self.on_cancel_cb() + + def on_confirm(self) -> None: + self.choice_made = True + self.on_confirm_cb() + + def closed(self): + if self.choice_made: + return + # The caller should be careful not to close the overlay again. + self.on_cancel_cb() diff --git a/subiquitycore/view.py b/subiquitycore/view.py index bfc07314..5aac66bd 100644 --- a/subiquitycore/view.py +++ b/subiquitycore/view.py @@ -18,6 +18,7 @@ Contains some default key navigations """ +import asyncio import logging from urwid import ( @@ -31,6 +32,8 @@ from subiquitycore.ui.container import ( Pile, WidgetWrap, ) + +from subiquitycore.ui.confirmation import ConfirmationOverlay from subiquitycore.ui.stretchy import StretchyOverlay from subiquitycore.ui.utils import disabled, undisabled @@ -82,6 +85,37 @@ class BaseView(WidgetWrap): stretchy.opened() self._w = StretchyOverlay(disabled(self._w), stretchy) + async def ask_confirmation(self, title: str, question: str, + confirm_label: str, cancel_label: str) -> bool: + """ Open a confirmation dialog using a strechy overlay. + If the user selects the "yes" button, the function returns True. + If the user selects the "no" button or closes the dialog, the function + returns False. + """ + confirm_queue = asyncio.Queue(maxsize=1) + + def on_confirm(): + confirm_queue.put_nowait(True) + + def on_cancel(): + confirm_queue.put_nowait(False) + + stretchy = ConfirmationOverlay(title=title, question=question, + confirm_label=confirm_label, + cancel_label=cancel_label, + on_confirm=on_confirm, + on_cancel=on_cancel) + + self.show_stretchy_overlay(stretchy) + + confirmed = await confirm_queue.get() + # The callback might have been called as the result of the overlay + # getting closed (when ESC is pressed). Therefore, the overlay may or + # may not still be opened. + self.remove_overlay(stretchy, not_found_ok=True) + + return confirmed + def remove_overlay(self, stretchy=None, *, not_found_ok=False) -> None: """ Remove (frontmost) overlay from the view. """