mirror of
https://github.com/infiniteCable2/openpilot.git
synced 2026-08-05 08:16:03 +08:00
Merge branch 'master' of https://github.com/sunnypilot/sunnypilot into sync
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class VehicleLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import ButtonAction
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.platform_selector import PlatformSelector, LegendWidget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
|
||||
|
||||
|
||||
class VehicleLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._platform_selector = PlatformSelector(self._update_brand_settings)
|
||||
|
||||
self._vehicle_item = ListItemSP(title=self._platform_selector.text, action_item=ButtonAction(text=tr("Select")),
|
||||
callback=self._platform_selector._on_clicked)
|
||||
self._vehicle_item.title_color = self._platform_selector.color
|
||||
self._legend_widget = LegendWidget(self._platform_selector)
|
||||
|
||||
self.items = [self._vehicle_item, self._legend_widget]
|
||||
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
||||
|
||||
def _update_brand_settings(self):
|
||||
self._vehicle_item._title = self._platform_selector.text
|
||||
self._vehicle_item.title_color = self._platform_selector.color
|
||||
vehicle_text = tr("Remove") if ui_state.params.get("CarPlatformBundle") else tr("Select")
|
||||
self._vehicle_item.action_item.set_text(vehicle_text)
|
||||
|
||||
def _update_state(self):
|
||||
self._update_brand_settings()
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
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, Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
CAR_LIST_JSON_OUT = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "car", "car_list.json")
|
||||
|
||||
|
||||
class LegendWidget(Widget):
|
||||
def __init__(self, platform_selector):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 0, 350))
|
||||
self._platform_selector = platform_selector
|
||||
self._font = gui_app.font(FontWeight.NORMAL)
|
||||
self._bold_font = gui_app.font(FontWeight.BOLD)
|
||||
|
||||
def _render(self, rect):
|
||||
x = rect.x + 20
|
||||
y = rect.y + 20
|
||||
rl.draw_text_ex(self._font, tr("Select vehicle to force fingerprint manually."), rl.Vector2(x, y), 40, 0, style.ITEM_DESC_TEXT_COLOR)
|
||||
y += 80
|
||||
rl.draw_text_ex(self._font, tr("Colors represent vehicle fingerprint status:"), rl.Vector2(x, y), 40, 0, style.ITEM_DESC_TEXT_COLOR)
|
||||
y += 80
|
||||
|
||||
items = [
|
||||
(style.GREEN, tr("Fingerprinted automatically")),
|
||||
(style.BLUE, tr("Manually selected fingerprint")),
|
||||
(style.YELLOW, tr("Not fingerprinted or manually selected")),
|
||||
]
|
||||
for color, text in items:
|
||||
p_color = self._platform_selector.color
|
||||
is_active = p_color.r == color.r and p_color.g == color.g and p_color.b == color.b and p_color.a == color.a
|
||||
rl.draw_rectangle(int(x), int(y + 5), 30, 30, color)
|
||||
font = self._bold_font if is_active else self._font
|
||||
text_color = rl.WHITE if is_active else style.ITEM_DESC_TEXT_COLOR
|
||||
rl.draw_text_ex(font, f"- {text}", rl.Vector2(x + 50, y - 7), 40, 0, text_color)
|
||||
y += 50
|
||||
|
||||
|
||||
class PlatformSelector(Button):
|
||||
def __init__(self, on_platform_change: Callable[[], None] | None = None):
|
||||
super().__init__(tr("Vehicle"), self._on_clicked, button_style=ButtonStyle.NORMAL)
|
||||
self.set_rect(rl.Rectangle(0, 0, 0, 120))
|
||||
|
||||
with open(CAR_LIST_JSON_OUT) as car_list_json:
|
||||
self._platforms = json.load(car_list_json)
|
||||
|
||||
self._on_platform_change = on_platform_change
|
||||
self.refresh()
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._label._text
|
||||
|
||||
def set_parent_rect(self, parent_rect):
|
||||
super().set_parent_rect(parent_rect)
|
||||
self._rect.width = parent_rect.width
|
||||
|
||||
def _on_clicked(self):
|
||||
if ui_state.params.get("CarPlatformBundle"):
|
||||
ui_state.params.remove("CarPlatformBundle")
|
||||
self.refresh()
|
||||
if self._on_platform_change:
|
||||
self._on_platform_change()
|
||||
else:
|
||||
self._show_platform_dialog()
|
||||
|
||||
def _set_platform(self, platform_name):
|
||||
if data := self._platforms.get(platform_name):
|
||||
ui_state.params.put("CarPlatformBundle", {**data, "name": platform_name})
|
||||
self.refresh()
|
||||
if self._on_platform_change:
|
||||
self._on_platform_change()
|
||||
|
||||
def _on_platform_selected(self, dialog, res):
|
||||
if res == DialogResult.CONFIRM and dialog.selection_ref:
|
||||
offroad_msg = tr("This setting will take effect immediately.") if ui_state.is_offroad else \
|
||||
tr("This setting will take effect once the device enters offroad state.")
|
||||
|
||||
confirm_dialog = ConfirmDialog(offroad_msg, tr("Confirm"))
|
||||
|
||||
callback = partial(self._confirm_platform, dialog.selection_ref)
|
||||
gui_app.set_modal_overlay(confirm_dialog, callback=callback)
|
||||
|
||||
def _confirm_platform(self, platform_name, res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._set_platform(platform_name)
|
||||
|
||||
def _show_platform_dialog(self):
|
||||
platforms = sorted(self._platforms.keys())
|
||||
makes = sorted({self._platforms[p].get('make') for p in platforms})
|
||||
folders = [TreeFolder(make, [TreeNode(p, {
|
||||
'display_name': p,
|
||||
'search_tags': f"{p} {self._platforms[p].get('make')} {' '.join(map(str, self._platforms[p].get('year', [])))} {self._platforms[p].get('model', p)}"
|
||||
}) for p in platforms if self._platforms[p].get('make') == make]) for make in makes]
|
||||
dialog = TreeOptionDialog(
|
||||
tr("Select a vehicle"),
|
||||
folders,
|
||||
search_title=tr("Search your vehicle"),
|
||||
search_subtitle=tr("Enter model year (e.g., 2021) and model (Toyota Corolla):"),
|
||||
search_funcs=[lambda node: node.data.get('display_name', ''), lambda node: node.data.get('search_tags', '')]
|
||||
)
|
||||
callback = partial(self._on_platform_selected, dialog)
|
||||
dialog.on_exit = callback
|
||||
gui_app.set_modal_overlay(dialog, callback=callback)
|
||||
|
||||
def refresh(self):
|
||||
self.brand = ""
|
||||
self.color = style.YELLOW
|
||||
self._platform = tr("Unrecognized Vehicle")
|
||||
self.set_text(tr("No vehicle selected"))
|
||||
|
||||
if bundle := ui_state.params.get("CarPlatformBundle"):
|
||||
self._platform = bundle.get("name", "")
|
||||
self.brand = bundle.get("brand", "")
|
||||
self.set_text(self._platform)
|
||||
self.color = style.BLUE
|
||||
elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK":
|
||||
self._platform = ui_state.CP.carFingerprint
|
||||
self.brand = ui_state.CP.brand
|
||||
self.set_text(self._platform)
|
||||
self.color = style.GREEN
|
||||
self.set_enabled(True)
|
||||
@@ -70,5 +70,10 @@ class DefaultStyleSP(Base):
|
||||
BUTTON_NEUTRAL_GRAY = rl.Color(51, 51, 51, 255)
|
||||
BUTTON_DISABLED_BG_COLOR = rl.Color(30, 30, 30, 255) # Very Dark Grey
|
||||
|
||||
# Vehicle Description Colors
|
||||
GREEN = rl.Color(0, 241, 0, 255)
|
||||
BLUE = rl.Color(0, 134, 233, 255)
|
||||
YELLOW = rl.Color(255, 213, 0, 255)
|
||||
|
||||
|
||||
style = DefaultStyleSP
|
||||
|
||||
@@ -8,7 +8,6 @@ from collections.abc import Callable
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
|
||||
@@ -27,14 +26,16 @@ class InputDialogSP:
|
||||
|
||||
def show(self):
|
||||
self.keyboard.reset(min_text_size=self.keyboard._min_text_size)
|
||||
self.keyboard.set_title(tr(self.title), *(tr(self.sub_title),) if self.sub_title else ())
|
||||
if self.sub_title:
|
||||
self.keyboard.set_title(self.title, self.sub_title)
|
||||
else:
|
||||
self.keyboard.set_title(self.title)
|
||||
self.keyboard.set_text(self.current_text)
|
||||
|
||||
def internal_callback(result: DialogResult):
|
||||
text = self.keyboard.text if result == DialogResult.CONFIRM else ""
|
||||
if result == DialogResult.CONFIRM:
|
||||
if self.param:
|
||||
self._params.put(self.param, text)
|
||||
if result == DialogResult.CONFIRM and self.param:
|
||||
self._params.put(self.param, text)
|
||||
if self.callback:
|
||||
self.callback(result, text)
|
||||
|
||||
|
||||
@@ -80,16 +80,22 @@ class MultipleButtonActionSP(MultipleButtonAction):
|
||||
class ListItemSP(ListItem):
|
||||
def __init__(self, title: str | Callable[[], str] = "", icon: str | None = None, description: str | Callable[[], str] | None = None,
|
||||
description_visible: bool = False, callback: Callable | None = None,
|
||||
action_item: ItemAction | None = None, inline: bool = True):
|
||||
action_item: ItemAction | None = None, inline: bool = True, title_color: rl.Color = style.ITEM_TEXT_COLOR):
|
||||
ListItem.__init__(self, title, icon, description, description_visible, callback, action_item)
|
||||
self.title_color = title_color
|
||||
self.inline = inline
|
||||
if not self.inline:
|
||||
self._rect.height += style.ITEM_BASE_HEIGHT/1.75
|
||||
|
||||
def get_item_height(self, font: rl.Font, max_width: int) -> float:
|
||||
height = super().get_item_height(font, max_width)
|
||||
|
||||
if self.description_visible:
|
||||
height += style.ITEM_PADDING * 1.5
|
||||
|
||||
if not self.inline:
|
||||
height = height + style.ITEM_BASE_HEIGHT/1.75
|
||||
height += style.ITEM_BASE_HEIGHT / 1.75
|
||||
|
||||
return height
|
||||
|
||||
def show_description(self, show: bool):
|
||||
@@ -100,13 +106,18 @@ class ListItemSP(ListItem):
|
||||
return rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
if not self.inline:
|
||||
action_y = item_rect.y + self._text_size.y + style.ITEM_PADDING * 3
|
||||
has_description = bool(self.description) and self.description_visible
|
||||
|
||||
if has_description:
|
||||
action_y = item_rect.y + self._text_size.y + style.ITEM_PADDING * 3
|
||||
else:
|
||||
action_y = item_rect.y + item_rect.height - style.BUTTON_HEIGHT - style.ITEM_PADDING * 1.5
|
||||
|
||||
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, action_y, item_rect.width - (style.ITEM_PADDING * 2), style.BUTTON_HEIGHT)
|
||||
|
||||
right_width = self.action_item.rect.width
|
||||
if right_width == 0: # Full width action (like DualButtonAction)
|
||||
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, item_rect.y,
|
||||
item_rect.width - (style.ITEM_PADDING * 2), style.ITEM_BASE_HEIGHT)
|
||||
if right_width == 0:
|
||||
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, item_rect.y, item_rect.width - (style.ITEM_PADDING * 2), style.ITEM_BASE_HEIGHT)
|
||||
|
||||
action_width = self.action_item.rect.width
|
||||
if isinstance(self.action_item, ToggleAction):
|
||||
@@ -141,7 +152,7 @@ class ListItemSP(ListItem):
|
||||
if self.title:
|
||||
self._text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
|
||||
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - self._text_size.y) // 2
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, style.ITEM_TEXT_COLOR)
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, self.title_color)
|
||||
|
||||
# Render toggle and handle callback
|
||||
if self.action_item.render(left_rect) and self.action_item.enabled:
|
||||
@@ -153,7 +164,7 @@ class ListItemSP(ListItem):
|
||||
# Draw main text
|
||||
self._text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
|
||||
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - self._text_size.y) // 2 if self.inline else self._rect.y + style.ITEM_PADDING * 1.5
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, style.ITEM_TEXT_COLOR)
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, self.title_color)
|
||||
|
||||
# Draw right item if present
|
||||
if self.action_item:
|
||||
@@ -170,7 +181,7 @@ class ListItemSP(ListItem):
|
||||
|
||||
desc_y = self._rect.y + style.ITEM_DESC_V_OFFSET
|
||||
if not self.inline and self.action_item:
|
||||
desc_y = self.action_item.rect.y + style.ITEM_DESC_V_OFFSET - style.ITEM_PADDING * 1.75
|
||||
desc_y = self.action_item.rect.y + style.ITEM_DESC_V_OFFSET - style.ITEM_PADDING * 0.5
|
||||
|
||||
description_rect = rl.Rectangle(self._rect.x + style.ITEM_PADDING, desc_y, content_width, description_height)
|
||||
self._html_renderer.render(description_rect)
|
||||
|
||||
@@ -11,8 +11,8 @@ from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.label import gui_label, Label
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle, BUTTON_PRESSED_BACKGROUND_COLORS
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
@@ -34,7 +34,7 @@ class TreeFolder:
|
||||
|
||||
|
||||
class TreeItemWidget(Button):
|
||||
def __init__(self, text, ref, is_folder=False, indent_level=0, click_callback=None, favorite_callback=None, is_favorite=False):
|
||||
def __init__(self, text, ref, is_folder=False, indent_level=0, click_callback=None, favorite_callback=None, is_favorite=False, is_expanded=False):
|
||||
super().__init__(text, click_callback, button_style=ButtonStyle.NORMAL, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
text_padding=20 + indent_level * 30, elide_right=True)
|
||||
self.text = text
|
||||
@@ -46,14 +46,21 @@ class TreeItemWidget(Button):
|
||||
self._favorite_callback = favorite_callback
|
||||
self.text_padding = 20 + indent_level * 30
|
||||
self.border_radius = 10
|
||||
self.is_expanded = is_expanded
|
||||
|
||||
def _render(self, rect):
|
||||
indent = 60 * self.indent_level if self.indent_level > 0 else 10
|
||||
indent = 60 * self.indent_level
|
||||
self._rect = rl.Rectangle(rect.x + indent, rect.y, rect.width - indent, rect.height)
|
||||
color = style.BUTTON_PRIMARY_COLOR if self.selected and not (self.ref == "search_bar" or self.is_folder) else style.BUTTON_DISABLED_BG_COLOR
|
||||
if self.is_pressed:
|
||||
color = BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style]
|
||||
elif self.selected and self.ref != "search_bar":
|
||||
color = style.BUTTON_PRIMARY_COLOR
|
||||
else:
|
||||
color = style.BUTTON_DISABLED_BG_COLOR
|
||||
roundness = self.border_radius / (min(self._rect.width, self._rect.height) / 2)
|
||||
rl.draw_rectangle_rounded(self._rect, roundness, 10, color)
|
||||
text_rect = rl.Rectangle(self._rect.x + self.text_padding + 20, self._rect.y, self._rect.width - self.text_padding - 20 - 90, self._rect.height)
|
||||
text_offset = self.text_padding + 20 - 15 if self.is_expanded and not self.is_folder and self.indent_level > 0 else self.text_padding + 20
|
||||
text_rect = rl.Rectangle(self._rect.x + text_offset, self._rect.y, self._rect.width - self.text_padding - 20 - 90, self._rect.height)
|
||||
self._label.render(text_rect)
|
||||
|
||||
if not self.is_folder and self._favorite_callback:
|
||||
@@ -70,7 +77,7 @@ class TreeItemWidget(Button):
|
||||
|
||||
class TreeOptionDialog(MultiOptionDialog):
|
||||
def __init__(self, title, folders, current_ref="", fav_param="", option_font_weight=FontWeight.MEDIUM, search_prompt=None,
|
||||
get_folders_fn=None, on_exit=None, display_func=None, search_funcs=None):
|
||||
get_folders_fn=None, on_exit=None, display_func=None, search_funcs=None, search_title=None, search_subtitle=None):
|
||||
super().__init__(title, [], "", option_font_weight)
|
||||
self.folders = folders
|
||||
self.selection_ref = current_ref
|
||||
@@ -85,9 +92,15 @@ class TreeOptionDialog(MultiOptionDialog):
|
||||
self.on_exit = on_exit
|
||||
self.display_func = display_func or (lambda node: node.data.get('display_name', node.ref))
|
||||
self.search_funcs = search_funcs or [lambda node: node.data.get('display_name', ''), lambda node: node.data.get('short_name', '')]
|
||||
self._search_rect = None
|
||||
self._search_width = 0.475
|
||||
|
||||
# Default title & overridable subtitle for InputDialogSP
|
||||
self.search_title = search_title or tr("Enter search query")
|
||||
self.search_subtitle = search_subtitle
|
||||
self.search_dialog = None
|
||||
|
||||
self._build_visible_items()
|
||||
self.cancel_rect = None
|
||||
self.select_rect = None
|
||||
|
||||
def _on_search_confirm(self, result, text):
|
||||
if result == DialogResult.CONFIRM:
|
||||
@@ -96,7 +109,13 @@ class TreeOptionDialog(MultiOptionDialog):
|
||||
gui_app.set_modal_overlay(self, callback=self.on_exit)
|
||||
|
||||
def _on_search_clicked(self):
|
||||
InputDialogSP(tr("Enter search query"), current_text=self.query, callback=self._on_search_confirm).show()
|
||||
self.search_dialog = InputDialogSP(
|
||||
self.search_title,
|
||||
self.search_subtitle,
|
||||
current_text=self.query,
|
||||
callback=self._on_search_confirm,
|
||||
)
|
||||
self.search_dialog.show()
|
||||
|
||||
def _toggle_folder(self, folder):
|
||||
if folder.folder:
|
||||
@@ -121,7 +140,7 @@ class TreeOptionDialog(MultiOptionDialog):
|
||||
self._build_visible_items(reset_scroll=False)
|
||||
|
||||
def _build_visible_items(self, reset_scroll=True):
|
||||
self.visible_items = [TreeItemWidget(self.query or self.search_prompt, "search_bar", False, 0, self._on_search_clicked)]
|
||||
self.visible_items = []
|
||||
for folder in self.folders:
|
||||
nodes = [node for node in folder.nodes if not self.query or search_from_list(self.query, [search_func(node) for search_func in self.search_funcs])]
|
||||
if not nodes and self.query:
|
||||
@@ -134,33 +153,68 @@ class TreeOptionDialog(MultiOptionDialog):
|
||||
for node in nodes:
|
||||
favorite_cb = (lambda node_ref=node: self._toggle_favorite(node_ref)) if self.fav_param and node.ref != "Default" else None
|
||||
self.visible_items.append(TreeItemWidget(self.display_func(node), node.ref, False, 1 if folder.folder else 0,
|
||||
lambda node_ref=node: self._select_node(node_ref), favorite_cb, node.ref in self.favorites))
|
||||
lambda node_ref=node: self._select_node(node_ref),
|
||||
favorite_cb, node.ref in self.favorites, is_expanded=expanded))
|
||||
self.option_buttons = self.visible_items
|
||||
self.options = [item.text for item in self.visible_items]
|
||||
self.scroller._items = self.visible_items
|
||||
if reset_scroll:
|
||||
self.scroller.scroll_panel.set_offset(0)
|
||||
|
||||
def _draw_button(self, button_rect, button_text, is_primary=False, is_enabled=True):
|
||||
if is_primary and is_enabled:
|
||||
button_color = style.BUTTON_PRIMARY_COLOR
|
||||
elif not is_enabled:
|
||||
button_color = style.BUTTON_NEUTRAL_GRAY
|
||||
else:
|
||||
button_color = style.BUTTON_DISABLED_BG_COLOR
|
||||
roundness = 10 / (min(button_rect.width, button_rect.height) / 2)
|
||||
rl.draw_rectangle_rounded(button_rect, roundness, 10, button_color)
|
||||
label = Label(button_text, 60, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_color=rl.WHITE if is_enabled else rl.GRAY)
|
||||
label.render(button_rect)
|
||||
|
||||
def _render(self, rect):
|
||||
dialog_content_rect = rl.Rectangle(rect.x + 50, rect.y + 50, rect.width - 100, rect.height - 100)
|
||||
rl.draw_rectangle_rounded(dialog_content_rect, 0.02, 20, rl.BLACK)
|
||||
gui_label(rl.Rectangle(dialog_content_rect.x + 50, dialog_content_rect.y + 50, dialog_content_rect.width - 100, 70),
|
||||
self.title, 70, font_weight=FontWeight.BOLD)
|
||||
|
||||
options_area_rect = rl.Rectangle(dialog_content_rect.x + 50, dialog_content_rect.y + 170, dialog_content_rect.width - 100, dialog_content_rect.height - 330)
|
||||
# Title on the left
|
||||
title_rect = rl.Rectangle(dialog_content_rect.x + 50, dialog_content_rect.y + 50, dialog_content_rect.width * 0.5, 70)
|
||||
gui_label(title_rect, self.title, 70, font_weight=FontWeight.BOLD)
|
||||
|
||||
# Search bar on the top right
|
||||
search_width = dialog_content_rect.width * self._search_width
|
||||
search_height = 110
|
||||
search_x = dialog_content_rect.x + dialog_content_rect.width - 50 - search_width
|
||||
search_y = dialog_content_rect.y + 40 # align roughly with title
|
||||
|
||||
self._search_rect = rl.Rectangle(search_x, search_y, search_width, search_height)
|
||||
|
||||
# Draw search field
|
||||
inset = 4
|
||||
roundness = 0.3
|
||||
input_rect = rl.Rectangle(self._search_rect.x + inset, self._search_rect.y + inset,
|
||||
self._search_rect.width - inset * 2, self._search_rect.height - inset * 2)
|
||||
|
||||
# Transparent fill + border
|
||||
rl.draw_rectangle_rounded(input_rect, roundness, 10, rl.Color(0, 0, 0, 0))
|
||||
rl.draw_rectangle_rounded_lines_ex(input_rect, roundness, 10, 3, rl.Color(150, 150, 150, 200))
|
||||
|
||||
# Magnifying glass icon
|
||||
icon_color = rl.Color(180, 180, 180, 240)
|
||||
cx = input_rect.x + 60
|
||||
cy = input_rect.y + input_rect.height / 2 - 5
|
||||
radius = min(input_rect.height * 0.28, 26)
|
||||
|
||||
circle_thickness = 4
|
||||
for i in range(circle_thickness):
|
||||
rl.draw_circle_lines(int(cx), int(cy), radius - i, icon_color)
|
||||
|
||||
handle_thickness = 5
|
||||
inner_x = cx + radius * 0.65
|
||||
inner_y = cy + radius * 0.65
|
||||
outer_x = cx + radius * 1.45
|
||||
outer_y = cy + radius * 1.45
|
||||
|
||||
rl.draw_line_ex(rl.Vector2(inner_x, inner_y), rl.Vector2(outer_x, outer_y), handle_thickness, icon_color)
|
||||
|
||||
# User text (query), placed after the icon if present
|
||||
if self.query:
|
||||
text_start_x = outer_x + 45
|
||||
text_rect = rl.Rectangle(text_start_x, input_rect.y, input_rect.x + input_rect.width - text_start_x - 10, input_rect.height)
|
||||
gui_label(text_rect, self.query, 70, font_weight=FontWeight.MEDIUM)
|
||||
|
||||
options_top = self._search_rect.y + self._search_rect.height + 40
|
||||
options_area_rect = rl.Rectangle(dialog_content_rect.x + 50, options_top, dialog_content_rect.width - 100,
|
||||
dialog_content_rect.height - (options_top - dialog_content_rect.y) - 210)
|
||||
|
||||
for index, option_text in enumerate(self.options):
|
||||
self.option_buttons[index].selected = (option_text == self.selection)
|
||||
self.option_buttons[index].set_button_style(ButtonStyle.PRIMARY if option_text == self.selection else ButtonStyle.NORMAL)
|
||||
@@ -169,18 +223,18 @@ class TreeOptionDialog(MultiOptionDialog):
|
||||
|
||||
button_width = (dialog_content_rect.width - 150) / 2
|
||||
button_y_position = dialog_content_rect.y + dialog_content_rect.height - 160
|
||||
self.cancel_rect = rl.Rectangle(dialog_content_rect.x + 50, button_y_position, button_width, 160)
|
||||
self.select_rect = rl.Rectangle(dialog_content_rect.x + 100 + button_width, button_y_position, button_width, 160)
|
||||
|
||||
self._draw_button(self.cancel_rect, tr("Cancel"))
|
||||
self._draw_button(self.select_rect, tr("Select"), True, self.selection != self.current)
|
||||
cancel_rect = rl.Rectangle(dialog_content_rect.x + 50, button_y_position, button_width, 160)
|
||||
self.cancel_button.render(cancel_rect)
|
||||
|
||||
select_rect = rl.Rectangle(dialog_content_rect.x + 100 + button_width, button_y_position, button_width, 160)
|
||||
self.select_button.set_enabled(self.selection != self.current)
|
||||
self.select_button.render(select_rect)
|
||||
|
||||
return self._result
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
if self.cancel_rect and rl.check_collision_point_rec(mouse_pos, self.cancel_rect):
|
||||
self._set_result(DialogResult.CANCEL)
|
||||
return True
|
||||
if self.select_rect and rl.check_collision_point_rec(mouse_pos, self.select_rect) and self.selection != self.current:
|
||||
self._set_result(DialogResult.CONFIRM)
|
||||
if self._search_rect and rl.check_collision_point_rec(mouse_pos, self._search_rect):
|
||||
self._on_search_clicked()
|
||||
return True
|
||||
return super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
Reference in New Issue
Block a user