ui: add navigation stack for tici (#37275)

* initial

* start to support nav stack in settings panels + fix some navwidget bugs

* add deprecation warning and move more to new nav stack

* fix overriding NavWidget enabled and do developer panel

* fix interactive timeout and do main

* more device, not done yet

* minor network fixes

* dcam dialog

* start onboarding

* fix onboarding

* do mici setup

* remove now useless CUSTOM_SOFTWARE

* support big ui with old modal overlay

* reset can be old modal overlay, but updater needs new since it uses wifiui

* flip name truthiness to inspire excitement

* all *should* work, but will do pass later

* clean up main

* clean up settiings

* clean up dialog and developer

* cleanup mici setup some

* rm one more

* fix keyboard

* revert

* might as well but clarify

* fix networkinfopage buttons

* lint

* nice clean up from cursor

* animate background fade with position

* fix device overlays

* cursor fix pt1

cursor fix pt2

* rm print

* capital

* temp fix from cursor for onboarding not freeing space after reviewing training guide

* fix home screen scroller snap not resetting

* stash

* nice gradient on top

* 40

* 20

* no gradient

* return unused returns and always show regulatory btn

* nice!

* revert selfdrive/ui

* let's do tici first

* bring back ui

* not sure why __del__, SetupWidget was never deleted?

* device "done"

* network "done!!"

* toggles "done"

* software "done"

* developer "done"

* fix onboarding

* use new modal for debug windows

* and aug

* setup "done"

* clean up

* updater "done"

* reset "done"

* pop first before callbacks in case callbacks push

* fix cmt

* not needed

* remove two commented functions for mici

* clean up application

* typing

* static

* not sure what this means

* fix big

* more static

* actually great catch

* fix cmt
This commit is contained in:
Shane Smiskol
2026-02-20 02:43:11 -08:00
committed by GitHub
parent f829c90de6
commit cefddf4b9b
22 changed files with 231 additions and 188 deletions
+50 -8
View File
@@ -230,6 +230,10 @@ class GuiApplication:
self._modal_overlay_shown = False
self._modal_overlay_tick: Callable[[], None] | None = None
# TODO: move over the entire ui and deprecate
self._new_modal = False
self._nav_stack: list[object] = []
self._mouse = MouseState(self._scale)
self._mouse_events: list[MouseEvent] = []
self._last_mouse_event: MouseEvent = MouseEvent(MousePos(0, 0), 0, False, False, False, 0.0)
@@ -262,7 +266,7 @@ class GuiApplication:
def request_close(self):
self._window_close_requested = True
def init_window(self, title: str, fps: int = _DEFAULT_FPS):
def init_window(self, title: str, fps: int = _DEFAULT_FPS, new_modal: bool = False):
with self._startup_profile_context():
def _close(sig, frame):
self.close()
@@ -270,6 +274,8 @@ class GuiApplication:
signal.signal(signal.SIGINT, _close)
atexit.register(self.close)
self._new_modal = new_modal
flags = rl.ConfigFlags.FLAG_MSAA_4X_HINT
if ENABLE_VSYNC:
flags |= rl.ConfigFlags.FLAG_VSYNC_HINT
@@ -373,7 +379,34 @@ class GuiApplication:
except Exception:
break
def push_widget(self, widget: object):
assert self._new_modal
# disable previous widget to prevent input processing
if len(self._nav_stack) > 0:
prev_widget = self._nav_stack[-1]
prev_widget.set_enabled(False)
self._nav_stack.append(widget)
widget.show_event()
def pop_widget(self):
assert self._new_modal
if len(self._nav_stack) < 2:
cloudlog.warning("At least one widget should remain on the stack, ignoring pop")
return
# re-enable previous widget and pop current
prev_widget = self._nav_stack[-2]
prev_widget.set_enabled(True)
widget = self._nav_stack.pop()
widget.hide_event()
def set_modal_overlay(self, overlay, callback: Callable | None = None):
assert not self._new_modal, "set_modal_overlay is deprecated, use push_widget instead"
if self._modal_overlay.overlay is not None:
if hasattr(self._modal_overlay.overlay, 'hide_event'):
self._modal_overlay.overlay.hide_event()
@@ -528,15 +561,24 @@ class GuiApplication:
rl.begin_drawing()
rl.clear_background(rl.BLACK)
# Handle modal overlay rendering and input processing
if self._handle_modal_overlay():
# Allow a Widget to still run a function while overlay is shown
if self._modal_overlay_tick is not None:
self._modal_overlay_tick()
yield False
else:
if self._new_modal:
# Only render last widget
for widget in self._nav_stack[-1:]:
widget.render(rl.Rectangle(0, 0, self.width, self.height))
# Yield to allow caller to run non-rendering related code
yield True
else:
# Handle modal overlay rendering and input processing
if self._handle_modal_overlay():
# Allow a Widget to still run a function while overlay is shown
if self._modal_overlay_tick is not None:
self._modal_overlay_tick()
yield False
else:
yield True
if self._render_texture:
rl.end_texture_mode()
rl.begin_drawing()
+17 -21
View File
@@ -36,13 +36,9 @@ class Reset(Widget):
self._mode = mode
self._previous_reset_state = None
self._reset_state = ResetState.NONE
self._cancel_button = Button("Cancel", self._cancel_callback)
self._cancel_button = Button("Cancel", gui_app.request_close)
self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY)
self._reboot_button = Button("Reboot", lambda: os.system("sudo reboot"))
self._render_status = True
def _cancel_callback(self):
self._render_status = False
def _do_erase(self):
if PC:
@@ -69,30 +65,30 @@ class Reset(Widget):
elif self._reset_state != ResetState.RESETTING and (time.monotonic() - self._timeout_st) > TIMEOUT:
exit(0)
def _render(self, rect: rl.Rectangle):
label_rect = rl.Rectangle(rect.x + 140, rect.y, rect.width - 280, 100 * FONT_SCALE)
def _render(self, _):
content_rect = rl.Rectangle(45, 200, self._rect.width - 90, self._rect.height - 245)
label_rect = rl.Rectangle(content_rect.x + 140, content_rect.y, content_rect.width - 280, 100 * FONT_SCALE)
gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD)
text_rect = rl.Rectangle(rect.x + 140, rect.y + 140, rect.width - 280, rect.height - 90 - 100 * FONT_SCALE)
text_rect = rl.Rectangle(content_rect.x + 140, content_rect.y + 140, content_rect.width - 280, content_rect.height - 90 - 100 * FONT_SCALE)
gui_text_box(text_rect, self._get_body_text(), 90)
button_height = 160
button_spacing = 50
button_top = rect.y + rect.height - button_height
button_width = (rect.width - button_spacing) / 2.0
button_top = content_rect.y + content_rect.height - button_height
button_width = (content_rect.width - button_spacing) / 2.0
if self._reset_state != ResetState.RESETTING:
if self._mode == ResetMode.RECOVER:
self._reboot_button.render(rl.Rectangle(rect.x, button_top, button_width, button_height))
self._reboot_button.render(rl.Rectangle(content_rect.x, button_top, button_width, button_height))
elif self._mode == ResetMode.USER_RESET:
self._cancel_button.render(rl.Rectangle(rect.x, button_top, button_width, button_height))
self._cancel_button.render(rl.Rectangle(content_rect.x, button_top, button_width, button_height))
if self._reset_state != ResetState.FAILED:
self._confirm_button.render(rl.Rectangle(rect.x + button_width + 50, button_top, button_width, button_height))
self._confirm_button.render(rl.Rectangle(content_rect.x + button_width + 50, button_top, button_width, button_height))
else:
self._reboot_button.render(rl.Rectangle(rect.x, button_top, rect.width, button_height))
return self._render_status
self._reboot_button.render(rl.Rectangle(content_rect.x, button_top, content_rect.width, button_height))
def _confirm(self):
if self._reset_state == ResetState.CONFIRM:
@@ -120,16 +116,16 @@ def main():
elif sys.argv[1] == "--format":
mode = ResetMode.FORMAT
gui_app.init_window("System Reset", 20)
gui_app.init_window("System Reset", 20, new_modal=True)
reset = Reset(mode)
if mode == ResetMode.FORMAT:
reset.start_reset()
for should_render in gui_app.render():
if should_render:
if not reset.render(rl.Rectangle(45, 200, gui_app.width - 90, gui_app.height - 245)):
break
gui_app.push_widget(reset)
for _ in gui_app.render():
pass
if __name__ == "__main__":
+9 -8
View File
@@ -16,7 +16,7 @@ from openpilot.common.utils import run_cmd
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.label import Label
@@ -327,19 +327,20 @@ class Setup(Widget):
def render_custom_software(self):
def handle_keyboard_result(result):
# Enter pressed
if result == 1:
if result == DialogResult.CONFIRM:
url = self.keyboard.text
self.keyboard.clear()
if url:
self.download(url)
# Cancel pressed
elif result == 0:
elif result == DialogResult.CANCEL:
self.state = SetupState.SOFTWARE_SELECTION
self.keyboard.reset(min_text_size=1)
self.keyboard.set_title("Enter URL", "for Custom Software")
gui_app.set_modal_overlay(self.keyboard, callback=handle_keyboard_result)
self.keyboard.set_callback(handle_keyboard_result)
gui_app.push_widget(self.keyboard)
def use_openpilot(self):
if os.path.isdir(INSTALL_PATH) and os.path.isfile(VALID_CACHE_PATH):
@@ -435,11 +436,11 @@ class Setup(Widget):
def main():
try:
gui_app.init_window("Setup", 20)
gui_app.init_window("Setup", 20, new_modal=True)
setup = Setup()
for should_render in gui_app.render():
if should_render:
setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
gui_app.push_widget(setup)
for _ in gui_app.render():
pass
setup.close()
except Exception as e:
print(f"Setup error: {e}")
+4 -5
View File
@@ -160,11 +160,10 @@ def main():
manifest_path = sys.argv[2]
try:
gui_app.init_window("System Update")
updater = Updater(updater_path, manifest_path)
for should_render in gui_app.render():
if should_render:
updater.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
gui_app.init_window("System Update", new_modal=True)
gui_app.push_widget(Updater(updater_path, manifest_path))
for _ in gui_app.render():
pass
finally:
# Make sure we clean up even if there's an error
gui_app.close()
+11 -11
View File
@@ -1,4 +1,5 @@
import pyray as rl
from collections.abc import Callable
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult
@@ -17,7 +18,7 @@ BACKGROUND_COLOR = rl.Color(27, 27, 27, 255)
class ConfirmDialog(Widget):
def __init__(self, text: str, confirm_text: str, cancel_text: str | None = None, rich: bool = False):
def __init__(self, text: str, confirm_text: str, cancel_text: str | None = None, rich: bool = False, callback: Callable[[DialogResult], None] | None = None):
super().__init__()
if cancel_text is None:
cancel_text = tr("Cancel")
@@ -26,7 +27,7 @@ class ConfirmDialog(Widget):
self._cancel_button = Button(cancel_text, self._cancel_button_callback)
self._confirm_button = Button(confirm_text, self._confirm_button_callback, button_style=ButtonStyle.PRIMARY)
self._rich = rich
self._dialog_result = DialogResult.NO_ACTION
self._callback = callback
self._cancel_text = cancel_text
self._scroller = Scroller([self._html_renderer], line_separator=False, spacing=0)
@@ -36,14 +37,15 @@ class ConfirmDialog(Widget):
else:
self._html_renderer.parse_html_content(text)
def reset(self):
self._dialog_result = DialogResult.NO_ACTION
def _cancel_button_callback(self):
self._dialog_result = DialogResult.CANCEL
gui_app.pop_widget()
if self._callback:
self._callback(DialogResult.CANCEL)
def _confirm_button_callback(self):
self._dialog_result = DialogResult.CONFIRM
gui_app.pop_widget()
if self._callback:
self._callback(DialogResult.CONFIRM)
def _render(self, rect: rl.Rectangle):
dialog_x = OUTER_MARGIN if not self._rich else RICH_OUTER_MARGIN
@@ -73,9 +75,9 @@ class ConfirmDialog(Widget):
self._scroller.render(text_rect)
if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):
self._dialog_result = DialogResult.CONFIRM
self._confirm_button_callback()
elif rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
self._dialog_result = DialogResult.CANCEL
self._cancel_button_callback()
if self._cancel_text:
self._confirm_button.render(confirm_button)
@@ -85,8 +87,6 @@ class ConfirmDialog(Widget):
full_confirm_button = rl.Rectangle(dialog_rect.x + MARGIN, button_y, full_button_width, BUTTON_HEIGHT)
self._confirm_button.render(full_confirm_button)
return self._dialog_result
def alert_dialog(message: str, button_text: str | None = None):
if button_text is None:
+1 -1
View File
@@ -260,7 +260,7 @@ class HtmlModal(Widget):
super().__init__()
self._content = HtmlRenderer(file_path=file_path, text=text)
self._scroll_panel = GuiScrollPanel()
self._ok_button = Button(tr("OK"), click_callback=lambda: gui_app.set_modal_overlay(None), button_style=ButtonStyle.PRIMARY)
self._ok_button = Button(tr("OK"), click_callback=gui_app.pop_widget, button_style=ButtonStyle.PRIMARY)
def _render(self, rect: rl.Rectangle):
margin = 50
+26 -17
View File
@@ -1,12 +1,13 @@
from functools import partial
import time
from typing import Literal
from collections.abc import Callable
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.inputbox import InputBox
from openpilot.system.ui.widgets.label import Label
@@ -58,7 +59,8 @@ KEYBOARD_LAYOUTS = {
class Keyboard(Widget):
def __init__(self, max_text_size: int = 255, min_text_size: int = 0, password_mode: bool = False, show_password_toggle: bool = False):
def __init__(self, max_text_size: int = 255, min_text_size: int = 0, password_mode: bool = False, show_password_toggle: bool = False,
callback: Callable[[DialogResult], None] | None = None):
super().__init__()
self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase"
self._caps_lock = False
@@ -71,13 +73,13 @@ class Keyboard(Widget):
self._input_box = InputBox(max_text_size)
self._password_mode = password_mode
self._show_password_toggle = show_password_toggle
self._callback = callback
# Backspace key repeat tracking
self._backspace_pressed: bool = False
self._backspace_press_time: float = 0.0
self._backspace_last_repeat: float = 0.0
self._render_return_status = -1
self._cancel_button = Button(lambda: tr("Cancel"), self._cancel_button_callback)
self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT)
@@ -122,16 +124,23 @@ class Keyboard(Widget):
self._title.set_text(title)
self._sub_title.set_text(sub_title)
def set_callback(self, callback: Callable[[DialogResult], None] | None):
self._callback = callback
def _eye_button_callback(self):
self._password_mode = not self._password_mode
def _cancel_button_callback(self):
self.clear()
self._render_return_status = 0
gui_app.pop_widget()
if self._callback:
self._callback(DialogResult.CANCEL)
def _key_callback(self, k):
if k == ENTER_KEY:
self._render_return_status = 1
gui_app.pop_widget()
if self._callback:
self._callback(DialogResult.CONFIRM)
else:
self.handle_key_press(k)
@@ -197,8 +206,6 @@ class Keyboard(Widget):
self._all_keys[key].set_enabled(is_enabled)
self._all_keys[key].render(key_rect)
return self._render_return_status
def _render_input_area(self, input_rect: rl.Rectangle):
if self._show_password_toggle:
self._input_box.set_password_mode(self._password_mode)
@@ -250,7 +257,6 @@ class Keyboard(Widget):
def reset(self, min_text_size: int | None = None):
if min_text_size is not None:
self._min_text_size = min_text_size
self._render_return_status = -1
self._last_shift_press_time = 0
self._backspace_pressed = False
self._backspace_press_time = 0.0
@@ -259,15 +265,18 @@ class Keyboard(Widget):
if __name__ == "__main__":
gui_app.init_window("Keyboard")
keyboard = Keyboard(min_text_size=8, show_password_toggle=True)
for _ in gui_app.render():
keyboard.set_title("Keyboard Input", "Type your text below")
result = keyboard.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
if result == 1:
def callback(result: DialogResult):
if result == DialogResult.CONFIRM:
print(f"You typed: {keyboard.text}")
gui_app.request_close()
elif result == 0:
elif result == DialogResult.CANCEL:
print("Canceled")
gui_app.request_close()
gui_app.request_close()
gui_app.init_window("Keyboard", new_modal=True)
keyboard = Keyboard(min_text_size=8, show_password_toggle=True, callback=callback)
keyboard.set_title("Keyboard Input", "Type your text below")
gui_app.push_widget(keyboard)
for _ in gui_app.render():
pass
gui_app.close()
+32 -25
View File
@@ -7,7 +7,7 @@ from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType, normalize_ssid
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.keyboard import Keyboard
@@ -187,8 +187,8 @@ class AdvancedNetworkSettings(Widget):
self._wifi_manager.update_gsm_settings(roaming_state, self._params.get("GsmApn") or "", self._params.get_bool("GsmMetered"))
def _edit_apn(self):
def update_apn(result):
if result != 1:
def update_apn(result: DialogResult):
if result != DialogResult.CONFIRM:
return
apn = self._keyboard.text.strip()
@@ -203,7 +203,8 @@ class AdvancedNetworkSettings(Widget):
self._keyboard.reset(min_text_size=0)
self._keyboard.set_title(tr("Enter APN"), tr("leave blank for automatic configuration"))
self._keyboard.set_text(current_apn)
gui_app.set_modal_overlay(self._keyboard, update_apn)
self._keyboard.set_callback(update_apn)
gui_app.push_widget(self._keyboard)
def _toggle_cellular_metered(self):
metered = self._cellular_metered_action.get_state()
@@ -216,15 +217,18 @@ class AdvancedNetworkSettings(Widget):
self._wifi_manager.set_current_network_metered(metered_type)
def _connect_to_hidden_network(self):
def connect_hidden(result):
if result != 1:
def connect_hidden(result: DialogResult):
if result != DialogResult.CONFIRM:
return
ssid = self._keyboard.text
if not ssid:
return
def enter_password(result):
def enter_password(result: DialogResult):
if result != DialogResult.CONFIRM:
return
password = self._keyboard.text
if password == "":
# connect without password
@@ -235,15 +239,17 @@ class AdvancedNetworkSettings(Widget):
self._keyboard.reset(min_text_size=0)
self._keyboard.set_title(tr("Enter password"), tr("for \"{}\"").format(ssid))
gui_app.set_modal_overlay(self._keyboard, enter_password)
self._keyboard.set_callback(enter_password)
gui_app.push_widget(self._keyboard)
self._keyboard.reset(min_text_size=1)
self._keyboard.set_title(tr("Enter SSID"), "")
gui_app.set_modal_overlay(self._keyboard, connect_hidden)
self._keyboard.set_callback(connect_hidden)
gui_app.push_widget(self._keyboard)
def _edit_tethering_password(self):
def update_password(result):
if result != 1:
def update_password(result: DialogResult):
if result != DialogResult.CONFIRM:
return
password = self._keyboard.text
@@ -253,7 +259,8 @@ class AdvancedNetworkSettings(Widget):
self._keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH)
self._keyboard.set_title(tr("Enter new tethering password"), "")
self._keyboard.set_text(self._wifi_manager.tethering_password)
gui_app.set_modal_overlay(self._keyboard, update_password)
self._keyboard.set_callback(update_password)
gui_app.push_widget(self._keyboard)
def _update_state(self):
self._wifi_manager.process_callbacks()
@@ -314,29 +321,29 @@ class WifiManagerUI(Widget):
self.keyboard.set_title(tr("Wrong password") if self._password_retry else tr("Enter password"),
tr("for \"{}\"").format(normalize_ssid(self._state_network.ssid)))
self.keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH)
gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result))
self.keyboard.set_callback(lambda result: self._on_password_entered(cast(Network, self._state_network), result))
gui_app.push_widget(self.keyboard)
elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network:
confirm_dialog = ConfirmDialog("", tr("Forget"), tr("Cancel"))
confirm_dialog = ConfirmDialog("", tr("Forget"), tr("Cancel"), callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result))
confirm_dialog.set_text(tr("Forget Wi-Fi Network \"{}\"?").format(normalize_ssid(self._state_network.ssid)))
confirm_dialog.reset()
gui_app.set_modal_overlay(confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result))
gui_app.push_widget(confirm_dialog)
else:
self._draw_network_list(rect)
def _on_password_entered(self, network: Network, result: int):
if result == 1:
def _on_password_entered(self, network: Network, result: DialogResult):
if result == DialogResult.CONFIRM:
password = self.keyboard.text
self.keyboard.clear()
if len(password) >= MIN_PASSWORD_LENGTH:
self.connect_to_network(network, password)
elif result == 0:
elif result == DialogResult.CANCEL:
self.state = UIState.IDLE
def on_forgot_confirm_finished(self, network, result: int):
if result == 1:
def on_forgot_confirm_finished(self, network, result: DialogResult):
if result == DialogResult.CONFIRM:
self.forget_network(network)
elif result == 0:
elif result == DialogResult.CANCEL:
self.state = UIState.IDLE
def _draw_network_list(self, rect: rl.Rectangle):
@@ -474,11 +481,11 @@ class WifiManagerUI(Widget):
def main():
gui_app.init_window("Wi-Fi Manager")
wifi_ui = WifiManagerUI(WifiManager())
gui_app.init_window("Wi-Fi Manager", new_modal=True)
gui_app.push_widget(WifiManagerUI(WifiManager()))
for _ in gui_app.render():
wifi_ui.render(rl.Rectangle(50, 50, gui_app.width - 100, gui_app.height - 100))
pass
gui_app.close()
+7 -6
View File
@@ -1,5 +1,6 @@
import pyray as rl
from openpilot.system.ui.lib.application import FontWeight
from collections.abc import Callable
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.button import Button, ButtonStyle
@@ -17,13 +18,13 @@ LIST_ITEM_SPACING = 25
class MultiOptionDialog(Widget):
def __init__(self, title, options, current="", option_font_weight=FontWeight.MEDIUM):
def __init__(self, title, options, current="", option_font_weight=FontWeight.MEDIUM, callback: Callable[[DialogResult], None] | None = None):
super().__init__()
self.title = title
self.options = options
self.current = current
self.selection = current
self._result: DialogResult = DialogResult.NO_ACTION
self._callback = callback
# Create scroller with option buttons
self.option_buttons = [Button(option, click_callback=lambda opt=option: self._on_option_clicked(opt),
@@ -36,7 +37,9 @@ class MultiOptionDialog(Widget):
self.select_button = Button(lambda: tr("Select"), click_callback=lambda: self._set_result(DialogResult.CONFIRM), button_style=ButtonStyle.PRIMARY)
def _set_result(self, result: DialogResult):
self._result = result
gui_app.pop_widget()
if self._callback:
self._callback(result)
def _on_option_clicked(self, option):
self.selection = option
@@ -74,5 +77,3 @@ class MultiOptionDialog(Widget):
select_rect = rl.Rectangle(content_rect.x + button_w + BUTTON_SPACING, button_y, button_w, BUTTON_HEIGHT)
self.select_button.set_enabled(self.selection != self.current)
self.select_button.render(select_rect)
return self._result