mirror of
https://jihulab.com/mr-one/openpilot.git
synced 2026-10-31 02:54:14 +08:00
TSK Manager v0.10.0
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# tsk/ui/button.py
|
||||
from typing import Callable, List, Dict, Any
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.button import gui_button
|
||||
from tsk.ui.layout import Theme # Import Theme
|
||||
|
||||
|
||||
class Button:
|
||||
"""Represents a button with text labels and an action."""
|
||||
|
||||
def __init__(self, action: Callable[[], None], labels: List[Dict[str, Any]], width: int, height: int, x: int, y: int,
|
||||
font_size: int):
|
||||
"""
|
||||
Initializes a Button object.
|
||||
|
||||
Args:
|
||||
key: The key associated with the button.
|
||||
action: The function to be called when the button is pressed.
|
||||
labels: A list of dictionaries containing text and offset information for the button labels.
|
||||
width: The width of the button.
|
||||
height: The height of the button.
|
||||
x: The x-coordinate of the button.
|
||||
y: The y-coordinate of the button.
|
||||
font_size: The font size of the button labels.
|
||||
"""
|
||||
self.action: Callable[[], None] = action
|
||||
self.labels: List[Dict[str, Any]] = labels
|
||||
self.width: int = width
|
||||
self.height: int = height
|
||||
self.x: int = x
|
||||
self.y: int = y
|
||||
self.font_size: int = font_size
|
||||
self.last_pressed_time: float = 0.0
|
||||
self.debounce_delay: float = 0.2 # seconds
|
||||
|
||||
def render(self) -> None:
|
||||
"""Renders the button."""
|
||||
current_time = rl.get_time()
|
||||
if gui_button(rl.Rectangle(self.x, self.y, self.width, self.height), ""):
|
||||
if current_time - self.last_pressed_time > self.debounce_delay:
|
||||
self.action()
|
||||
self.last_pressed_time = current_time
|
||||
|
||||
self._draw_labels()
|
||||
|
||||
def _draw_labels(self) -> None:
|
||||
"""Draws the labels on the button."""
|
||||
for label_data in self.labels:
|
||||
rl.draw_text_ex(
|
||||
gui_app.font(),
|
||||
label_data["text"],
|
||||
rl.Vector2(self.x + label_data["x_offset"], self.y + label_data["y_offset"]),
|
||||
self.font_size,
|
||||
1.0,
|
||||
Theme.brighten_color(rl.Color(100, 100, 100, 255), Theme.brighten_amount), # Use the brightened color here
|
||||
)
|
||||
@@ -0,0 +1,178 @@
|
||||
# tsk/ui/dialog.py
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.button import gui_button, DEFAULT_BUTTON_FONT_SIZE
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
|
||||
|
||||
class BaseDialog:
|
||||
"""Base class for full-screen dialogs with a scrollable text area."""
|
||||
|
||||
BORDER_SIZE = 20
|
||||
BUTTON_HEIGHT = 80
|
||||
BUTTON_WIDTH = 310
|
||||
BUTTON_SPACING = 20
|
||||
FONT_SIZE = 100
|
||||
LINE_HEIGHT = FONT_SIZE * 1.1
|
||||
TEXT_PADDING = 10
|
||||
|
||||
def __init__(self, body_text: str, font_size: int = FONT_SIZE, scroll_to_bottom: bool = False):
|
||||
self.body_text = body_text
|
||||
self.font_size = font_size
|
||||
self.scroll_to_bottom = scroll_to_bottom
|
||||
self.LINE_HEIGHT = self.font_size * 1.1
|
||||
self.textarea_rect = rl.Rectangle(
|
||||
self.BORDER_SIZE,
|
||||
self.BORDER_SIZE,
|
||||
gui_app.width - 2 * self.BORDER_SIZE,
|
||||
gui_app.height - 3 * self.BORDER_SIZE - self.BUTTON_HEIGHT # Account for buttons and spacing
|
||||
)
|
||||
self.wrapped_lines = self._wrap_text(self.body_text, self.font_size, self.textarea_rect.width - 2 * self.TEXT_PADDING)
|
||||
self.content_height = len(self.wrapped_lines) * self.LINE_HEIGHT
|
||||
self.content_rect = rl.Rectangle(0, 0, self.textarea_rect.width - 2 * self.TEXT_PADDING, self.content_height)
|
||||
self.scroll_panel = GuiScrollPanel(show_vertical_scroll_bar=True)
|
||||
self.scroll_offset = rl.Vector2(0, 0) # Store the scroll offset
|
||||
self.initial_scroll_applied = False # Flag to track initial scroll application
|
||||
|
||||
def render_text_area(self):
|
||||
"""Renders the scrollable text area."""
|
||||
|
||||
scroll = self.scroll_panel.handle_scroll(self.textarea_rect, self.content_rect)
|
||||
self.scroll_offset = scroll # Update the scroll offset after handling user input
|
||||
|
||||
# Apply initial scroll to bottom after the first render
|
||||
if self.scroll_to_bottom and not self.initial_scroll_applied:
|
||||
self.scroll_offset.y = min(0, self.textarea_rect.height - self.content_height - 2 * self.TEXT_PADDING)
|
||||
self.initial_scroll_applied = True
|
||||
|
||||
rl.begin_scissor_mode(int(self.textarea_rect.x), int(self.textarea_rect.y), int(self.textarea_rect.width), int(self.textarea_rect.height))
|
||||
y_offset = 0
|
||||
for line in self.wrapped_lines:
|
||||
position = rl.Vector2(self.textarea_rect.x + self.TEXT_PADDING + self.scroll_offset.x, self.textarea_rect.y + self.TEXT_PADDING + self.scroll_offset.y + y_offset)
|
||||
if position.y + self.LINE_HEIGHT < self.textarea_rect.y + self.TEXT_PADDING or position.y > self.textarea_rect.y + self.textarea_rect.height - self.TEXT_PADDING:
|
||||
y_offset += self.LINE_HEIGHT
|
||||
continue
|
||||
rl.draw_text_ex(gui_app.font(), line.strip(), position, self.font_size, 0, rl.WHITE)
|
||||
y_offset += self.LINE_HEIGHT
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _wrap_text(self, text, font_size, max_width):
|
||||
lines = []
|
||||
font = gui_app.font()
|
||||
|
||||
# Split the text by newline characters
|
||||
for block in text.splitlines():
|
||||
if not block: # Handle empty lines (consecutive newlines)
|
||||
lines.append("") # Add an empty line
|
||||
continue
|
||||
|
||||
current_line = ""
|
||||
for word in block.split():
|
||||
test_line = current_line + word + " "
|
||||
if rl.measure_text_ex(font, test_line, font_size, 0).x <= max_width:
|
||||
current_line = test_line
|
||||
else:
|
||||
lines.append(current_line)
|
||||
current_line = word + " "
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
class OkayDialog(BaseDialog):
|
||||
"""A full-screen dialog with a scrollable text area and an Okay button."""
|
||||
|
||||
@staticmethod
|
||||
def ask(body_text: str, font_size: int = BaseDialog.FONT_SIZE, scroll_to_bottom: bool = False, okay_text: str = "Okay") -> None:
|
||||
"""Displays a full-screen Okay dialog."""
|
||||
okay_pressed = False
|
||||
|
||||
dialog = OkayDialog(body_text, font_size, scroll_to_bottom)
|
||||
|
||||
def render_dialog():
|
||||
nonlocal okay_pressed # Allow modification of the okay_pressed variable
|
||||
|
||||
dialog.render_text_area()
|
||||
|
||||
# Calculate the available height for the button area
|
||||
button_area_height = gui_app.height - dialog.textarea_rect.height - dialog.textarea_rect.y
|
||||
|
||||
# Button position (centered horizontally, vertically centered in the button area)
|
||||
button_x = (gui_app.width - BaseDialog.BUTTON_WIDTH) / 2
|
||||
button_y = dialog.textarea_rect.y + dialog.textarea_rect.height + (button_area_height - BaseDialog.BUTTON_HEIGHT) / 2
|
||||
|
||||
# Okay Button
|
||||
if gui_button(rl.Rectangle(button_x, button_y, BaseDialog.BUTTON_WIDTH, BaseDialog.BUTTON_HEIGHT), okay_text):
|
||||
okay_pressed = True
|
||||
|
||||
# Main loop
|
||||
while not okay_pressed and not rl.window_should_close():
|
||||
rl.begin_drawing()
|
||||
rl.clear_background(rl.BLACK)
|
||||
|
||||
render_dialog()
|
||||
|
||||
rl.end_drawing()
|
||||
|
||||
|
||||
class YesNoDialog(BaseDialog):
|
||||
"""A full-screen dialog with a scrollable text area and Yes/No buttons."""
|
||||
|
||||
@staticmethod
|
||||
def ask(body_text: str, font_size: int = BaseDialog.FONT_SIZE, scroll_to_bottom: bool = False, yes_text: str = "Yes", no_text: str = "No") -> bool | None:
|
||||
"""Displays a full-screen Yes/No dialog and returns a boolean based on the user's choice."""
|
||||
result = None # Use None to indicate the dialog is still active
|
||||
|
||||
dialog = YesNoDialog(body_text, font_size, scroll_to_bottom)
|
||||
|
||||
def render_dialog():
|
||||
nonlocal result # Allow modification of the result variable
|
||||
|
||||
dialog.render_text_area()
|
||||
|
||||
button_top = gui_app.height - BaseDialog.BORDER_SIZE - BaseDialog.BUTTON_HEIGHT
|
||||
no_button_x = BaseDialog.BORDER_SIZE
|
||||
yes_button_x = gui_app.width - BaseDialog.BORDER_SIZE - BaseDialog.BUTTON_WIDTH
|
||||
|
||||
no_button_rect = rl.Rectangle(no_button_x, button_top, BaseDialog.BUTTON_WIDTH, BaseDialog.BUTTON_HEIGHT)
|
||||
yes_button_rect = rl.Rectangle(yes_button_x, button_top, BaseDialog.BUTTON_WIDTH, BaseDialog.BUTTON_HEIGHT)
|
||||
|
||||
if draw_custom_button(no_button_rect, no_text, rl.Color(100, 20, 20, 255)):
|
||||
result = False
|
||||
if draw_custom_button(yes_button_rect, yes_text, rl.Color(20, 100, 20, 255)):
|
||||
result = True
|
||||
|
||||
# Main loop
|
||||
while result is None and not rl.window_should_close():
|
||||
rl.begin_drawing()
|
||||
rl.clear_background(rl.BLACK)
|
||||
|
||||
render_dialog()
|
||||
|
||||
rl.end_drawing()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def draw_custom_button(rect: rl.Rectangle, text: str, color: rl.Color) -> bool:
|
||||
"""Draws a custom button with specified color and handles click detection."""
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
is_hovering = rl.check_collision_point_rec(mouse_pos, rect)
|
||||
is_pressed = is_hovering and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
||||
result = False
|
||||
|
||||
rl.draw_rectangle_rec(rect, color)
|
||||
|
||||
# Draw button text (centered)
|
||||
font = gui_app.font()
|
||||
text_size = rl.measure_text_ex(font, text, DEFAULT_BUTTON_FONT_SIZE, 0)
|
||||
text_x = rect.x + (rect.width - text_size.x) / 2
|
||||
text_y = rect.y + (rect.height - text_size.y) / 2
|
||||
rl.draw_text_ex(font, text, rl.Vector2(text_x, text_y), DEFAULT_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||
|
||||
if is_pressed:
|
||||
result = True
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,119 @@
|
||||
# tsk/ui/header.py
|
||||
from typing import Optional
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from tsk.common.key_file_manager import KeyFileManager
|
||||
from tsk.ui.layout import Theme # Import Theme
|
||||
|
||||
|
||||
class Header:
|
||||
"""Renders the top navigation bar with title and key status."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initializes the Header."""
|
||||
self.key_manager = KeyFileManager()
|
||||
|
||||
def _draw_title(self, rect: rl.Rectangle, current_menu: int) -> None:
|
||||
"""Draws the title strip."""
|
||||
title_text_color = Theme.brighten_color(Theme.title_bg_color, Theme.brighten_amount)
|
||||
|
||||
menu_name = Theme.menu_names.get(current_menu, "Unknown Menu")
|
||||
full_title_text = "TSK Manager: " + menu_name
|
||||
full_title_text_size = rl.measure_text_ex(gui_app.font(), full_title_text, Theme.title_font_size, 1.0)
|
||||
|
||||
title_text_prefix_size = rl.measure_text_ex(gui_app.font(), "TSK Manager: ", Theme.title_font_size, 1.0)
|
||||
menu_name_x = gui_app.width * Theme.title_prefix_x_offset_percent + title_text_prefix_size.x + Theme.title_x_offset
|
||||
text_y = (self._calculate_height() - full_title_text_size.y) / 2
|
||||
|
||||
rl.draw_text_ex(gui_app.font(), "TSK Manager: ", rl.Vector2(gui_app.width * Theme.title_prefix_x_offset_percent + Theme.title_x_offset, text_y),
|
||||
Theme.title_font_size, 1.0, title_text_color)
|
||||
|
||||
rl.draw_text_ex(gui_app.font(), menu_name, rl.Vector2(menu_name_x, text_y), Theme.title_font_size, 1.0, title_text_color)
|
||||
|
||||
def _draw_navigation_buttons(self, rect: rl.Rectangle, current_menu: int) -> Optional[int]:
|
||||
"""Draws the navigation buttons and handles button clicks."""
|
||||
nav_button_width = self._calculate_nav_button_width()
|
||||
|
||||
left_button_x = 0
|
||||
left_button_rect = rl.Rectangle(left_button_x, 0, nav_button_width, rect.height)
|
||||
right_button_x = gui_app.width - nav_button_width
|
||||
right_button_rect = rl.Rectangle(right_button_x, 0, nav_button_width, rect.height)
|
||||
|
||||
new_menu = None
|
||||
|
||||
def handle_button(button_rect: rl.Rectangle, text: str, target_menu: int) -> Optional[int]:
|
||||
"""Handles drawing, input, and logic for a single navigation button."""
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
is_hovering = rl.check_collision_point_rec(mouse_pos, button_rect)
|
||||
is_pressed = is_hovering and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
||||
|
||||
rl.draw_rectangle_rec(button_rect, Theme.button_color)
|
||||
|
||||
lines = text.splitlines()
|
||||
total_text_height = len(lines) * rl.measure_text_ex(gui_app.font(), "A", Theme.nav_button_font_size, 1.0).y
|
||||
start_y = rect.y + (rect.height - total_text_height) / 2
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
text_size = rl.measure_text_ex(gui_app.font(), line, Theme.nav_button_font_size, 1.0)
|
||||
text_x = button_rect.x + (button_rect.width - text_size.x) / 2
|
||||
text_y = start_y + i * text_size.y
|
||||
font_color = Theme.brighten_color(rl.Color(100, 100, 100, 255), Theme.brighten_amount) # Use the same color as other buttons
|
||||
rl.draw_text_ex(gui_app.font(), line, rl.Vector2(text_x, text_y), Theme.nav_button_font_size, 1.0, font_color)
|
||||
|
||||
if is_pressed:
|
||||
return target_menu
|
||||
return None
|
||||
if current_menu == Theme.menu_reboot:
|
||||
new_menu = handle_button(left_button_rect, Theme.nav_button_text_left, Theme.menu_tools)
|
||||
|
||||
# --- Handle Right Button ---
|
||||
if current_menu == Theme.menu_tools:
|
||||
right_menu = handle_button(right_button_rect, Theme.nav_button_text_right, Theme.menu_reboot)
|
||||
if new_menu is None:
|
||||
new_menu = right_menu
|
||||
|
||||
return new_menu
|
||||
|
||||
def _draw_key_status(self, rect: rl.Rectangle) -> None:
|
||||
"""Draws the key status label."""
|
||||
key_status_text = f"Key installed: {self.key_manager.installed_key}" if self.key_manager.installed_key else "Key not installed"
|
||||
status_text_x = (gui_app.width - rl.measure_text_ex(gui_app.font(), key_status_text, Theme.key_status_font_size, 1.0).x) / 2
|
||||
status_text_y = rect.y + (rect.height - rl.measure_text_ex(gui_app.font(), key_status_text, Theme.key_status_font_size, 1.0).y) / 2 # Use rect.y
|
||||
rl.draw_text_ex(gui_app.font(), key_status_text, rl.Vector2(status_text_x, status_text_y), Theme.key_status_font_size, 1.0, Theme.key_text_color)
|
||||
|
||||
def _calculate_height(self) -> float:
|
||||
"""Calculates the height of the header."""
|
||||
title_text_prefix = "TSK Manager: "
|
||||
return rl.measure_text_ex(gui_app.font(), title_text_prefix, Theme.title_font_size, 1.0).y * 1.5
|
||||
|
||||
def _calculate_nav_button_width(self) -> float:
|
||||
"""Calculates the width of the navigation buttons."""
|
||||
return max(rl.measure_text_ex(gui_app.font(), Theme.nav_button_text_left, Theme.nav_button_font_size, 1.0).x,
|
||||
rl.measure_text_ex(gui_app.font(), Theme.nav_button_text_right, Theme.nav_button_font_size, 1.0).x) + 10
|
||||
|
||||
def render(self, rect: rl.Rectangle, current_menu: int) -> Optional[int]:
|
||||
"""Renders the top strip with the title and menu navigation."""
|
||||
|
||||
title_height = self._calculate_height()
|
||||
key_status_height = Theme.key_status_font_size * 1.5 # Approximate height
|
||||
|
||||
# --- Draw Title Strip ---
|
||||
title_rect = rl.Rectangle(0, 0, gui_app.width, title_height)
|
||||
rl.draw_rectangle_rec(title_rect, Theme.title_bg_color)
|
||||
self._draw_title(rect, current_menu)
|
||||
|
||||
# --- Draw Key Status Strip ---
|
||||
key_status_y = title_height
|
||||
key_status_rect = rl.Rectangle(0, key_status_y, gui_app.width, key_status_height)
|
||||
rl.draw_rectangle_rec(key_status_rect, Theme.nav_bg_color) # Use nav_bg_color for key status
|
||||
self._draw_key_status(key_status_rect)
|
||||
|
||||
# --- Draw Navigation Buttons (Overlapping) ---
|
||||
nav_rect = rl.Rectangle(0, 0, gui_app.width, title_height + key_status_height) # Overlap both
|
||||
new_menu = self._draw_navigation_buttons(nav_rect, current_menu)
|
||||
|
||||
if new_menu is not None:
|
||||
return new_menu
|
||||
return None
|
||||
@@ -0,0 +1,67 @@
|
||||
# tsk/ui/layout.py
|
||||
import pyray as rl
|
||||
|
||||
class Theme:
|
||||
"""Defines the visual theme and constants for the TSK Manager."""
|
||||
|
||||
# --- Colors ---
|
||||
nav_bg_color = rl.Color(20, 20, 20, 255)
|
||||
key_text_color = rl.Color(255, 255, 255, 255)
|
||||
button_color = rl.Color(51, 51, 51, 255)
|
||||
title_bg_color = rl.Color(40, 40, 40, 255)
|
||||
nav_text_color = rl.Color(255, 255, 255, 255) # Added nav_text_color
|
||||
|
||||
# --- Font Sizes ---
|
||||
title_font_size = 90
|
||||
nav_button_font_size = 80
|
||||
key_status_font_size = 65
|
||||
|
||||
# --- Text ---
|
||||
nav_button_text_left = "< Tools\n< Menu"
|
||||
nav_button_text_right = "Reboot >\n Menu >"
|
||||
nav_button_text_to_tools = "< Tools\n< Menu"
|
||||
|
||||
# --- Layout ---
|
||||
title_prefix_x_offset_percent = 0.2
|
||||
title_x_offset = 180
|
||||
brighten_amount = 0.6
|
||||
status_update_interval = 1 # seconds
|
||||
|
||||
# --- Menu Identifiers ---
|
||||
menu_tools = 1
|
||||
menu_reboot = 2
|
||||
|
||||
menu_names = {
|
||||
menu_tools: "Tools Menu",
|
||||
menu_reboot: "Reboot Menu",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def brighten_color(color: rl.Color, amount: float) -> rl.Color:
|
||||
"""Brightens a color by a given amount (0.0 to 1.0)."""
|
||||
r = int(min(color.r + (255 - color.r) * amount, 255))
|
||||
g = int(min(color.g + (255 - color.g) * amount, 255))
|
||||
b = int(min(color.b + (255 - color.b) * amount, 255))
|
||||
return rl.Color(r, g, b, color.a)
|
||||
|
||||
|
||||
class Layout:
|
||||
"""Provides layout calculation functions."""
|
||||
|
||||
@staticmethod
|
||||
def calculate_button_dimensions(rect_height: int, header_height: int) -> int:
|
||||
"""Calculates button height based on available screen space and desired spacing."""
|
||||
available_height = rect_height - header_height
|
||||
guide_button_height = 200
|
||||
num_spacers = 3 # Top, between buttons, and below guide button
|
||||
remaining_height = available_height - guide_button_height - (num_spacers * 80)
|
||||
button_height = remaining_height
|
||||
return int(button_height)
|
||||
|
||||
@staticmethod
|
||||
def calculate_button_positions(rect: rl.Rectangle, header_height: int, num_buttons: int) -> tuple:
|
||||
"""Calculates the starting positions for a row of buttons."""
|
||||
total_width = (num_buttons * 600) + ((num_buttons - 1) * 80)
|
||||
start_x = (rect.width - total_width) / 2 + rect.x
|
||||
start_y = rect.y + header_height + 80
|
||||
return start_x, start_y
|
||||
Reference in New Issue
Block a user