diff --git a/subiquity/ui/views/ubuntu_pro.py b/subiquity/ui/views/ubuntu_pro.py index cfe96ea6..8b1de997 100644 --- a/subiquity/ui/views/ubuntu_pro.py +++ b/subiquity/ui/views/ubuntu_pro.py @@ -14,6 +14,7 @@ # along with this program. If not, see . """ Module that defines the view class for Ubuntu Pro configuration. """ +import asyncio import logging import re from typing import Callable, List @@ -325,6 +326,9 @@ class UbuntuProView(BaseView): connect_signal(self.upgrade_mode_form, 'cancel', on_upgrade_mode_cancel) + # Throwaway tasks + self.tasks: List[asyncio.Task] = [] + super().__init__(self.upgrade_yes_no_screen()) def upgrade_mode_screen(self) -> Widget: @@ -644,7 +648,19 @@ class UbuntuProView(BaseView): network connection, temporary service unavailability, API issue ... The user is prompted to continue anyway or go back. """ - self.show_stretchy_overlay(ContinueAnywayWidget(self)) + question = _("Unable to check your subscription information." + " Do you want to go back or continue anyway?") + + async def confirm_continue_anyway() -> None: + confirmed = await self.ask_confirmation( + title=_("Unknown error"), question=question, + cancel_label=_("Back"), confirm_label=_("Continue anyway")) + + if confirmed: + subform = self.upgrade_mode_form.with_contract_token_subform + self.controller.done(subform.value["token"]) + + self.tasks.append(asyncio.create_task(confirm_continue_anyway())) def show_subscription(self, subscription: UbuntuProSubscription) -> None: """ Display a screen with information about the subscription, including @@ -860,39 +876,3 @@ class HowToRegisterWidget(Stretchy): def close(self) -> None: """ Close the overlay. """ self.parent.remove_overlay() - - -class ContinueAnywayWidget(Stretchy): - """ Widget that requests the user if he wants to go back or continue - anyway. - +--------------------- Unknown error ---------------------+ - | | - | Unable to check your subscription information. Do you | - | want to go back or continue anyway? | - | | - | [ Back ] | - | [ Continue anyway ] | - +---------------------------------------------------------+ - """ - def __init__(self, parent: UbuntuProView) -> None: - """ Initializes the widget by showing two buttons, one to go back and - one to move forward anyway. """ - self.parent = parent - back = back_btn(label=_("Back"), on_press=self.back) - cont = done_btn(label=_("Continue anyway"), on_press=self.cont) - widgets = [ - Text("Unable to check your subscription information." - " Do you want to go back or continue anyway?"), - Text(""), - button_pile([back, cont]), - ] - super().__init__("Unknown error", widgets, 0, 2) - - def back(self, sender) -> None: - """ Close the overlay. """ - self.parent.remove_overlay() - - def cont(self, sender) -> None: - """ Move on to the next screen. """ - subform = self.parent.upgrade_mode_form.with_contract_token_subform - self.parent.controller.done(subform.value["token"]) 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. """