mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-18 21:43:51 +08:00
1102 lines
38 KiB
Python
1102 lines
38 KiB
Python
import math
|
|
import os
|
|
import pyray as rl
|
|
|
|
from collections.abc import Callable
|
|
|
|
from openpilot.common.params import Params
|
|
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
|
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.widgets.label import Label
|
|
from openpilot.system.ui.widgets.list_view import ITEM_DESC_FONT_SIZE, ITEM_DESC_TEXT_COLOR
|
|
from openpilot.system.ui.widgets.toggle import Toggle
|
|
|
|
__all__ = [
|
|
"CONTROL_HEIGHT",
|
|
"DEFAULT_BUTTON_HEIGHT",
|
|
"DEFAULT_BUTTON_WIDTH",
|
|
"FROGPILOT_VALUE_COLOR",
|
|
"ITEM_SPACING",
|
|
"SEPARATOR_MARGIN",
|
|
"FrogPilotButtonControl",
|
|
"FrogPilotButtonsControl",
|
|
"FrogPilotButtonToggleControl",
|
|
"FrogPilotConfirmationDialog",
|
|
"FrogPilotDualParamValueControl",
|
|
"FrogPilotListWidget",
|
|
"FrogPilotManageControl",
|
|
"FrogPilotParamValueButtonControl",
|
|
"FrogPilotParamValueControl",
|
|
"GifAnimation",
|
|
"clear_gif",
|
|
"load_gif",
|
|
"load_image",
|
|
"load_texture_cached",
|
|
"open_descriptions",
|
|
]
|
|
|
|
CONTROL_HEIGHT = 120
|
|
DEFAULT_BUTTON_HEIGHT = 100
|
|
DEFAULT_BUTTON_WIDTH = 225
|
|
FROGPILOT_VALUE_COLOR = rl.Color(224, 232, 121, 255)
|
|
ITEM_SPACING = 25
|
|
SEPARATOR_MARGIN = 40
|
|
|
|
_gif_cache: dict[str, "GifAnimation"] = {}
|
|
_texture_cache: dict[str, rl.Texture] = {}
|
|
|
|
|
|
class GifAnimation:
|
|
def __init__(self, gif_path: str, size: tuple[int, int] | None = None):
|
|
self._current_frame = 0
|
|
self._frame_count = 0
|
|
self._frame_delay = 0.1
|
|
self._frames: list[rl.Texture] = []
|
|
self._last_update = 0.0
|
|
self._loaded = False
|
|
self._path = gif_path
|
|
self._running = False
|
|
self._size = size
|
|
|
|
self._load_gif()
|
|
|
|
def _load_gif(self) -> None:
|
|
if not os.path.exists(self._path):
|
|
return
|
|
|
|
try:
|
|
frame_count = rl.ffi.new("int *")
|
|
image = rl.load_image_anim(self._path.encode(), frame_count)
|
|
self._frame_count = frame_count[0]
|
|
|
|
if self._frame_count <= 0:
|
|
rl.unload_image(image)
|
|
return
|
|
|
|
frame_height = image.height // self._frame_count
|
|
for i in range(self._frame_count):
|
|
frame_rect = rl.Rectangle(0, i * frame_height, image.width, frame_height)
|
|
frame_image = rl.image_from_image(image, frame_rect)
|
|
|
|
if self._size:
|
|
rl.image_resize(rl.ffi.addressof(frame_image), self._size[0], self._size[1])
|
|
|
|
texture = rl.load_texture_from_image(frame_image)
|
|
self._frames.append(texture)
|
|
rl.unload_image(frame_image)
|
|
|
|
rl.unload_image(image)
|
|
self._loaded = True
|
|
except Exception:
|
|
self._loaded = False
|
|
|
|
@property
|
|
def file_name(self) -> str:
|
|
return self._path
|
|
|
|
@property
|
|
def is_loaded(self) -> bool:
|
|
return self._loaded and len(self._frames) > 0
|
|
|
|
@property
|
|
def is_running(self) -> bool:
|
|
return self._running
|
|
|
|
@property
|
|
def scaled_size(self) -> tuple[int, int] | None:
|
|
return self._size
|
|
|
|
def get_current_texture(self) -> rl.Texture | None:
|
|
if not self.is_loaded:
|
|
return None
|
|
return self._frames[self._current_frame]
|
|
|
|
def render(self, x: int, y: int, tint: rl.Color = rl.WHITE) -> None:
|
|
self.update()
|
|
texture = self.get_current_texture()
|
|
if texture:
|
|
rl.draw_texture(texture, x, y, tint)
|
|
|
|
def set_scaled_size(self, size: tuple[int, int]) -> None:
|
|
if self._size == size:
|
|
return
|
|
self._size = size
|
|
|
|
def start(self) -> None:
|
|
self._running = True
|
|
self._last_update = rl.get_time()
|
|
|
|
def stop(self) -> None:
|
|
self._running = False
|
|
|
|
def unload(self) -> None:
|
|
for texture in self._frames:
|
|
rl.unload_texture(texture)
|
|
self._frames.clear()
|
|
self._loaded = False
|
|
|
|
def update(self) -> None:
|
|
if not self._running or not self.is_loaded:
|
|
return
|
|
|
|
current_time = rl.get_time()
|
|
if current_time - self._last_update >= self._frame_delay:
|
|
self._current_frame = (self._current_frame + 1) % self._frame_count
|
|
self._last_update = current_time
|
|
|
|
|
|
def clear_gif(gif: GifAnimation | None) -> None:
|
|
if gif is None:
|
|
return
|
|
|
|
gif.stop()
|
|
gif.unload()
|
|
|
|
|
|
def load_gif(gif_path: str, size: tuple[int, int], use_cache: bool = True) -> GifAnimation | None:
|
|
if not gif_path or not os.path.exists(gif_path):
|
|
return None
|
|
|
|
cache_key = f"{gif_path}_{size[0]}x{size[1]}"
|
|
|
|
if use_cache and cache_key in _gif_cache:
|
|
cached = _gif_cache[cache_key]
|
|
if cached.is_loaded:
|
|
if not cached.is_running:
|
|
cached.start()
|
|
return cached
|
|
|
|
gif = GifAnimation(gif_path, size)
|
|
if not gif.is_loaded:
|
|
return None
|
|
|
|
gif.start()
|
|
|
|
if use_cache:
|
|
_gif_cache[cache_key] = gif
|
|
|
|
return gif
|
|
|
|
|
|
def load_image(base_path: str, size: tuple[int, int]) -> tuple[rl.Texture | None, GifAnimation | None]:
|
|
gif_path = base_path + ".gif"
|
|
if os.path.exists(gif_path):
|
|
gif = load_gif(gif_path, size)
|
|
return (None, gif)
|
|
|
|
png_path = base_path + ".png"
|
|
texture = load_texture_cached(png_path, size[0], size[1])
|
|
return (texture, None)
|
|
|
|
|
|
def load_texture_cached(path: str, width: int = 0, height: int = 0) -> rl.Texture | None:
|
|
if not path:
|
|
return None
|
|
|
|
cache_key = f"{path}_{width}_{height}"
|
|
if cache_key in _texture_cache:
|
|
return _texture_cache[cache_key]
|
|
|
|
actual_path = path
|
|
if not os.path.exists(path):
|
|
for ext in [".png", ".gif", ".jpg", ".jpeg"]:
|
|
test_path = path + ext
|
|
if os.path.exists(test_path):
|
|
actual_path = test_path
|
|
break
|
|
else:
|
|
return None
|
|
|
|
texture = gui_app.texture(actual_path, width, height) if width > 0 and height > 0 else rl.load_texture(actual_path)
|
|
_texture_cache[cache_key] = texture
|
|
return texture
|
|
|
|
|
|
def open_descriptions(force_open: bool, toggles: dict) -> None:
|
|
if force_open:
|
|
for key, toggle in toggles.items():
|
|
if key != "CESpeed" and hasattr(toggle, "show_description"):
|
|
toggle.show_description()
|
|
|
|
|
|
class FrogPilotButtonControl(Widget):
|
|
def __init__(self,
|
|
param: str,
|
|
title: str,
|
|
description: str,
|
|
icon: str = "",
|
|
button_texts: list[str] = None,
|
|
checkable: bool = False,
|
|
exclusive: bool = False,
|
|
minimum_button_width: int = DEFAULT_BUTTON_WIDTH):
|
|
super().__init__()
|
|
|
|
self._button_checked: list[bool] = []
|
|
self._button_enabled: list[bool] = []
|
|
self._buttons: list[Button] = []
|
|
self._checkable = checkable
|
|
self._click_callback: Callable[[int], None] | None = None
|
|
self._description = description
|
|
self._disabled_click_callback: Callable[[int], None] | None = None
|
|
self._exclusive = exclusive
|
|
self._minimum_button_width = minimum_button_width
|
|
self._param_key = param
|
|
self._params = Params()
|
|
|
|
self._desc_label = Label(description, font_size=ITEM_DESC_FONT_SIZE,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
|
text_color=ITEM_DESC_TEXT_COLOR)
|
|
self._title_label = Label(title, font_size=50, font_weight=FontWeight.MEDIUM,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
|
self._toggle = Toggle(initial_state=self._params.get_bool(param),
|
|
callback=self._on_toggle_changed)
|
|
|
|
button_texts = button_texts or []
|
|
for i, text in enumerate(button_texts):
|
|
btn = Button(text, click_callback=lambda idx=i: self._on_button_click(idx),
|
|
button_style=ButtonStyle.LIST_ACTION)
|
|
self._buttons.append(btn)
|
|
self._button_checked.append(False)
|
|
self._button_enabled.append(True)
|
|
|
|
def _on_button_click(self, button_id: int) -> None:
|
|
if not self._button_enabled[button_id]:
|
|
if self._disabled_click_callback:
|
|
self._disabled_click_callback(button_id)
|
|
return
|
|
|
|
if self._checkable:
|
|
if self._exclusive:
|
|
for i in range(len(self._button_checked)):
|
|
self._button_checked[i] = (i == button_id)
|
|
else:
|
|
self._button_checked[button_id] = not self._button_checked[button_id]
|
|
|
|
if self._click_callback:
|
|
self._click_callback(button_id)
|
|
|
|
def _on_toggle_changed(self, state: bool) -> None:
|
|
self._params.put_bool(self._param_key, state)
|
|
self.refresh()
|
|
|
|
def _render(self, rect: rl.Rectangle) -> None:
|
|
desc_height = ITEM_DESC_FONT_SIZE if self._description else 0
|
|
title_height = 50
|
|
toggle_width = 160
|
|
|
|
visible_buttons = [(i, btn) for i, btn in enumerate(self._buttons) if btn.is_visible]
|
|
total_button_width = sum(self._minimum_button_width for _ in visible_buttons) + 10 * max(0, len(visible_buttons) - 1)
|
|
|
|
text_width = rect.width - total_button_width - toggle_width - 60
|
|
|
|
title_rect = rl.Rectangle(rect.x + 20, rect.y + 10, text_width, title_height)
|
|
self._title_label.render(title_rect)
|
|
|
|
if self._description:
|
|
desc_rect = rl.Rectangle(rect.x + 20, rect.y + 10 + title_height + 5, text_width, desc_height)
|
|
self._desc_label.render(desc_rect)
|
|
|
|
if visible_buttons:
|
|
button_x = rect.x + text_width + 30
|
|
button_y = rect.y + (rect.height - DEFAULT_BUTTON_HEIGHT) // 2
|
|
|
|
toggle_state = self._toggle.get_state()
|
|
for i, btn in visible_buttons:
|
|
btn.set_enabled(self._button_enabled[i] and toggle_state)
|
|
|
|
if self._checkable and self._button_checked[i]:
|
|
btn.set_button_style(ButtonStyle.PRIMARY)
|
|
else:
|
|
btn.set_button_style(ButtonStyle.LIST_ACTION)
|
|
|
|
btn_rect = rl.Rectangle(button_x, button_y, self._minimum_button_width, DEFAULT_BUTTON_HEIGHT)
|
|
btn.render(btn_rect)
|
|
button_x += self._minimum_button_width + 10
|
|
|
|
toggle_x = rect.x + rect.width - toggle_width - 20
|
|
toggle_y = rect.y + (rect.height - 80) // 2
|
|
self._toggle.render(rl.Rectangle(toggle_x, toggle_y, toggle_width, 80))
|
|
|
|
def clear_checked_buttons(self) -> None:
|
|
for i in range(len(self._button_checked)):
|
|
self._button_checked[i] = False
|
|
|
|
def refresh(self) -> None:
|
|
state = self._params.get_bool(self._param_key)
|
|
self._toggle.set_state(state)
|
|
|
|
for i in range(len(self._button_enabled)):
|
|
self._button_enabled[i] = state
|
|
|
|
def set_checked_button(self, button_id: int) -> None:
|
|
if 0 <= button_id < len(self._button_checked):
|
|
self._button_checked[button_id] = True
|
|
|
|
def set_click_callback(self, callback: Callable[[int], None]) -> None:
|
|
self._click_callback = callback
|
|
|
|
def set_disabled_click_callback(self, callback: Callable[[int], None]) -> None:
|
|
self._disabled_click_callback = callback
|
|
|
|
def set_enabled(self, enable: bool) -> None:
|
|
for i in range(len(self._button_enabled)):
|
|
self._button_enabled[i] = enable
|
|
|
|
def set_enabled_buttons(self, button_id: int, enable: bool) -> None:
|
|
if 0 <= button_id < len(self._button_enabled):
|
|
self._button_enabled[button_id] = enable
|
|
|
|
def set_text(self, button_id: int, text: str) -> None:
|
|
if 0 <= button_id < len(self._buttons):
|
|
self._buttons[button_id].set_text(text)
|
|
|
|
def set_visible_button(self, button_id: int, visible: bool) -> None:
|
|
if 0 <= button_id < len(self._buttons):
|
|
self._buttons[button_id].set_visible(visible)
|
|
|
|
def show_event(self) -> None:
|
|
self.refresh()
|
|
|
|
|
|
class FrogPilotButtonsControl(Widget):
|
|
def __init__(self,
|
|
title: str,
|
|
description: str,
|
|
icon: str = "",
|
|
button_texts: list[str] = None,
|
|
checkable: bool = False,
|
|
exclusive: bool = True,
|
|
minimum_button_width: int = DEFAULT_BUTTON_WIDTH):
|
|
super().__init__()
|
|
|
|
self._button_checked: list[bool] = []
|
|
self._button_enabled: list[bool] = []
|
|
self._buttons: list[Button] = []
|
|
self._checkable = checkable
|
|
self._click_callback: Callable[[int], None] | None = None
|
|
self._description = description
|
|
self._disabled_click_callback: Callable[[int], None] | None = None
|
|
self._exclusive = exclusive
|
|
self._minimum_button_width = minimum_button_width
|
|
|
|
self._desc_label = Label(description, font_size=ITEM_DESC_FONT_SIZE,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
|
text_color=ITEM_DESC_TEXT_COLOR)
|
|
self._title_label = Label(title, font_size=50, font_weight=FontWeight.MEDIUM,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
|
|
|
button_texts = button_texts or []
|
|
for i, text in enumerate(button_texts):
|
|
btn = Button(text, click_callback=lambda idx=i: self._on_button_click(idx),
|
|
button_style=ButtonStyle.LIST_ACTION)
|
|
self._buttons.append(btn)
|
|
self._button_checked.append(False)
|
|
self._button_enabled.append(True)
|
|
|
|
def _on_button_click(self, button_id: int) -> None:
|
|
if not self._button_enabled[button_id]:
|
|
if self._disabled_click_callback:
|
|
self._disabled_click_callback(button_id)
|
|
return
|
|
|
|
if self._checkable:
|
|
if self._exclusive:
|
|
for i in range(len(self._button_checked)):
|
|
self._button_checked[i] = (i == button_id)
|
|
else:
|
|
self._button_checked[button_id] = not self._button_checked[button_id]
|
|
|
|
if self._click_callback:
|
|
self._click_callback(button_id)
|
|
|
|
def _render(self, rect: rl.Rectangle) -> None:
|
|
desc_height = ITEM_DESC_FONT_SIZE if self._description else 0
|
|
text_width = rect.width * 0.5
|
|
title_height = 50
|
|
|
|
title_rect = rl.Rectangle(rect.x + 20, rect.y + 10, text_width, title_height)
|
|
self._title_label.render(title_rect)
|
|
|
|
if self._description:
|
|
desc_rect = rl.Rectangle(rect.x + 20, rect.y + 10 + title_height + 5, text_width, desc_height)
|
|
self._desc_label.render(desc_rect)
|
|
|
|
visible_buttons = [(i, btn) for i, btn in enumerate(self._buttons) if btn.is_visible]
|
|
if visible_buttons:
|
|
total_button_width = sum(self._minimum_button_width for _ in visible_buttons) + 10 * (len(visible_buttons) - 1)
|
|
button_x = rect.x + rect.width - total_button_width - 20
|
|
button_y = rect.y + (rect.height - DEFAULT_BUTTON_HEIGHT) // 2
|
|
|
|
for i, btn in visible_buttons:
|
|
btn.set_enabled(self._button_enabled[i])
|
|
|
|
if self._checkable and self._button_checked[i]:
|
|
btn.set_button_style(ButtonStyle.PRIMARY)
|
|
else:
|
|
btn.set_button_style(ButtonStyle.LIST_ACTION)
|
|
|
|
btn_rect = rl.Rectangle(button_x, button_y, self._minimum_button_width, DEFAULT_BUTTON_HEIGHT)
|
|
btn.render(btn_rect)
|
|
button_x += self._minimum_button_width + 10
|
|
|
|
def clear_checked_buttons(self) -> None:
|
|
for i in range(len(self._button_checked)):
|
|
self._button_checked[i] = False
|
|
|
|
def set_checked_button(self, button_id: int) -> None:
|
|
if 0 <= button_id < len(self._button_checked):
|
|
self._button_checked[button_id] = True
|
|
|
|
def set_click_callback(self, callback: Callable[[int], None]) -> None:
|
|
self._click_callback = callback
|
|
|
|
def set_disabled_click_callback(self, callback: Callable[[int], None]) -> None:
|
|
self._disabled_click_callback = callback
|
|
|
|
def set_enabled(self, enable: bool) -> None:
|
|
for i in range(len(self._button_enabled)):
|
|
self._button_enabled[i] = enable
|
|
|
|
def set_enabled_buttons(self, button_id: int, enable: bool) -> None:
|
|
if 0 <= button_id < len(self._button_enabled):
|
|
self._button_enabled[button_id] = enable
|
|
|
|
def set_text(self, button_id: int, text: str) -> None:
|
|
if 0 <= button_id < len(self._buttons):
|
|
self._buttons[button_id].set_text(text)
|
|
|
|
def set_visible_button(self, button_id: int, visible: bool) -> None:
|
|
if 0 <= button_id < len(self._buttons):
|
|
self._buttons[button_id].set_visible(visible)
|
|
|
|
|
|
class FrogPilotButtonToggleControl(FrogPilotButtonControl):
|
|
def __init__(self,
|
|
param: str,
|
|
title: str,
|
|
description: str,
|
|
icon: str = "",
|
|
button_params: list[str] = None,
|
|
button_texts: list[str] = None,
|
|
exclusive: bool = False,
|
|
minimum_button_width: int = DEFAULT_BUTTON_WIDTH):
|
|
super().__init__(param, title, description, icon, button_texts, True, exclusive, minimum_button_width)
|
|
|
|
self._button_params = button_params or []
|
|
|
|
for i, bp in enumerate(self._button_params):
|
|
if i < len(self._button_checked):
|
|
self._button_checked[i] = self._params.get_bool(bp)
|
|
|
|
def _on_button_click(self, button_id: int) -> None:
|
|
if not self._button_enabled[button_id]:
|
|
if self._disabled_click_callback:
|
|
self._disabled_click_callback(button_id)
|
|
return
|
|
|
|
if button_id < len(self._button_params):
|
|
new_state = not self._button_checked[button_id]
|
|
self._button_checked[button_id] = new_state
|
|
self._params.put_bool(self._button_params[button_id], new_state)
|
|
|
|
if self._click_callback:
|
|
self._click_callback(button_id)
|
|
|
|
def refresh(self) -> None:
|
|
super().refresh()
|
|
|
|
for i, bp in enumerate(self._button_params):
|
|
if i < len(self._button_checked):
|
|
self._button_checked[i] = self._params.get_bool(bp)
|
|
|
|
|
|
class FrogPilotConfirmationDialog(ConfirmDialog):
|
|
def __init__(self, prompt_text: str, confirm_text: str, cancel_text: str, rich: bool = False):
|
|
super().__init__(prompt_text, confirm_text, cancel_text, rich)
|
|
|
|
@staticmethod
|
|
def create_toggle_reboot() -> "FrogPilotConfirmationDialog":
|
|
return FrogPilotConfirmationDialog(
|
|
"Reboot required to take effect.",
|
|
"Reboot Now",
|
|
"Reboot Later"
|
|
)
|
|
|
|
@staticmethod
|
|
def create_yesorno(prompt_text: str) -> "FrogPilotConfirmationDialog":
|
|
return FrogPilotConfirmationDialog(prompt_text, "Yes", "No")
|
|
|
|
@staticmethod
|
|
def toggle_reboot(parent=None) -> bool:
|
|
return False
|
|
|
|
@staticmethod
|
|
def yesorno(prompt_text: str, parent=None) -> bool:
|
|
return False
|
|
|
|
|
|
class FrogPilotDualParamValueControl(Widget):
|
|
def __init__(self, control1: "FrogPilotParamValueControl", control2: "FrogPilotParamValueControl"):
|
|
super().__init__()
|
|
self._control1 = control1
|
|
self._control2 = control2
|
|
|
|
def _render(self, rect: rl.Rectangle) -> None:
|
|
half_width = rect.width // 2 - 5
|
|
|
|
control1_rect = rl.Rectangle(rect.x, rect.y, half_width, rect.height)
|
|
control2_rect = rl.Rectangle(rect.x + half_width + 10, rect.y, half_width, rect.height)
|
|
|
|
self._control1.render(control1_rect)
|
|
self._control2.render(control2_rect)
|
|
|
|
def refresh(self) -> None:
|
|
self._control1.refresh()
|
|
self._control2.refresh()
|
|
|
|
def update_control(self, min_value: float, max_value: float, value_labels: dict[float, str] = None) -> None:
|
|
self._control1.update_control(min_value, max_value, value_labels)
|
|
self._control2.update_control(min_value, max_value, value_labels)
|
|
|
|
|
|
class FrogPilotListWidget(Widget):
|
|
def __init__(self, spacing: int = ITEM_SPACING):
|
|
super().__init__()
|
|
self._items: list[Widget] = []
|
|
self._spacing = spacing
|
|
|
|
def _render(self, rect: rl.Rectangle) -> None:
|
|
if not self._items:
|
|
return
|
|
|
|
current_y = rect.y
|
|
visible_items = [item for item in self._items if item.is_visible]
|
|
|
|
for i, item in enumerate(visible_items):
|
|
item_height = item.rect.height if item.rect.height > 0 else CONTROL_HEIGHT
|
|
item_rect = rl.Rectangle(rect.x, current_y, rect.width, item_height)
|
|
|
|
item.render(item_rect)
|
|
|
|
if i < len(visible_items) - 1:
|
|
separator_y = current_y + item_height + self._spacing // 2
|
|
rl.draw_line(
|
|
int(rect.x + SEPARATOR_MARGIN),
|
|
int(separator_y),
|
|
int(rect.x + rect.width - SEPARATOR_MARGIN),
|
|
int(separator_y),
|
|
ITEM_DESC_TEXT_COLOR
|
|
)
|
|
|
|
current_y += item_height + self._spacing
|
|
|
|
def add_item(self, widget: Widget, expanding: bool = False) -> None:
|
|
self._items.append(widget)
|
|
|
|
def clear(self) -> None:
|
|
self._items.clear()
|
|
|
|
def insert_item(self, index: int, widget: Widget, expanding: bool = False) -> None:
|
|
self._items.insert(index, widget)
|
|
|
|
def set_spacing(self, spacing: int) -> None:
|
|
self._spacing = spacing
|
|
|
|
|
|
class FrogPilotManageControl(Widget):
|
|
def __init__(self, param: str, title: str, description: str, icon: str = ""):
|
|
super().__init__()
|
|
|
|
self._description = description
|
|
self._manage_callback: Callable[[], None] | None = None
|
|
self._manage_visible = True
|
|
self._param_key = param
|
|
self._params = Params()
|
|
|
|
self._desc_label = Label(description, font_size=ITEM_DESC_FONT_SIZE,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
|
text_color=ITEM_DESC_TEXT_COLOR)
|
|
self._manage_button = Button("MANAGE", click_callback=self._on_manage_clicked,
|
|
button_style=ButtonStyle.LIST_ACTION)
|
|
self._title_label = Label(title, font_size=50, font_weight=FontWeight.MEDIUM,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
|
self._toggle = Toggle(initial_state=self._params.get_bool(param),
|
|
callback=self._on_toggle_changed)
|
|
|
|
def _on_manage_clicked(self) -> None:
|
|
if self._manage_callback:
|
|
self._manage_callback()
|
|
|
|
def _on_toggle_changed(self, state: bool) -> None:
|
|
self._params.put_bool(self._param_key, state)
|
|
self.refresh()
|
|
|
|
def _render(self, rect: rl.Rectangle) -> None:
|
|
desc_height = ITEM_DESC_FONT_SIZE if self._description else 0
|
|
manage_width = 150 if self._manage_visible else 0
|
|
title_height = 50
|
|
toggle_width = 160
|
|
|
|
text_width = rect.width - toggle_width - manage_width - 60
|
|
|
|
title_rect = rl.Rectangle(rect.x + 20, rect.y + 10, text_width, title_height)
|
|
self._title_label.render(title_rect)
|
|
|
|
if self._description:
|
|
desc_rect = rl.Rectangle(rect.x + 20, rect.y + 10 + title_height + 5, text_width, desc_height)
|
|
self._desc_label.render(desc_rect)
|
|
|
|
if self._manage_visible:
|
|
manage_x = rect.x + text_width + 30
|
|
manage_y = rect.y + (rect.height - DEFAULT_BUTTON_HEIGHT) // 2
|
|
self._manage_button.render(rl.Rectangle(manage_x, manage_y, manage_width, DEFAULT_BUTTON_HEIGHT))
|
|
|
|
toggle_x = rect.x + rect.width - toggle_width - 20
|
|
toggle_y = rect.y + (rect.height - 80) // 2
|
|
self._toggle.render(rl.Rectangle(toggle_x, toggle_y, toggle_width, 80))
|
|
|
|
def refresh(self) -> None:
|
|
state = self._params.get_bool(self._param_key)
|
|
self._toggle.set_state(state)
|
|
self._manage_button.set_enabled(state)
|
|
|
|
def set_manage_callback(self, callback: Callable[[], None]) -> None:
|
|
self._manage_callback = callback
|
|
|
|
def set_manage_visibility(self, visible: bool) -> None:
|
|
self._manage_visible = visible
|
|
|
|
def show_event(self) -> None:
|
|
self.refresh()
|
|
|
|
|
|
class FrogPilotParamValueButtonControl(Widget):
|
|
def __init__(self,
|
|
param: str,
|
|
title: str,
|
|
description: str,
|
|
icon: str = "",
|
|
min_value: float = 0,
|
|
max_value: float = 100,
|
|
label: str = "",
|
|
value_labels: dict[float, str] = None,
|
|
interval: float = 1.0,
|
|
fast_increase: bool = False,
|
|
button_params: list[str] = None,
|
|
button_texts: list[str] = None,
|
|
left_button: bool = False,
|
|
checkable: bool = True,
|
|
minimum_button_width: int = DEFAULT_BUTTON_WIDTH):
|
|
super().__init__()
|
|
|
|
self._button_checked: list[bool] = []
|
|
self._button_click_callback: Callable[[int], None] | None = None
|
|
self._button_enabled: list[bool] = []
|
|
self._button_params = button_params or []
|
|
self._buttons: list[Button] = []
|
|
self._checkable = checkable
|
|
self._decrement_repeating = False
|
|
self._description = description
|
|
self._display_warning = False
|
|
self._factor = 10 ** math.ceil(-math.log10(interval)) if interval > 0 else 1
|
|
self._fast_increase = fast_increase
|
|
self._increment_repeating = False
|
|
self._interval = interval
|
|
self._label_suffix = label
|
|
self._label_width = 200
|
|
self._last_action_time = 0.0
|
|
self._left_button = left_button
|
|
self._max_value = max_value
|
|
self._min_value = min_value
|
|
self._minimum_button_width = minimum_button_width
|
|
self._param_key = param
|
|
self._params = Params()
|
|
self._value_changed_callback: Callable[[float], None] | None = None
|
|
self._value_labels = value_labels or {}
|
|
self._warning_shown = False
|
|
|
|
self._value = self._read_param_value()
|
|
self._previous_value = self._value
|
|
|
|
self._decrement_button = Button("-", click_callback=self._on_decrement,
|
|
font_size=50, button_style=ButtonStyle.LIST_ACTION)
|
|
self._desc_label = Label(description, font_size=ITEM_DESC_FONT_SIZE,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
|
text_color=ITEM_DESC_TEXT_COLOR)
|
|
self._increment_button = Button("+", click_callback=self._on_increment,
|
|
font_size=50, button_style=ButtonStyle.LIST_ACTION)
|
|
self._title_label = Label(title, font_size=50, font_weight=FontWeight.MEDIUM,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
|
self._value_label = Label(self._format_value(), font_size=50,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
|
text_color=FROGPILOT_VALUE_COLOR)
|
|
|
|
button_texts = button_texts or []
|
|
for i, text in enumerate(button_texts):
|
|
checked = checkable and i < len(self._button_params) and self._params.get_bool(self._button_params[i])
|
|
btn = Button(text, click_callback=lambda idx=i: self._on_button_click(idx),
|
|
button_style=ButtonStyle.PRIMARY if checked else ButtonStyle.LIST_ACTION)
|
|
self._buttons.append(btn)
|
|
self._button_checked.append(checked)
|
|
self._button_enabled.append(True)
|
|
|
|
def _format_value(self) -> str:
|
|
for val, label in self._value_labels.items():
|
|
if round(val * self._factor) == round(self._value * self._factor):
|
|
return label
|
|
|
|
if self._interval >= 1:
|
|
return f"{int(self._value)}{self._label_suffix}"
|
|
else:
|
|
decimals = max(0, int(math.ceil(-math.log10(self._interval))))
|
|
return f"{self._value:.{decimals}f}{self._label_suffix}"
|
|
|
|
def _on_button_click(self, button_id: int) -> None:
|
|
if self._checkable and button_id < len(self._button_params):
|
|
new_state = not self._button_checked[button_id]
|
|
self._button_checked[button_id] = new_state
|
|
self._params.put_bool(self._button_params[button_id], new_state)
|
|
|
|
self._buttons[button_id].set_button_style(
|
|
ButtonStyle.PRIMARY if new_state else ButtonStyle.LIST_ACTION
|
|
)
|
|
|
|
if self._button_click_callback:
|
|
self._button_click_callback(button_id)
|
|
|
|
def _on_decrement(self) -> None:
|
|
if self._display_warning and not self._warning_shown:
|
|
self._show_warning()
|
|
|
|
current_time = rl.get_time()
|
|
if current_time - self._last_action_time > 0.65:
|
|
self._decrement_repeating = False
|
|
|
|
delta = self._interval * 5 if self._decrement_repeating and self._fast_increase else self._interval
|
|
self._value = max(self._value - delta, self._min_value)
|
|
self._update_value()
|
|
|
|
if round(self._value / self._interval) % 5 == 0:
|
|
self._decrement_repeating = True
|
|
|
|
self._last_action_time = current_time
|
|
|
|
def _on_increment(self) -> None:
|
|
if self._display_warning and not self._warning_shown:
|
|
self._show_warning()
|
|
|
|
current_time = rl.get_time()
|
|
if current_time - self._last_action_time > 0.65:
|
|
self._increment_repeating = False
|
|
|
|
delta = self._interval * 5 if self._increment_repeating and self._fast_increase else self._interval
|
|
self._value = min(self._value + delta, self._max_value)
|
|
self._update_value()
|
|
|
|
if round(self._value / self._interval) % 5 == 0:
|
|
self._increment_repeating = True
|
|
|
|
self._last_action_time = current_time
|
|
|
|
def _read_param_value(self) -> float:
|
|
try:
|
|
return float(self._params.get_int(self._param_key))
|
|
except (ValueError, TypeError):
|
|
try:
|
|
return self._params.get_float(self._param_key)
|
|
except (ValueError, TypeError):
|
|
return self._min_value
|
|
|
|
def _render(self, rect: rl.Rectangle) -> None:
|
|
button_size = 100
|
|
total_btn_width = sum(self._minimum_button_width for _ in self._buttons) + 10 * max(0, len(self._buttons) - 1)
|
|
|
|
if self._left_button:
|
|
btn_start_x = rect.x + 20
|
|
value_start_x = btn_start_x + total_btn_width + 20
|
|
else:
|
|
value_start_x = rect.x + rect.width - button_size * 2 - self._label_width - 40
|
|
btn_start_x = rect.x + rect.width - total_btn_width - button_size * 2 - 60
|
|
|
|
text_width = value_start_x - rect.x - 40 if not self._left_button else rect.width - total_btn_width - self._label_width - button_size * 2 - 100
|
|
|
|
desc_height = ITEM_DESC_FONT_SIZE if self._description else 0
|
|
title_height = 50
|
|
|
|
title_rect = rl.Rectangle(rect.x + 20, rect.y + 10, text_width, title_height)
|
|
self._title_label.render(title_rect)
|
|
|
|
if self._description:
|
|
desc_rect = rl.Rectangle(rect.x + 20, rect.y + 10 + title_height + 5, text_width, desc_height)
|
|
self._desc_label.render(desc_rect)
|
|
|
|
button_y = rect.y + (rect.height - DEFAULT_BUTTON_HEIGHT) // 2
|
|
current_btn_x = btn_start_x
|
|
|
|
for i, btn in enumerate(self._buttons):
|
|
btn.set_enabled(self._button_enabled[i])
|
|
btn.render(rl.Rectangle(current_btn_x, button_y, self._minimum_button_width, DEFAULT_BUTTON_HEIGHT))
|
|
current_btn_x += self._minimum_button_width + 10
|
|
|
|
value_x = rect.x + rect.width - button_size * 2 - self._label_width - 40
|
|
value_y = rect.y + (rect.height - 50) // 2
|
|
self._value_label.render(rl.Rectangle(value_x, value_y, self._label_width, 50))
|
|
|
|
button_y = rect.y + (rect.height - button_size) // 2
|
|
|
|
dec_x = rect.x + rect.width - button_size * 2 - 30
|
|
self._decrement_button.render(rl.Rectangle(dec_x, button_y, button_size, button_size))
|
|
|
|
inc_x = rect.x + rect.width - button_size - 20
|
|
self._increment_button.render(rl.Rectangle(inc_x, button_y, button_size, button_size))
|
|
|
|
def _show_warning(self) -> None:
|
|
self._warning_shown = True
|
|
|
|
def _update_value(self) -> None:
|
|
self._value = round(self._value * self._factor) / self._factor
|
|
self._value_label.set_text(self._format_value())
|
|
|
|
self._decrement_button.set_enabled(self._value > self._min_value)
|
|
self._increment_button.set_enabled(self._value < self._max_value)
|
|
|
|
if self._value_changed_callback:
|
|
self._value_changed_callback(self._value)
|
|
|
|
def _write_param_value(self) -> None:
|
|
if self._value == self._previous_value:
|
|
return
|
|
|
|
if self._interval >= 1 and self._interval == int(self._interval):
|
|
self._params.put_int(self._param_key, int(self._value))
|
|
else:
|
|
self._params.put_float(self._param_key, self._value)
|
|
|
|
self._previous_value = self._value
|
|
|
|
def hide_event(self) -> None:
|
|
self._warning_shown = False
|
|
self._write_param_value()
|
|
|
|
def refresh(self) -> None:
|
|
self._value = self._read_param_value()
|
|
self._value = max(self._min_value, min(self._max_value, self._value))
|
|
self._previous_value = self._value
|
|
self._update_value()
|
|
self._write_param_value()
|
|
|
|
if self._checkable:
|
|
for i, bp in enumerate(self._button_params):
|
|
if i < len(self._button_checked):
|
|
checked = self._params.get_bool(bp)
|
|
self._button_checked[i] = checked
|
|
self._buttons[i].set_button_style(
|
|
ButtonStyle.PRIMARY if checked else ButtonStyle.LIST_ACTION
|
|
)
|
|
|
|
def set_button_click_callback(self, callback: Callable[[int], None]) -> None:
|
|
self._button_click_callback = callback
|
|
|
|
def set_enabled_buttons(self, button_id: int, enable: bool) -> None:
|
|
if 0 <= button_id < len(self._button_enabled):
|
|
self._button_enabled[button_id] = enable
|
|
|
|
def set_value_changed_callback(self, callback: Callable[[float], None]) -> None:
|
|
self._value_changed_callback = callback
|
|
|
|
def set_warning(self, warning: str) -> None:
|
|
self._display_warning = True
|
|
|
|
def show_event(self) -> None:
|
|
self.refresh()
|
|
|
|
def update_control(self, min_value: float, max_value: float, value_labels: dict[float, str] = None) -> None:
|
|
self._min_value = min_value
|
|
self._max_value = max_value
|
|
if value_labels is not None:
|
|
self._value_labels = value_labels
|
|
self.refresh()
|
|
|
|
|
|
class FrogPilotParamValueControl(Widget):
|
|
def __init__(self,
|
|
param: str,
|
|
title: str,
|
|
description: str,
|
|
icon: str = "",
|
|
min_value: float = 0,
|
|
max_value: float = 100,
|
|
label: str = "",
|
|
value_labels: dict[float, str] = None,
|
|
interval: float = 1.0,
|
|
fast_increase: bool = False,
|
|
label_width: int = 350):
|
|
super().__init__()
|
|
|
|
self._decrement_repeating = False
|
|
self._description = description
|
|
self._display_warning = False
|
|
self._factor = 10 ** math.ceil(-math.log10(interval)) if interval > 0 else 1
|
|
self._fast_increase = fast_increase
|
|
self._increment_repeating = False
|
|
self._interval = interval
|
|
self._label_suffix = label
|
|
self._label_width = label_width
|
|
self._last_action_time = 0.0
|
|
self._max_value = max_value
|
|
self._min_value = min_value
|
|
self._param_key = param
|
|
self._params = Params()
|
|
self._value_changed_callback: Callable[[float], None] | None = None
|
|
self._value_labels = value_labels or {}
|
|
self._warning_shown = False
|
|
|
|
self._value = self._read_param_value()
|
|
self._previous_value = self._value
|
|
|
|
self._decrement_button = Button("-", click_callback=self._on_decrement,
|
|
font_size=50, button_style=ButtonStyle.LIST_ACTION)
|
|
self._desc_label = Label(description, font_size=ITEM_DESC_FONT_SIZE,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
|
text_color=ITEM_DESC_TEXT_COLOR)
|
|
self._increment_button = Button("+", click_callback=self._on_increment,
|
|
font_size=50, button_style=ButtonStyle.LIST_ACTION)
|
|
self._title_label = Label(title, font_size=50, font_weight=FontWeight.MEDIUM,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
|
self._value_label = Label(self._format_value(), font_size=50,
|
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
|
text_color=FROGPILOT_VALUE_COLOR)
|
|
|
|
def _format_value(self) -> str:
|
|
for val, label in self._value_labels.items():
|
|
if round(val * self._factor) == round(self._value * self._factor):
|
|
return label
|
|
|
|
if self._interval >= 1:
|
|
return f"{int(self._value)}{self._label_suffix}"
|
|
else:
|
|
decimals = max(0, int(math.ceil(-math.log10(self._interval))))
|
|
return f"{self._value:.{decimals}f}{self._label_suffix}"
|
|
|
|
def _on_decrement(self) -> None:
|
|
if self._display_warning and not self._warning_shown:
|
|
self._show_warning()
|
|
|
|
current_time = rl.get_time()
|
|
if current_time - self._last_action_time > 0.65:
|
|
self._decrement_repeating = False
|
|
|
|
delta = self._interval * 5 if self._decrement_repeating and self._fast_increase else self._interval
|
|
self._value = max(self._value - delta, self._min_value)
|
|
self._update_value()
|
|
|
|
if round(self._value / self._interval) % 5 == 0:
|
|
self._decrement_repeating = True
|
|
|
|
self._last_action_time = current_time
|
|
|
|
def _on_increment(self) -> None:
|
|
if self._display_warning and not self._warning_shown:
|
|
self._show_warning()
|
|
|
|
current_time = rl.get_time()
|
|
if current_time - self._last_action_time > 0.65:
|
|
self._increment_repeating = False
|
|
|
|
delta = self._interval * 5 if self._increment_repeating and self._fast_increase else self._interval
|
|
self._value = min(self._value + delta, self._max_value)
|
|
self._update_value()
|
|
|
|
if round(self._value / self._interval) % 5 == 0:
|
|
self._increment_repeating = True
|
|
|
|
self._last_action_time = current_time
|
|
|
|
def _read_param_value(self) -> float:
|
|
try:
|
|
return float(self._params.get_int(self._param_key))
|
|
except (ValueError, TypeError):
|
|
try:
|
|
return self._params.get_float(self._param_key)
|
|
except (ValueError, TypeError):
|
|
return self._min_value
|
|
|
|
def _render(self, rect: rl.Rectangle) -> None:
|
|
button_size = 100
|
|
desc_height = ITEM_DESC_FONT_SIZE if self._description else 0
|
|
text_width = rect.width - self._label_width - button_size * 2 - 60
|
|
title_height = 50
|
|
value_width = self._label_width
|
|
|
|
title_rect = rl.Rectangle(rect.x + 20, rect.y + 10, text_width, title_height)
|
|
self._title_label.render(title_rect)
|
|
|
|
if self._description:
|
|
desc_rect = rl.Rectangle(rect.x + 20, rect.y + 10 + title_height + 5, text_width, desc_height)
|
|
self._desc_label.render(desc_rect)
|
|
|
|
value_x = rect.x + text_width + 20
|
|
value_y = rect.y + (rect.height - 50) // 2
|
|
self._value_label.render(rl.Rectangle(value_x, value_y, value_width, 50))
|
|
|
|
button_y = rect.y + (rect.height - button_size) // 2
|
|
|
|
dec_x = rect.x + rect.width - button_size * 2 - 30
|
|
self._decrement_button.render(rl.Rectangle(dec_x, button_y, button_size, button_size))
|
|
|
|
inc_x = rect.x + rect.width - button_size - 20
|
|
self._increment_button.render(rl.Rectangle(inc_x, button_y, button_size, button_size))
|
|
|
|
def _show_warning(self) -> None:
|
|
self._warning_shown = True
|
|
|
|
def _update_value(self) -> None:
|
|
self._value = round(self._value * self._factor) / self._factor
|
|
self._value_label.set_text(self._format_value())
|
|
|
|
self._decrement_button.set_enabled(self._value > self._min_value)
|
|
self._increment_button.set_enabled(self._value < self._max_value)
|
|
|
|
if self._value_changed_callback:
|
|
self._value_changed_callback(self._value)
|
|
|
|
def _write_param_value(self) -> None:
|
|
if self._value == self._previous_value:
|
|
return
|
|
|
|
if self._interval >= 1 and self._interval == int(self._interval):
|
|
self._params.put_int(self._param_key, int(self._value))
|
|
else:
|
|
self._params.put_float(self._param_key, self._value)
|
|
|
|
self._previous_value = self._value
|
|
|
|
def hide_event(self) -> None:
|
|
self._warning_shown = False
|
|
self._write_param_value()
|
|
|
|
def refresh(self) -> None:
|
|
self._value = self._read_param_value()
|
|
self._value = max(self._min_value, min(self._max_value, self._value))
|
|
self._previous_value = self._value
|
|
self._update_value()
|
|
self._write_param_value()
|
|
|
|
def set_value_changed_callback(self, callback: Callable[[float], None]) -> None:
|
|
self._value_changed_callback = callback
|
|
|
|
def set_warning(self, warning: str) -> None:
|
|
self._display_warning = True
|
|
|
|
def show_event(self) -> None:
|
|
self.refresh()
|
|
|
|
def update_control(self, min_value: float, max_value: float, value_labels: dict[float, str] = None) -> None:
|
|
self._min_value = min_value
|
|
self._max_value = max_value
|
|
if value_labels is not None:
|
|
self._value_labels = value_labels
|
|
self.refresh()
|