UI: Add Exit Offfroad button on the home screen (#872)

* exit offroad btn on offroad-home

* Remove alert for Always-Offroad.
Replaced with header "Always Offroad Active"

* refresh state when device wakes up

* cleanup

* cleaning

* Add pulsing glow effect to ExitOffroadButton

Introduced a QTimer-based mechanism to create a pulsing glow effect around the ExitOffroadButton. This enhances visual feedback by animating the outline with varying alpha transparency, improving UI responsiveness and aesthetics.

* move custom asset to sunnypilot

---------

Co-authored-by: DevTekVE <devtekve@gmail.com>
This commit is contained in:
Nayan
2025-06-07 09:03:13 -04:00
committed by GitHub
parent fa4c3a80b6
commit 8da0514955
12 changed files with 182 additions and 14 deletions
-1
View File
@@ -137,7 +137,6 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
{"Brightness", PERSISTENT | BACKUP},
{"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION},
{"OffroadMode", CLEAR_ON_MANAGER_START},
{"OffroadMode_Status", CLEAR_ON_MANAGER_START},
{"QuietMode", PERSISTENT | BACKUP},
// MADS params
-4
View File
@@ -45,10 +45,6 @@
"text": "sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.",
"severity": 0
},
"OffroadMode_Status": {
"text": "sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to \"Settings\" -> \"Device\" to exit Always Offroad mode.",
"severity": 1
},
"Offroad_OSMUpdateRequired": {
"text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1",
"severity": 0
+1 -1
View File
@@ -18,7 +18,7 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
main_layout->setContentsMargins(40, 40, 40, 40);
// top header
QHBoxLayout* header_layout = new QHBoxLayout();
header_layout = new QHBoxLayout();
header_layout->setContentsMargins(0, 0, 0, 0);
header_layout->setSpacing(16);
+4 -2
View File
@@ -39,11 +39,13 @@ public:
protected:
QHBoxLayout *home_layout;
QHBoxLayout *header_layout;
void showEvent(QShowEvent *event) override;
void refresh();
private:
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
void refresh();
Params params;
+2
View File
@@ -19,6 +19,7 @@ qt_src = [
"sunnypilot/qt/sidebar.cc",
"sunnypilot/qt/window.cc",
"sunnypilot/qt/home.cc",
"sunnypilot/qt/offroad/exit_offroad_button.cc",
"sunnypilot/qt/offroad/offroad_home.cc",
"sunnypilot/qt/offroad/settings/device_panel.cc",
"sunnypilot/qt/offroad/settings/lateral_panel.cc",
@@ -80,6 +81,7 @@ brand_settings_qt_src = [
"sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc",
]
sp_widgets_src = widgets_src + network_src
sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src + osm_panel_qt_src
sp_qt_util = qt_util
@@ -0,0 +1,100 @@
#include <QDebug>
#include <QHBoxLayout>
#include <QPainter>
#include <QPainterPath>
#include <QStyle>
#include "selfdrive/ui/ui.h"
#include "selfdrive/ui/qt/widgets/input.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h"
ExitOffroadButton::ExitOffroadButton(QWidget *parent) : QPushButton(parent), glowTimer(new QTimer(this)) {
setMouseTracking(true);
connect(glowTimer, &QTimer::timeout, this, [this]() {
// Pulse alpha up and down
glowAlpha += glowDelta;
if (glowAlpha > 220 || glowAlpha < 10) {
glowDelta *= -1;
}
update(); // trigger repaint
});
glowTimer->start(45);
pixmap = QPixmap("../../sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png").scaledToWidth(img_width, Qt::SmoothTransformation);
// go to toggles and expand experimental mode description
connect(this, &QPushButton::clicked, [=]() {
if (ConfirmationDialog::confirm(tr("Are you sure you want to exit Always Offroad mode?"), tr("Confirm"), this)) {
params.remove("OffroadMode");
}
});
setFixedHeight(125);
QHBoxLayout *main_layout = new QHBoxLayout;
main_layout->setContentsMargins(horizontal_padding, 0, horizontal_padding, 0);
mode_label = new QLabel(tr("EXIT ALWAYS OFFROAD MODE"));
mode_icon = new QLabel;
mode_icon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
mode_icon->setPixmap(pixmap);
main_layout->addWidget(mode_label, 1, Qt::AlignLeft);
main_layout->addWidget(mode_icon, 0, Qt::AlignRight);
setLayout(main_layout);
setStyleSheet(R"(
QPushButton {
border: none;
}
QLabel {
font-size: 45px;
font-weight: 300;
text-align: left;
font-family: JetBrainsMono;
color: #000000;
}
)");
}
void drawPulsingGlowOverlay(QPainter &p, QPainterPath path, int glowAlpha) {
// Draw pulsing glow effect clipped to button area
p.save();
p.setClipPath(path);
p.setCompositionMode(QPainter::CompositionMode_HardLight);
const QColor animatedGlowColor(255, 255, 255, std::min(255, glowAlpha));
QPen glowPen(animatedGlowColor, 8);
glowPen.setJoinStyle(Qt::RoundJoin);
p.setPen(glowPen);
p.drawPath(path);
p.restore();
}
void ExitOffroadButton::paintEvent(QPaintEvent *event) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
path.addRoundedRect(rect(), 10, 10);
// gradient
bool pressed = isDown();
QLinearGradient gradient(rect().left(), 0, rect().right(), 0);
gradient.setColorAt(0, QColor(35, 149, 255, pressed ? 0xcc : 0xff));
gradient.setColorAt(0.3, QColor(35, 149, 255, pressed ? 0xcc : 0xff));
gradient.setColorAt(1, QColor(20, 255, 171, pressed ? 0xcc : 0xff));
p.fillPath(path, gradient);
drawPulsingGlowOverlay(p, path, glowAlpha);
// vertical line
p.setPen(QPen(QColor(0, 0, 0, 0x4d), 3, Qt::SolidLine));
int line_x = rect().right() - img_width - (2 * horizontal_padding);
p.drawLine(line_x, rect().bottom(), line_x, rect().top());
}
@@ -0,0 +1,29 @@
#pragma once
#include <QLabel>
#include <QPushButton>
#include "common/params.h"
class ExitOffroadButton : public QPushButton {
Q_OBJECT
private:
QTimer *glowTimer;
int glowAlpha = 100; // Current alpha of glow
int glowDelta = 10; // Change per tick
public:
explicit ExitOffroadButton(QWidget* parent = 0);
Params params;
bool offroad_mode;
int img_width = 100;
int horizontal_padding = 30;
QPixmap pixmap;
QLabel *mode_label;
QLabel *mode_icon;
protected:
void paintEvent(QPaintEvent *event) override;
};
@@ -5,16 +5,45 @@
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h"
#include <QStackedWidget>
#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h"
OffroadHomeSP::OffroadHomeSP(QWidget *parent) : OffroadHome(parent) {
QStackedWidget *left_widget = new QStackedWidget(this);
left_widget->addWidget(new DriveStats(this));
QFrame *left_widget = new QFrame(this);
QVBoxLayout *left_layout = new QVBoxLayout(left_widget);
left_layout->setContentsMargins(0, 0, 0, 0);
left_layout->setSpacing(30);
btn_exit_offroad = new ExitOffroadButton(this);
QObject::connect(btn_exit_offroad, &ExitOffroadButton::clicked, [=]() {
refreshOffroadStatus();
});
left_layout->addWidget(btn_exit_offroad);
left_layout->addWidget(new DriveStats(this));
left_widget->setStyleSheet("border-radius: 10px;");
home_layout->insertWidget(0, left_widget);
offroad_notif = new QPushButton(tr("ALWAYS OFFROAD ACTIVE"));
offroad_notif->setVisible(false);
offroad_notif->setStyleSheet("background-color: #E22C2C;");
header_layout->insertWidget(0, offroad_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
QObject::connect(deviceSP(), &DeviceSP::displayPowerChanged, this, &OffroadHomeSP::refreshOffroadStatus);
}
void OffroadHomeSP::showEvent(QShowEvent *event) {
refreshOffroadStatus();
OffroadHome::showEvent(event);
}
void OffroadHomeSP::refreshOffroadStatus() {
bool is_offroad = params.getBool("OffroadMode");
btn_exit_offroad->setVisible(is_offroad);
offroad_notif->setVisible(is_offroad);
OffroadHome::refresh();
}
@@ -8,10 +8,19 @@
#pragma once
#include "selfdrive/ui/qt/offroad/offroad_home.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h"
class OffroadHomeSP : public OffroadHome {
Q_OBJECT
public:
explicit OffroadHomeSP(QWidget *parent = 0);
private:
ExitOffroadButton *btn_exit_offroad;
QPushButton *offroad_notif;
Params params;
void showEvent(QShowEvent *event) override;
void refreshOffroadStatus();
};
@@ -34,7 +34,7 @@ DriveStats::DriveStats(QWidget* parent) : QFrame(parent) {
int row = 0;
grid_layout->addWidget(newLabel(title, "title"), row++, 0, 1, 3);
grid_layout->addItem(new QSpacerItem(0, 50), row++, 0, 1, 1);
grid_layout->addItem(new QSpacerItem(0, 30), row++, 0, 1, 1);
grid_layout->addWidget(labels.routes = newLabel("0", "number"), row, 0, Qt::AlignLeft);
grid_layout->addWidget(labels.distance = newLabel("0", "number"), row, 1, Qt::AlignLeft);
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d44f71f7603b106986fff7835f35c4c18c4c619a29e6292f8626ae3eedd85e57
size 7811
-1
View File
@@ -323,7 +323,6 @@ def hardware_thread(end_event, hw_queue) -> None:
offroad_mode = params.get_bool("OffroadMode")
startup_conditions["not_always_offroad"] = not offroad_mode
onroad_conditions["not_always_offroad"] = not offroad_mode
set_offroad_alert("OffroadMode_Status", offroad_mode)
# if the temperature enters the danger zone, go offroad to cool down
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger