diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py index 97ae98082a..3bdacf8734 100644 --- a/selfdrive/ui/layouts/settings/device.py +++ b/selfdrive/ui/layouts/settings/device.py @@ -1,7 +1,12 @@ -from openpilot.system.ui.lib.application import Widget +import os +import json +from openpilot.system.ui.lib.application import gui_app, Widget from openpilot.system.ui.lib.list_view import ListView, text_item, button_item from openpilot.common.params import Params +from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog from openpilot.system.hardware import TICI +from openpilot.common.basedir import BASEDIR + # Description constants DESCRIPTIONS = { @@ -18,9 +23,10 @@ DESCRIPTIONS = { class DeviceLayout(Widget): def __init__(self): super().__init__() - params = Params() - dongle_id = params.get("DongleId", encoding="utf-8") or "N/A" - serial = params.get("HardwareSerial") or "N/A" + + self._params = Params() + dongle_id = self._params.get("DongleId", encoding="utf-8") or "N/A" + serial = self._params.get("HardwareSerial") or "N/A" items = [ text_item("Dongle ID", dongle_id), @@ -37,13 +43,32 @@ class DeviceLayout(Widget): items.append(button_item("Change Language", "CHANGE", callback=self._on_change_language)) self._list_widget = ListView(items) + self._select_language_dialog: MultiOptionDialog | None = None def _render(self, rect): self._list_widget.render(rect) + def _on_change_language(self): + try: + languages_file = os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json") + with open(languages_file, encoding='utf-8') as f: + languages = json.load(f) + + self._select_language_dialog = MultiOptionDialog("Select a language", languages) + gui_app.set_modal_overlay(self._select_language_dialog, callback=self._on_select_lang_dialog_closed) + except FileNotFoundError: + pass + + def _on_select_lang_dialog_closed(self, result: int): + if result == 1 and self._select_language_dialog: + selected_language = self._select_language_dialog.selection + self._params.put("LanguageSetting", selected_language) + + self._select_language_dialog = None + + def _on_pair_device(self): pass def _on_driver_camera(self): pass def _on_reset_calibration(self): pass def _on_review_training_guide(self): pass def _on_regulatory(self): pass - def _on_change_language(self): pass diff --git a/system/ui/widgets/option_dialog.py b/system/ui/widgets/option_dialog.py index a335ffc547..22bdc6ae6f 100644 --- a/system/ui/widgets/option_dialog.py +++ b/system/ui/widgets/option_dialog.py @@ -1,83 +1,70 @@ import pyray as rl - -from openpilot.system.ui.lib.application import Widget +from openpilot.system.ui.lib.application import Widget, FontWeight from openpilot.system.ui.lib.button import gui_button, ButtonStyle, TextAlignment from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +# Constants +MARGIN = 50 +TITLE_FONT_SIZE = 70 +ITEM_HEIGHT = 135 +BUTTON_SPACING = 50 +BUTTON_HEIGHT = 160 +ITEM_SPACING = 50 +LIST_ITEM_SPACING = 25 + class MultiOptionDialog(Widget): def __init__(self, title, options, current=""): super().__init__() - self._title = title - self._options = options - self._current = current if current in options else "" - self._selection = self._current - self._option_height = 80 - self._padding = 20 - self.scroll_panel = GuiScrollPanel() - - @property - def selection(self): - return self._selection + self.title = title + self.options = options + self.current = current + self.selection = current + self.scroll = GuiScrollPanel() def _render(self, rect): - title_rect = rl.Rectangle(rect.x + self._padding, rect.y + self._padding, rect.width - 2 * self._padding, 70) - gui_label(title_rect, self._title, 70) - - options_y_start = rect.y + 120 - options_height = len(self._options) * (self._option_height + 10) - options_rect = rl.Rectangle(rect.x + self._padding, options_y_start, rect.width - 2 * self._padding, options_height) - - view_rect = rl.Rectangle( - rect.x + self._padding, options_y_start, rect.width - 2 * self._padding, rect.height - 200 - 2 * self._padding - ) + dialog_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - 2 * MARGIN, rect.height - 2 * MARGIN) + rl.draw_rectangle_rounded(dialog_rect, 0.02, 20, rl.Color(30, 30, 30, 255)) - offset = self.scroll_panel.handle_scroll(view_rect, options_rect) - is_click_valid = self.scroll_panel.is_click_valid() + content_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y + MARGIN, + dialog_rect.width - 2 * MARGIN, dialog_rect.height - 2 * MARGIN) - rl.begin_scissor_mode(int(view_rect.x), int(view_rect.y), int(view_rect.width), int(view_rect.height)) + gui_label(rl.Rectangle(content_rect.x, content_rect.y, content_rect.width, TITLE_FONT_SIZE), self.title, 70, font_weight=FontWeight.BOLD) - for i, option in enumerate(self._options): - y_pos = view_rect.y + i * (self._option_height + 10) + offset.y - item_rect = rl.Rectangle(view_rect.x, y_pos, view_rect.width, self._option_height) + # Options area + options_y = content_rect.y + TITLE_FONT_SIZE + ITEM_SPACING + options_h = content_rect.height - TITLE_FONT_SIZE - BUTTON_HEIGHT - 2 * ITEM_SPACING + view_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, options_h) + content_h = len(self.options) * (ITEM_HEIGHT + 10) + list_content_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, content_h) - if not rl.check_collision_recs(item_rect, view_rect): - continue + # Scroll and render options + offset = self.scroll.handle_scroll(view_rect, list_content_rect) + valid_click = self.scroll.is_click_valid() - is_selected = option == self._selection - button_style = ButtonStyle.PRIMARY if is_selected else ButtonStyle.NORMAL + rl.begin_scissor_mode(int(view_rect.x), int(options_y), int(view_rect.width), int(options_h)) + for i, option in enumerate(self.options): + item_y = options_y + i * (ITEM_HEIGHT + LIST_ITEM_SPACING) + offset.y + item_rect = rl.Rectangle(view_rect.x, item_y, view_rect.width, ITEM_HEIGHT) - if gui_button(item_rect, option, button_style=button_style, text_alignment=TextAlignment.LEFT) and is_click_valid: - self._selection = option + if rl.check_collision_recs(item_rect, view_rect): + selected = option == self.selection + style = ButtonStyle.PRIMARY if selected else ButtonStyle.NORMAL + if gui_button(item_rect, option, button_style=style, text_alignment=TextAlignment.LEFT) and valid_click: + self.selection = option rl.end_scissor_mode() - button_y = rect.y + rect.height - 80 - self._padding - button_width = (rect.width - 3 * self._padding) / 2 - - cancel_rect = rl.Rectangle(rect.x + self._padding, button_y, button_width, 80) - if gui_button(cancel_rect, "Cancel"): - return 0 # Canceled - - select_rect = rl.Rectangle(rect.x + 2 * self._padding + button_width, button_y, button_width, 80) - has_new_selection = self._selection != "" and self._selection != self._current - - if gui_button(select_rect, "Select", is_enabled=has_new_selection, button_style=ButtonStyle.PRIMARY): - return 1 # Selected - - return -1 # Still active - + # Buttons + button_y = content_rect.y + content_rect.height - BUTTON_HEIGHT + button_w = (content_rect.width - BUTTON_SPACING) / 2 -if __name__ == "__main__": - from openpilot.system.ui.lib.application import gui_app + if gui_button(rl.Rectangle(content_rect.x, button_y, button_w, BUTTON_HEIGHT), "Cancel"): + return 0 - gui_app.init_window("Multi Option Dialog Example") - options = [f"Option {i}" for i in range(1, 11)] - dialog = MultiOptionDialog("Choose an option", options, options[0]) + if gui_button(rl.Rectangle(content_rect.x + button_w + BUTTON_SPACING, button_y, button_w, BUTTON_HEIGHT), + "Select", is_enabled=self.selection != self.current, button_style=ButtonStyle.PRIMARY): + return 1 - for _ in gui_app.render(): - result = dialog.render(rl.Rectangle(100, 100, 1024, 800)) - if isinstance(result, int) and result >= 0: - print(f"Selected: {dialog.selection}" if result > 0 else "Canceled") - break + return -1