ui: redraw screens after moving from one screen to another

Wieh urwid > 2.1.2, the screen is only redrawn after urwid handles an
event (e.g., keypress, resize, ...) or an urwid alarm (it is like a
timer).

All other changes made to the UI do not trigger a screen redraw. We now
redraw the screen properly after moving from one screen to another.

Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
This commit is contained in:
Olivier Gayot 2024-03-18 13:39:50 +01:00
parent eca86c58da
commit 020dcc0b88
27 changed files with 96 additions and 65 deletions

View File

@ -270,10 +270,10 @@ class ExampleController(SubiquityTuiController):
return ExampleView(self, thing) return ExampleView(self, thing)
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()
def done(self, thing): def done(self, thing):
self.app.next_screen(self.endpoint.POST(thing)) self.app.request_next_screen(self.endpoint.POST(thing))
``` ```
Setting `endpoint_name` means that self.client gets set to an implementation of Setting `endpoint_name` means that self.client gets set to an implementation of

View File

@ -70,7 +70,7 @@ class RecoveryChooserController(RecoveryChooserBaseController):
def select(self, system, action): def select(self, system, action):
self.model.select(system, action) self.model.select(system, action)
self.app.next_screen() self.app.request_next_screen()
def more_options(self): def more_options(self):
self._current_view = self._all_view self._current_view = self._all_view
@ -96,4 +96,4 @@ class RecoveryChooserConfirmController(RecoveryChooserBaseController):
def back(self): def back(self):
self.model.unselect() self.model.unselect()
self.app.prev_screen() self.app.request_prev_screen()

View File

@ -74,7 +74,7 @@ class TestChooserConfirmController(unittest.TestCase):
c.back() c.back()
app.respond.assert_not_called() app.respond.assert_not_called()
app.exit.assert_not_called() app.exit.assert_not_called()
app.prev_screen.assert_called() app.request_prev_screen.assert_called()
c.model.unselect.assert_called() c.model.unselect.assert_called()
def test_confirm(self): def test_confirm(self):
@ -106,7 +106,7 @@ class TestChooserController(unittest.TestCase):
) )
self.assertEqual(c.model.selection, exp) self.assertEqual(c.model.selection, exp)
app.next_screen.assert_called() app.request_next_screen.assert_called()
app.respond.assert_not_called() app.respond.assert_not_called()
app.exit.assert_not_called() app.exit.assert_not_called()

View File

@ -24,7 +24,7 @@ class WelcomeController(TuiController):
return self.welcome_view(self) return self.welcome_view(self)
def done(self): def done(self):
self.app.next_screen() self.app.request_next_screen()
def cancel(self): def cancel(self):
# Can't go back from here! # Can't go back from here!

View File

@ -483,7 +483,7 @@ class SubiquityClient(TuiApplication):
self._remove_last_screen() self._remove_last_screen()
super().exit() super().exit()
def select_initial_screen(self): async def select_initial_screen(self):
last_screen = None last_screen = None
if self.updated: if self.updated:
state_path = self.state_path("last-screen") state_path = self.state_path("last-screen")
@ -495,7 +495,7 @@ class SubiquityClient(TuiApplication):
for i, controller in enumerate(self.controllers.instances): for i, controller in enumerate(self.controllers.instances):
if controller.name == last_screen: if controller.name == last_screen:
index = i index = i
run_bg_task(self._select_initial_screen(index)) await self._select_initial_screen(index)
async def _select_initial_screen(self, index): async def _select_initial_screen(self, index):
endpoint_names = [] endpoint_names = []
@ -507,7 +507,7 @@ class SubiquityClient(TuiApplication):
if self.variant: if self.variant:
await self.client.meta.client_variant.POST(self.variant) await self.client.meta.client_variant.POST(self.variant)
self.controllers.index = index - 1 self.controllers.index = index - 1
self.next_screen() await self.next_screen()
async def move_screen(self, increment, coro): async def move_screen(self, increment, coro):
try: try:

View File

@ -62,8 +62,10 @@ class DriversController(SubiquityTuiController):
click(view.form.done_btn.base_widget) click(view.form.done_btn.base_widget)
def cancel(self) -> None: def cancel(self) -> None:
self.app.prev_screen() self.app.request_prev_screen()
def done(self, install: bool) -> None: def done(self, install: bool) -> None:
log.debug("DriversController.done next_screen install=%s", install) log.debug("DriversController.done next_screen install=%s", install)
self.app.next_screen(self.endpoint.POST(DriversPayload(install=install))) self.app.request_next_screen(
self.endpoint.POST(DriversPayload(install=install))
)

View File

@ -260,7 +260,7 @@ class FilesystemController(SubiquityTuiController, FilesystemManipulator):
async def _guided_choice(self, choice: GuidedChoiceV2): async def _guided_choice(self, choice: GuidedChoiceV2):
coro = self.endpoint.guided.POST(choice) coro = self.endpoint.guided.POST(choice)
if not choice.capability.supports_manual_customization(): if not choice.capability.supports_manual_customization():
self.app.next_screen(coro) await self.app.next_screen(coro)
return return
status = await self.app.wait_with_progress(coro) status = await self.app.wait_with_progress(coro)
self.model = FilesystemModel(status.bootloader, root="/") self.model = FilesystemModel(status.bootloader, root="/")
@ -294,11 +294,11 @@ class FilesystemController(SubiquityTuiController, FilesystemManipulator):
self.ui.set_body(FilesystemView(self.model, self)) self.ui.set_body(FilesystemView(self.model, self))
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()
def finish(self): def finish(self):
log.debug("FilesystemController.finish next_screen") log.debug("FilesystemController.finish next_screen")
self.app.next_screen( self.app.request_next_screen(
self.endpoint.POST( self.endpoint.POST(
self.model._render_actions(mode=ActionRenderMode.FOR_API_CLIENT) self.model._render_actions(mode=ActionRenderMode.FOR_API_CLIENT)
) )

View File

@ -43,11 +43,11 @@ class IdentityController(SubiquityTuiController):
self.done(identity) self.done(identity)
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()
def done(self, identity_data): def done(self, identity_data):
log.debug("IdentityController.done next_screen user_spec=%s", identity_data) log.debug("IdentityController.done next_screen user_spec=%s", identity_data)
self.app.next_screen(self.endpoint.POST(identity_data)) self.app.request_next_screen(self.endpoint.POST(identity_data))
async def validate_username(self, username): async def validate_username(self, username):
return await self.endpoint.validate_username.GET(username) return await self.endpoint.validate_username.GET(username)

View File

@ -49,7 +49,7 @@ class KeyboardController(SubiquityTuiController):
await self.endpoint.POST(setting) await self.endpoint.POST(setting)
def done(self): def done(self):
self.app.next_screen() self.app.request_next_screen()
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()

View File

@ -67,8 +67,8 @@ class MirrorController(SubiquityTuiController):
self.app.ui.body.form._click_done(None) self.app.ui.body.form._click_done(None)
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()
def done(self, mirror): def done(self, mirror):
log.debug("MirrorController.done next_screen mirror=%s", mirror) log.debug("MirrorController.done next_screen mirror=%s", mirror)
self.app.next_screen(self.endpoint.POST(MirrorPost(elected=mirror))) self.app.request_next_screen(self.endpoint.POST(MirrorPost(elected=mirror)))

View File

@ -118,10 +118,10 @@ class NetworkController(SubiquityTuiController, NetworkAnswersMixin):
run_bg_task(self.unsubscribe()) run_bg_task(self.unsubscribe())
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()
def done(self): def done(self):
self.app.next_screen(self.endpoint.POST()) self.app.request_next_screen(self.endpoint.POST())
def set_static_config( def set_static_config(
self, dev_name: str, ip_version: int, static_config: StaticConfig self, dev_name: str, ip_version: int, static_config: StaticConfig

View File

@ -33,8 +33,8 @@ class ProxyController(SubiquityTuiController):
self.done(self.answers["proxy"]) self.done(self.answers["proxy"])
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()
def done(self, proxy): def done(self, proxy):
log.debug("ProxyController.done next_screen proxy=%s", proxy) log.debug("ProxyController.done next_screen proxy=%s", proxy)
self.app.next_screen(self.endpoint.POST(proxy)) self.app.request_next_screen(self.endpoint.POST(proxy))

View File

@ -77,7 +77,7 @@ class RefreshController(SubiquityTuiController):
def done(self, sender=None): def done(self, sender=None):
log.debug("RefreshController.done next_screen") log.debug("RefreshController.done next_screen")
self.app.next_screen() self.app.request_next_screen()
def cancel(self, sender=None): def cancel(self, sender=None):
self.app.prev_screen() self.app.request_prev_screen()

View File

@ -39,7 +39,7 @@ class SerialController(SubiquityTuiController):
def done(self, rich): def done(self, rich):
log.debug("SerialController.done rich %s next_screen", rich) log.debug("SerialController.done rich %s next_screen", rich)
self.app.set_rich(rich) self.app.set_rich(rich)
self.app.next_screen() self.app.request_next_screen()
def cancel(self): def cancel(self):
# Can't go back from here! # Can't go back from here!

View File

@ -47,10 +47,10 @@ class SnapListController(SubiquityTuiController):
def done(self, selections: List[SnapSelection]): def done(self, selections: List[SnapSelection]):
log.debug("SnapListController.done next_screen snaps_to_install=%s", selections) log.debug("SnapListController.done next_screen snaps_to_install=%s", selections)
self.app.next_screen(self.endpoint.POST(selections)) self.app.request_next_screen(self.endpoint.POST(selections))
def cancel(self, sender=None): def cancel(self, sender=None):
self.app.prev_screen() self.app.request_prev_screen()
async def get_list_wait(self): async def get_list_wait(self):
return await self.endpoint.GET(wait=True) return await self.endpoint.GET(wait=True)

View File

@ -43,7 +43,7 @@ class SourceController(SubiquityTuiController):
form._click_done(None) form._click_done(None)
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()
def done(self, source_id, search_drivers: bool): def done(self, source_id, search_drivers: bool):
log.debug( log.debug(
@ -51,4 +51,4 @@ class SourceController(SubiquityTuiController):
source_id, source_id,
search_drivers, search_drivers,
) )
self.app.next_screen(self.endpoint.POST(source_id, search_drivers)) self.app.request_next_screen(self.endpoint.POST(source_id, search_drivers))

View File

@ -84,7 +84,7 @@ class SSHController(SubiquityTuiController):
form._click_done(None) form._click_done(None)
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()
def _fetch_cancel(self): def _fetch_cancel(self):
if self._fetch_task is None: if self._fetch_task is None:
@ -123,4 +123,4 @@ class SSHController(SubiquityTuiController):
def done(self, result): def done(self, result):
log.debug("SSHController.done next_screen result=%s", result) log.debug("SSHController.done next_screen result=%s", result)
self.app.next_screen(self.endpoint.POST(result)) self.app.request_next_screen(self.endpoint.POST(result))

View File

@ -216,13 +216,13 @@ class UbuntuProController(SubiquityTuiController):
schedule_task(inner()) schedule_task(inner())
def cancel(self) -> None: def cancel(self) -> None:
self.app.prev_screen() self.app.request_prev_screen()
def done(self, token: str) -> None: def done(self, token: str) -> None:
"""Submit the token and move on to the next screen.""" """Submit the token and move on to the next screen."""
self.app.next_screen(self.endpoint.POST(UbuntuProInfo(token=token))) self.app.request_next_screen(self.endpoint.POST(UbuntuProInfo(token=token)))
def next_screen(self) -> None: def next_screen(self) -> None:
"""Move on to the next screen. Assume the token should not be """Move on to the next screen. Assume the token should not be
submitted (or has already been submitted).""" submitted (or has already been submitted)."""
self.app.next_screen() self.app.request_next_screen()

View File

@ -48,10 +48,10 @@ class WelcomeController(SubiquityTuiController):
log.debug("WelcomeController.done %s next_screen", code) log.debug("WelcomeController.done %s next_screen", code)
i18n.switch_language(code) i18n.switch_language(code)
self.app.native_language = display_name self.app.native_language = display_name
self.app.next_screen(self.endpoint.POST(code)) self.app.request_next_screen(self.endpoint.POST(code))
def cancel(self, sender=None): def cancel(self, sender=None):
if not self.serial: if not self.serial:
# Can't go back from here unless we're on serial! # Can't go back from here unless we're on serial!
pass pass
self.app.prev_screen() self.app.request_prev_screen()

View File

@ -33,11 +33,11 @@ class ZdevController(SubiquityTuiController):
self.done() self.done()
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()
def done(self): def done(self):
# switch to next screen # switch to next screen
self.app.next_screen() self.app.request_next_screen()
async def chzdev(self, action, zdevinfo): async def chzdev(self, action, zdevinfo):
return await self.endpoint.chzdev.POST(action, zdevinfo) return await self.endpoint.chzdev.POST(action, zdevinfo)

View File

@ -296,7 +296,7 @@ class InstallConfirmation(Stretchy):
if self.app.controllers.Progress.showing: if self.app.controllers.Progress.showing:
run_bg_task(self.app.confirm_install()) run_bg_task(self.app.confirm_install())
else: else:
self.app.next_screen(self.app.confirm_install()) self.app.request_next_screen(self.app.confirm_install())
def cancel(self, sender): def cancel(self, sender):
self.app.remove_global_overlay(self) self.app.remove_global_overlay(self)

View File

@ -663,7 +663,7 @@ class NetworkController(BaseNetworkController, TuiController, NetworkAnswersMixi
def done(self): def done(self):
log.debug("NetworkController.done next_screen") log.debug("NetworkController.done next_screen")
self.model.has_network = self.network_event_receiver.has_default_route self.model.has_network = self.network_event_receiver.has_default_route
self.app.next_screen() self.app.request_next_screen()
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.prev_screen()

View File

@ -41,8 +41,8 @@ def make_app(model=None):
app.context = Context.new(app) app.context = Context.new(app)
app.exit = mock.Mock() app.exit = mock.Mock()
app.respond = mock.Mock() app.respond = mock.Mock()
app.next_screen = mock.Mock() app.request_next_screen = mock.Mock()
app.prev_screen = mock.Mock() app.request_prev_screen = mock.Mock()
app.hub = MessageHub() app.hub = MessageHub()
app.opts = mock.Mock() app.opts = mock.Mock()
app.opts.dry_run = True app.opts.dry_run = True

View File

@ -156,7 +156,7 @@ class TuiApplication(Application):
self.ui.block_input = False self.ui.block_input = False
nonlocal min_show_task nonlocal min_show_task
min_show_task = asyncio.create_task(asyncio.sleep(MIN_SHOW_PROGRESS_TIME)) min_show_task = asyncio.create_task(asyncio.sleep(MIN_SHOW_PROGRESS_TIME))
show() await show()
self.ui.block_input = True self.ui.block_input = True
show_task = asyncio.create_task(_show()) show_task = asyncio.create_task(_show())
@ -166,7 +166,7 @@ class TuiApplication(Application):
if min_show_task: if min_show_task:
await min_show_task await min_show_task
if hide is not None: if hide is not None:
hide() await hide()
else: else:
self.ui.block_input = False self.ui.block_input = False
show_task.cancel() show_task.cancel()
@ -191,18 +191,24 @@ class TuiApplication(Application):
else: else:
task_to_cancel = None task_to_cancel = None
def show_load(): async def show_load():
nonlocal ld nonlocal ld
ld = LoadingDialog(self.ui.body, message, task_to_cancel) ld = LoadingDialog(self.ui.body, message, task_to_cancel)
self.ui.body.show_overlay(ld, width=ld.width) self.ui.body.show_overlay(ld, width=ld.width)
await self.redraw_screen()
def hide_load(): async def hide_load():
ld.close() ld.close()
await self.redraw_screen()
return await self._wait_with_indication(awaitable, show_load, hide_load) return await self._wait_with_indication(awaitable, show_load, hide_load)
async def wait_with_progress(self, awaitable): async def wait_with_progress(self, awaitable):
return await self._wait_with_indication(awaitable, self.show_progress) async def show_progress():
self.show_progress()
await self.redraw_screen()
return await self._wait_with_indication(awaitable, show_progress)
async def _move_screen( async def _move_screen(
self, increment, coro self, increment, coro
@ -243,14 +249,36 @@ class TuiApplication(Application):
view = view_or_callable view = view_or_callable
self.ui.set_body(view) self.ui.set_body(view)
def next_screen(self, coro=None): async def redraw_screen(self):
run_bg_task(self.move_screen(1, coro)) self.urwid_loop.draw_screen()
def prev_screen(self): async def next_screen(self, coro=None):
run_bg_task(self.move_screen(-1, None)) await self.move_screen(1, coro)
def select_initial_screen(self): async def prev_screen(self):
self.next_screen() await self.move_screen(-1, None)
async def select_initial_screen(self):
await self.next_screen()
def request_next_screen(self, coro=None, *, redraw=True):
async def next_screen():
await self.next_screen(coro)
if redraw:
await self.redraw_screen()
run_bg_task(next_screen())
def request_prev_screen(self, *, redraw=True):
async def prev_screen():
await self.prev_screen()
if redraw:
await self.redraw_screen()
run_bg_task(prev_screen())
def request_screen_redraw(self):
run_bg_task(self.redraw_screen())
def set_rich(self, rich): def set_rich(self, rich):
if rich == self.rich_mode: if rich == self.rich_mode:
@ -321,7 +349,7 @@ class TuiApplication(Application):
# By default, basic on serial - rich otherwise. # By default, basic on serial - rich otherwise.
return not self.opts.run_on_serial return not self.opts.run_on_serial
def start_urwid(self, input=None, output=None): async def start_urwid(self, input=None, output=None):
# This stops the tcsetpgrp call in run_command_in_foreground from # This stops the tcsetpgrp call in run_command_in_foreground from
# suspending us. See the rant there for more details. # suspending us. See the rant there for more details.
signal.signal(signal.SIGTTOU, signal.SIG_IGN) signal.signal(signal.SIGTTOU, signal.SIG_IGN)
@ -339,12 +367,13 @@ class TuiApplication(Application):
extend_dec_special_charmap() extend_dec_special_charmap()
self.set_rich(self.get_initial_rich_mode()) self.set_rich(self.get_initial_rich_mode())
self.urwid_loop.start() self.urwid_loop.start()
self.select_initial_screen() await self.select_initial_screen()
await self.redraw_screen()
async def start(self, start_urwid=True): async def start(self, start_urwid=True):
await super().start() await super().start()
if start_urwid: if start_urwid:
self.start_urwid() await self.start_urwid()
async def run(self): async def run(self):
try: try:

View File

@ -47,7 +47,7 @@ class WSLConfigurationAdvancedController(SubiquityTuiController):
"WSLConfigurationAdvancedController.done next_screen user_spec=%s", "WSLConfigurationAdvancedController.done next_screen user_spec=%s",
reconf_data, reconf_data,
) )
self.app.next_screen(self.endpoint.POST(reconf_data)) self.app.request_next_screen(self.endpoint.POST(reconf_data))
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()

View File

@ -32,7 +32,7 @@ class WSLConfigurationBaseController(SubiquityTuiController):
"WSLConfigurationBaseController.done next_screen user_spec=%s", "WSLConfigurationBaseController.done next_screen user_spec=%s",
configuration_data, configuration_data,
) )
self.app.next_screen(self.endpoint.POST(configuration_data)) self.app.request_next_screen(self.endpoint.POST(configuration_data))
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()

View File

@ -41,7 +41,7 @@ class WSLSetupOptionsController(SubiquityTuiController):
"WSLSetupOptionsController.done next_screen user_spec=%s", "WSLSetupOptionsController.done next_screen user_spec=%s",
configuration_data, configuration_data,
) )
self.app.next_screen(self.endpoint.POST(configuration_data)) self.app.request_next_screen(self.endpoint.POST(configuration_data))
def cancel(self): def cancel(self):
self.app.prev_screen() self.app.request_prev_screen()