mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-02-18 18:53:55 +08:00
ui: implement home layout with fully functional offroad alerts (#35468)
implement home layout with offroad alerts
This commit is contained in:
@@ -1,17 +1,212 @@
|
||||
import time
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.label import gui_text_box
|
||||
from collections.abc import Callable
|
||||
from enum import IntEnum
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.label import gui_label
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR
|
||||
|
||||
|
||||
HEADER_HEIGHT = 80
|
||||
HEAD_BUTTON_FONT_SIZE = 40
|
||||
CONTENT_MARGIN = 40
|
||||
SPACING = 25
|
||||
RIGHT_COLUMN_WIDTH = 750
|
||||
REFRESH_INTERVAL = 10.0
|
||||
|
||||
PRIME_BG_COLOR = rl.Color(51, 51, 51, 255)
|
||||
|
||||
|
||||
class HomeLayoutState(IntEnum):
|
||||
HOME = 0
|
||||
UPDATE = 1
|
||||
ALERTS = 2
|
||||
|
||||
|
||||
class HomeLayout:
|
||||
def __init__(self):
|
||||
pass
|
||||
self.params = Params()
|
||||
|
||||
self.update_alert = UpdateAlert()
|
||||
self.offroad_alert = OffroadAlert()
|
||||
|
||||
self.current_state = HomeLayoutState.HOME
|
||||
self.last_refresh = 0
|
||||
self.settings_callback: callable | None = None
|
||||
|
||||
self.update_available = False
|
||||
self.alert_count = 0
|
||||
|
||||
self.header_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self.content_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self.left_column_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self.right_column_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
self.update_notif_rect = rl.Rectangle(0, 0, 200, HEADER_HEIGHT - 10)
|
||||
self.alert_notif_rect = rl.Rectangle(0, 0, 220, HEADER_HEIGHT - 10)
|
||||
|
||||
self._setup_callbacks()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
|
||||
self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
|
||||
|
||||
def set_settings_callback(self, callback: Callable):
|
||||
self.settings_callback = callback
|
||||
|
||||
def _set_state(self, state: HomeLayoutState):
|
||||
self.current_state = state
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
gui_text_box(
|
||||
rect,
|
||||
"Demo Home Layout",
|
||||
font_size=170,
|
||||
color=rl.WHITE,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||
self._update_layout_rects(rect)
|
||||
|
||||
current_time = time.time()
|
||||
if current_time - self.last_refresh >= REFRESH_INTERVAL:
|
||||
self._refresh()
|
||||
self.last_refresh = current_time
|
||||
|
||||
self._handle_input()
|
||||
self._render_header()
|
||||
|
||||
# Render content based on current state
|
||||
if self.current_state == HomeLayoutState.HOME:
|
||||
self._render_home_content()
|
||||
elif self.current_state == HomeLayoutState.UPDATE:
|
||||
self._render_update_view()
|
||||
elif self.current_state == HomeLayoutState.ALERTS:
|
||||
self._render_alerts_view()
|
||||
|
||||
def _update_layout_rects(self, rect: rl.Rectangle):
|
||||
self.header_rect = rl.Rectangle(
|
||||
rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
|
||||
)
|
||||
|
||||
content_y = rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING
|
||||
content_height = rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN
|
||||
|
||||
self.content_rect = rl.Rectangle(
|
||||
rect.x + CONTENT_MARGIN, content_y, rect.width - 2 * CONTENT_MARGIN, content_height
|
||||
)
|
||||
|
||||
left_width = self.content_rect.width - RIGHT_COLUMN_WIDTH - SPACING
|
||||
|
||||
self.left_column_rect = rl.Rectangle(self.content_rect.x, self.content_rect.y, left_width, self.content_rect.height)
|
||||
|
||||
self.right_column_rect = rl.Rectangle(
|
||||
self.content_rect.x + left_width + SPACING, self.content_rect.y, RIGHT_COLUMN_WIDTH, self.content_rect.height
|
||||
)
|
||||
|
||||
self.update_notif_rect.x = self.header_rect.x
|
||||
self.update_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2
|
||||
|
||||
notif_x = self.header_rect.x + (220 if self.update_available else 0)
|
||||
self.alert_notif_rect.x = notif_x
|
||||
self.alert_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2
|
||||
|
||||
def _handle_input(self):
|
||||
if not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
return
|
||||
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
|
||||
if self.update_available and rl.check_collision_point_rec(mouse_pos, self.update_notif_rect):
|
||||
self._set_state(HomeLayoutState.UPDATE)
|
||||
return
|
||||
|
||||
if self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect):
|
||||
self._set_state(HomeLayoutState.ALERTS)
|
||||
return
|
||||
|
||||
# Content area input handling
|
||||
if self.current_state == HomeLayoutState.UPDATE:
|
||||
self.update_alert.handle_input(mouse_pos, True)
|
||||
elif self.current_state == HomeLayoutState.ALERTS:
|
||||
self.offroad_alert.handle_input(mouse_pos, True)
|
||||
|
||||
def _render_header(self):
|
||||
font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
# Update notification button
|
||||
if self.update_available:
|
||||
# Highlight if currently viewing updates
|
||||
highlight_color = rl.Color(255, 140, 40, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(255, 102, 0, 255)
|
||||
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
|
||||
|
||||
text = "UPDATE"
|
||||
text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x
|
||||
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2
|
||||
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
|
||||
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||
|
||||
# Alert notification button
|
||||
if self.alert_count > 0:
|
||||
# Highlight if currently viewing alerts
|
||||
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
|
||||
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
|
||||
|
||||
alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}"
|
||||
text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x
|
||||
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2
|
||||
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
|
||||
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||
|
||||
# Version text (right aligned)
|
||||
version_text = self._get_version_text()
|
||||
text_width = measure_text_cached(gui_app.font(FontWeight.NORMAL), version_text, 48).x
|
||||
version_x = self.header_rect.x + self.header_rect.width - text_width
|
||||
version_y = self.header_rect.y + (self.header_rect.height - 48) // 2
|
||||
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), version_text, rl.Vector2(int(version_x), int(version_y)), 48, 0, DEFAULT_TEXT_COLOR)
|
||||
|
||||
def _render_home_content(self):
|
||||
self._render_left_column()
|
||||
self._render_right_column()
|
||||
|
||||
def _render_update_view(self):
|
||||
self.update_alert.render(self.content_rect)
|
||||
|
||||
def _render_alerts_view(self):
|
||||
self.offroad_alert.render(self.content_rect)
|
||||
|
||||
def _render_left_column(self):
|
||||
rl.draw_rectangle_rounded(self.left_column_rect, 0.02, 10, PRIME_BG_COLOR)
|
||||
gui_label(self.left_column_rect, "Prime Widget", 48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
|
||||
def _render_right_column(self):
|
||||
widget_height = (self.right_column_rect.height - SPACING) // 2
|
||||
|
||||
exp_rect = rl.Rectangle(
|
||||
self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, widget_height
|
||||
)
|
||||
rl.draw_rectangle_rounded(exp_rect, 0.02, 10, PRIME_BG_COLOR)
|
||||
gui_label(exp_rect, "Experimental Mode", 36, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
|
||||
setup_rect = rl.Rectangle(
|
||||
self.right_column_rect.x,
|
||||
self.right_column_rect.y + widget_height + SPACING,
|
||||
self.right_column_rect.width,
|
||||
widget_height,
|
||||
)
|
||||
rl.draw_rectangle_rounded(setup_rect, 0.02, 10, PRIME_BG_COLOR)
|
||||
gui_label(setup_rect, "Setup", 36, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
|
||||
def _refresh(self):
|
||||
self.update_available = self.update_alert.refresh()
|
||||
self.alert_count = self.offroad_alert.refresh()
|
||||
self._update_state_priority(self.update_available, self.alert_count > 0)
|
||||
|
||||
def _update_state_priority(self, update_available: bool, alerts_present: bool):
|
||||
current_state = self.current_state
|
||||
|
||||
if not update_available and not alerts_present:
|
||||
self.current_state = HomeLayoutState.HOME
|
||||
elif update_available and (current_state == HomeLayoutState.HOME or (not alerts_present and current_state == HomeLayoutState.ALERTS)):
|
||||
self.current_state = HomeLayoutState.UPDATE
|
||||
elif alerts_present and (current_state == HomeLayoutState.HOME or (not update_available and current_state == HomeLayoutState.UPDATE)):
|
||||
self.current_state = HomeLayoutState.ALERTS
|
||||
|
||||
def _get_version_text(self) -> str:
|
||||
brand = "openpilot"
|
||||
description = self.params.get("UpdaterCurrentDescription", encoding='utf-8')
|
||||
return f"{brand} {description}" if description else brand
|
||||
|
||||
328
selfdrive/ui/widgets/offroad_alerts.py
Normal file
328
selfdrive/ui/widgets/offroad_alerts.py
Normal file
@@ -0,0 +1,328 @@
|
||||
import json
|
||||
import pyray as rl
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
|
||||
class AlertColors:
|
||||
HIGH_SEVERITY = rl.Color(226, 44, 44, 255)
|
||||
LOW_SEVERITY = rl.Color(41, 41, 41, 255)
|
||||
BACKGROUND = rl.Color(57, 57, 57, 255)
|
||||
BUTTON = rl.WHITE
|
||||
BUTTON_TEXT = rl.BLACK
|
||||
SNOOZE_BG = rl.Color(79, 79, 79, 255)
|
||||
TEXT = rl.WHITE
|
||||
|
||||
|
||||
class AlertConstants:
|
||||
BUTTON_SIZE = (400, 125)
|
||||
SNOOZE_BUTTON_SIZE = (550, 125)
|
||||
REBOOT_BUTTON_SIZE = (600, 125)
|
||||
MARGIN = 50
|
||||
SPACING = 30
|
||||
FONT_SIZE = 48
|
||||
BORDER_RADIUS = 30
|
||||
ALERT_HEIGHT = 120
|
||||
ALERT_SPACING = 20
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlertData:
|
||||
key: str
|
||||
text: str
|
||||
severity: int
|
||||
visible: bool = False
|
||||
|
||||
|
||||
class AbstractAlert(ABC):
|
||||
def __init__(self, has_reboot_btn: bool = False):
|
||||
self.params = Params()
|
||||
self.has_reboot_btn = has_reboot_btn
|
||||
self.dismiss_callback: Callable | None = None
|
||||
|
||||
self.dismiss_btn_rect = rl.Rectangle(0, 0, *AlertConstants.BUTTON_SIZE)
|
||||
self.snooze_btn_rect = rl.Rectangle(0, 0, *AlertConstants.SNOOZE_BUTTON_SIZE)
|
||||
self.reboot_btn_rect = rl.Rectangle(0, 0, *AlertConstants.REBOOT_BUTTON_SIZE)
|
||||
|
||||
self.snooze_visible = False
|
||||
self.content_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self.scroll_panel_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self.scroll_panel = GuiScrollPanel()
|
||||
|
||||
def set_dismiss_callback(self, callback: Callable):
|
||||
self.dismiss_callback = callback
|
||||
|
||||
@abstractmethod
|
||||
def refresh(self) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_content_height(self) -> float:
|
||||
pass
|
||||
|
||||
def handle_input(self, mouse_pos: rl.Vector2, mouse_clicked: bool) -> bool:
|
||||
# TODO: fix scroll_panel.is_click_valid()
|
||||
if not mouse_clicked:
|
||||
return False
|
||||
|
||||
if rl.check_collision_point_rec(mouse_pos, self.dismiss_btn_rect):
|
||||
if self.dismiss_callback:
|
||||
self.dismiss_callback()
|
||||
return True
|
||||
|
||||
if self.snooze_visible and rl.check_collision_point_rec(mouse_pos, self.snooze_btn_rect):
|
||||
self.params.put_bool("SnoozeUpdate", True)
|
||||
if self.dismiss_callback:
|
||||
self.dismiss_callback()
|
||||
return True
|
||||
|
||||
if self.has_reboot_btn and rl.check_collision_point_rec(mouse_pos, self.reboot_btn_rect):
|
||||
HARDWARE.reboot()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.width, 10, AlertColors.BACKGROUND)
|
||||
|
||||
footer_height = AlertConstants.BUTTON_SIZE[1] + AlertConstants.SPACING
|
||||
content_height = rect.height - 2 * AlertConstants.MARGIN - footer_height
|
||||
|
||||
self.content_rect = rl.Rectangle(
|
||||
rect.x + AlertConstants.MARGIN,
|
||||
rect.y + AlertConstants.MARGIN,
|
||||
rect.width - 2 * AlertConstants.MARGIN,
|
||||
content_height,
|
||||
)
|
||||
self.scroll_panel_rect = rl.Rectangle(
|
||||
self.content_rect.x, self.content_rect.y, self.content_rect.width, self.content_rect.height
|
||||
)
|
||||
|
||||
self._render_scrollable_content()
|
||||
self._render_footer(rect)
|
||||
|
||||
def _render_scrollable_content(self):
|
||||
content_total_height = self.get_content_height()
|
||||
content_bounds = rl.Rectangle(0, 0, self.scroll_panel_rect.width, content_total_height)
|
||||
scroll_offset = self.scroll_panel.handle_scroll(self.scroll_panel_rect, content_bounds)
|
||||
|
||||
rl.begin_scissor_mode(
|
||||
int(self.scroll_panel_rect.x),
|
||||
int(self.scroll_panel_rect.y),
|
||||
int(self.scroll_panel_rect.width),
|
||||
int(self.scroll_panel_rect.height),
|
||||
)
|
||||
|
||||
content_rect_with_scroll = rl.Rectangle(
|
||||
self.scroll_panel_rect.x,
|
||||
self.scroll_panel_rect.y + scroll_offset.y,
|
||||
self.scroll_panel_rect.width,
|
||||
content_total_height,
|
||||
)
|
||||
|
||||
self._render_content(content_rect_with_scroll)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
@abstractmethod
|
||||
def _render_content(self, content_rect: rl.Rectangle):
|
||||
pass
|
||||
|
||||
def _render_footer(self, rect: rl.Rectangle):
|
||||
footer_y = rect.y + rect.height - AlertConstants.MARGIN - AlertConstants.BUTTON_SIZE[1]
|
||||
font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
self.dismiss_btn_rect.x = rect.x + AlertConstants.MARGIN
|
||||
self.dismiss_btn_rect.y = footer_y
|
||||
rl.draw_rectangle_rounded(self.dismiss_btn_rect, 0.3, 10, AlertColors.BUTTON)
|
||||
|
||||
text = "Close"
|
||||
text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x
|
||||
text_x = self.dismiss_btn_rect.x + (AlertConstants.BUTTON_SIZE[0] - text_width) // 2
|
||||
text_y = self.dismiss_btn_rect.y + (AlertConstants.BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2
|
||||
rl.draw_text_ex(
|
||||
font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.BUTTON_TEXT
|
||||
)
|
||||
|
||||
if self.snooze_visible:
|
||||
self.snooze_btn_rect.x = rect.x + rect.width - AlertConstants.MARGIN - AlertConstants.SNOOZE_BUTTON_SIZE[0]
|
||||
self.snooze_btn_rect.y = footer_y
|
||||
rl.draw_rectangle_rounded(self.snooze_btn_rect, 0.3, 10, AlertColors.SNOOZE_BG)
|
||||
|
||||
text = "Snooze Update"
|
||||
text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x
|
||||
text_x = self.snooze_btn_rect.x + (AlertConstants.SNOOZE_BUTTON_SIZE[0] - text_width) // 2
|
||||
text_y = self.snooze_btn_rect.y + (AlertConstants.SNOOZE_BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2
|
||||
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.TEXT)
|
||||
|
||||
elif self.has_reboot_btn:
|
||||
self.reboot_btn_rect.x = rect.x + rect.width - AlertConstants.MARGIN - AlertConstants.REBOOT_BUTTON_SIZE[0]
|
||||
self.reboot_btn_rect.y = footer_y
|
||||
rl.draw_rectangle_rounded(self.reboot_btn_rect, 0.3, 10, AlertColors.BUTTON)
|
||||
|
||||
text = "Reboot and Update"
|
||||
text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x
|
||||
text_x = self.reboot_btn_rect.x + (AlertConstants.REBOOT_BUTTON_SIZE[0] - text_width) // 2
|
||||
text_y = self.reboot_btn_rect.y + (AlertConstants.REBOOT_BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2
|
||||
rl.draw_text_ex(
|
||||
font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.BUTTON_TEXT
|
||||
)
|
||||
|
||||
|
||||
class OffroadAlert(AbstractAlert):
|
||||
def __init__(self):
|
||||
super().__init__(has_reboot_btn=False)
|
||||
self.sorted_alerts: list[AlertData] = []
|
||||
|
||||
def refresh(self):
|
||||
if not self.sorted_alerts:
|
||||
self._build_alerts()
|
||||
|
||||
active_count = 0
|
||||
connectivity_needed = False
|
||||
|
||||
for alert_data in self.sorted_alerts:
|
||||
text = ""
|
||||
bytes_data = self.params.get(alert_data.key)
|
||||
|
||||
if bytes_data:
|
||||
try:
|
||||
alert_json = json.loads(bytes_data)
|
||||
text = alert_json.get("text", "").replace("{}", alert_json.get("extra", ""))
|
||||
except json.JSONDecodeError:
|
||||
text = ""
|
||||
|
||||
alert_data.text = text
|
||||
alert_data.visible = bool(text)
|
||||
|
||||
if alert_data.visible:
|
||||
active_count += 1
|
||||
|
||||
if alert_data.key == "Offroad_ConnectivityNeeded" and alert_data.visible:
|
||||
connectivity_needed = True
|
||||
|
||||
self.snooze_visible = connectivity_needed
|
||||
return active_count
|
||||
|
||||
def get_content_height(self) -> float:
|
||||
if not self.sorted_alerts:
|
||||
return 0
|
||||
|
||||
total_height = 20
|
||||
font = gui_app.font(FontWeight.NORMAL)
|
||||
|
||||
for alert_data in self.sorted_alerts:
|
||||
if not alert_data.visible:
|
||||
continue
|
||||
|
||||
text_width = int(self.content_rect.width - 90)
|
||||
wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width)
|
||||
line_count = len(wrapped_lines)
|
||||
text_height = line_count * (AlertConstants.FONT_SIZE + 5)
|
||||
alert_item_height = max(text_height + 40, AlertConstants.ALERT_HEIGHT)
|
||||
total_height += alert_item_height + AlertConstants.ALERT_SPACING
|
||||
|
||||
if total_height > 20:
|
||||
total_height = total_height - AlertConstants.ALERT_SPACING + 20
|
||||
|
||||
return total_height
|
||||
|
||||
def _build_alerts(self):
|
||||
self.sorted_alerts = []
|
||||
try:
|
||||
with open("../selfdrived/alerts_offroad.json", "rb") as f:
|
||||
alerts_config = json.load(f)
|
||||
for key, config in sorted(alerts_config.items(), key=lambda x: x[1].get("severity", 0), reverse=True):
|
||||
severity = config.get("severity", 0)
|
||||
alert_data = AlertData(key=key, text="", severity=severity)
|
||||
self.sorted_alerts.append(alert_data)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
pass
|
||||
|
||||
def _render_content(self, content_rect: rl.Rectangle):
|
||||
y_offset = 20
|
||||
font = gui_app.font(FontWeight.NORMAL)
|
||||
|
||||
for alert_data in self.sorted_alerts:
|
||||
if not alert_data.visible:
|
||||
continue
|
||||
|
||||
bg_color = AlertColors.HIGH_SEVERITY if alert_data.severity > 0 else AlertColors.LOW_SEVERITY
|
||||
text_width = int(content_rect.width - 90)
|
||||
wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width)
|
||||
line_count = len(wrapped_lines)
|
||||
text_height = line_count * (AlertConstants.FONT_SIZE + 5)
|
||||
alert_item_height = max(text_height + 40, AlertConstants.ALERT_HEIGHT)
|
||||
|
||||
alert_rect = rl.Rectangle(
|
||||
content_rect.x + 10,
|
||||
content_rect.y + y_offset,
|
||||
content_rect.width - 30,
|
||||
alert_item_height,
|
||||
)
|
||||
|
||||
rl.draw_rectangle_rounded(alert_rect, 0.2, 10, bg_color)
|
||||
|
||||
text_x = alert_rect.x + 30
|
||||
text_y = alert_rect.y + 20
|
||||
|
||||
for i, line in enumerate(wrapped_lines):
|
||||
rl.draw_text_ex(
|
||||
font,
|
||||
line,
|
||||
rl.Vector2(text_x, text_y + i * (AlertConstants.FONT_SIZE + 5)),
|
||||
AlertConstants.FONT_SIZE,
|
||||
0,
|
||||
AlertColors.TEXT,
|
||||
)
|
||||
|
||||
y_offset += alert_item_height + AlertConstants.ALERT_SPACING
|
||||
|
||||
|
||||
class UpdateAlert(AbstractAlert):
|
||||
def __init__(self):
|
||||
super().__init__(has_reboot_btn=True)
|
||||
self.release_notes = ""
|
||||
self._wrapped_release_notes = ""
|
||||
self._cached_content_height: float = 0.0
|
||||
|
||||
def refresh(self) -> bool:
|
||||
update_available: bool = self.params.get_bool("UpdateAvailable")
|
||||
if update_available:
|
||||
self.release_notes = self.params.get("UpdaterNewReleaseNotes", encoding='utf-8')
|
||||
self._cached_content_height = 0
|
||||
|
||||
return update_available
|
||||
|
||||
def get_content_height(self) -> float:
|
||||
if not self.release_notes:
|
||||
return 100
|
||||
|
||||
if self._cached_content_height == 0:
|
||||
self._wrapped_release_notes = self.release_notes
|
||||
size = measure_text_cached(gui_app.font(FontWeight.NORMAL), self._wrapped_release_notes, AlertConstants.FONT_SIZE)
|
||||
self._cached_content_height = max(size.y + 60, 100)
|
||||
|
||||
return self._cached_content_height
|
||||
|
||||
def _render_content(self, content_rect: rl.Rectangle):
|
||||
if self.release_notes:
|
||||
rl.draw_text_ex(
|
||||
gui_app.font(FontWeight.NORMAL),
|
||||
self._wrapped_release_notes,
|
||||
rl.Vector2(content_rect.x + 30, content_rect.y + 30),
|
||||
AlertConstants.FONT_SIZE,
|
||||
0.0,
|
||||
AlertColors.TEXT,
|
||||
)
|
||||
else:
|
||||
no_notes_text = "No release notes available."
|
||||
text_width = rl.measure_text(no_notes_text, AlertConstants.FONT_SIZE)
|
||||
text_x = content_rect.x + (content_rect.width - text_width) // 2
|
||||
text_y = content_rect.y + 50
|
||||
rl.draw_text(no_notes_text, int(text_x), int(text_y), AlertConstants.FONT_SIZE, AlertColors.TEXT)
|
||||
Reference in New Issue
Block a user