diff --git a/.gitignore b/.gitignore
index 4562e47817..a58a46fef9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -103,3 +103,8 @@ Pipfile
# Ignore all local history of files
.history
.ionide
+
+### JetBrains ###
+!.idea/customTargets.xml
+!.idea/tools/*
+!.run/*
diff --git a/.idea/customTargets.xml b/.idea/customTargets.xml
new file mode 100644
index 0000000000..772000ec3a
--- /dev/null
+++ b/.idea/customTargets.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/tools/External Tools.xml b/.idea/tools/External Tools.xml
new file mode 100644
index 0000000000..fa2375e2ee
--- /dev/null
+++ b/.idea/tools/External Tools.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/Build Debug.run.xml b/.run/Build Debug.run.xml
new file mode 100644
index 0000000000..beddbc15d5
--- /dev/null
+++ b/.run/Build Debug.run.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/Build Release.run.xml b/.run/Build Release.run.xml
new file mode 100644
index 0000000000..0d5fe21933
--- /dev/null
+++ b/.run/Build Release.run.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SConstruct b/SConstruct
index 934143fa08..39f7d14d03 100644
--- a/SConstruct
+++ b/SConstruct
@@ -74,6 +74,12 @@ AddOption('--minimal',
default=os.path.exists(File('#.lfsconfig').abspath), # minimal by default on release branch (where there's no LFS)
help='the minimum build to run openpilot. no tests, tools, etc.')
+AddOption('--stock-ui',
+ action='store_true',
+ dest='stock_ui',
+ default=False,
+ help='Build stock openpilot UI instead of sunnypilot UI')
+
## Architecture name breakdown (arch)
## - larch64: linux tici aarch64
## - aarch64: linux pc aarch64
@@ -172,6 +178,10 @@ else:
if arch != "Darwin":
ldflags += ["-Wl,--as-needed", "-Wl,--no-undefined"]
+if not GetOption('stock_ui'):
+ cflags += ["-DSUNNYPILOT"]
+ cxxflags += ["-DSUNNYPILOT"]
+
ccflags_option = GetOption('ccflags')
if ccflags_option:
ccflags += ccflags_option.split(' ')
diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript
index c851d7a969..f3957c3d03 100644
--- a/selfdrive/ui/SConscript
+++ b/selfdrive/ui/SConscript
@@ -12,6 +12,12 @@ if arch == "Darwin":
del base_libs[base_libs.index('OpenCL')]
qt_env['FRAMEWORKS'] += ['OpenCL']
+sp_widgets_src = []
+sp_qt_src = []
+if not GetOption('stock_ui'):
+ SConscript(['../../sunnypilot/selfdrive/ui/SConscript'])
+ Import('sp_widgets_src', 'sp_qt_src')
+
# FIXME: remove this once we're on 5.15 (24.04)
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
@@ -20,7 +26,7 @@ widgets_src = ["qt/widgets/input.cc", "qt/widgets/wifi.cc", "qt/prime_state.cc",
"qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc",
"qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc",
"qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc",
- "qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"]
+ "qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"] + sp_widgets_src
widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs)
Export('widgets')
@@ -31,7 +37,7 @@ qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc",
"qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc",
"qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc",
"qt/onroad/onroad_home.cc", "qt/onroad/annotated_camera.cc", "qt/onroad/model.cc",
- "qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"]
+ "qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"] + sp_qt_src
# build translation files
with open(File("translations/languages.json").abspath) as f:
diff --git a/selfdrive/ui/main.cc b/selfdrive/ui/main.cc
index 4903a3db3d..268a3d000b 100644
--- a/selfdrive/ui/main.cc
+++ b/selfdrive/ui/main.cc
@@ -4,10 +4,16 @@
#include
#include "system/hardware/hw.h"
-#include "selfdrive/ui/qt/qt_window.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/window.h"
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/qt/window.h"
+#define MainWindow MainWindowSP
+#else
+#include "selfdrive/ui/qt/qt_window.h"
+#endif
+
int main(int argc, char *argv[]) {
setpriority(PRIO_PROCESS, 0, -20);
diff --git a/selfdrive/ui/qt/body.h b/selfdrive/ui/qt/body.h
index 567a54d49b..b0ea20e552 100644
--- a/selfdrive/ui/qt/body.h
+++ b/selfdrive/ui/qt/body.h
@@ -5,7 +5,13 @@
#include
#include "common/util.h"
+
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/ui.h"
+#define UIState UIStateSP
+#else
#include "selfdrive/ui/ui.h"
+#endif
class RecordButton : public QPushButton {
Q_OBJECT
diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h
index 55bc706c0d..24ae756584 100644
--- a/selfdrive/ui/qt/home.h
+++ b/selfdrive/ui/qt/home.h
@@ -9,12 +9,23 @@
#include "common/params.h"
#include "selfdrive/ui/qt/body.h"
-#include "selfdrive/ui/qt/onroad/onroad_home.h"
-#include "selfdrive/ui/qt/sidebar.h"
-#include "selfdrive/ui/qt/widgets/controls.h"
#include "selfdrive/ui/qt/widgets/offroad_alerts.h"
#include "selfdrive/ui/ui.h"
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h"
+#include "../../sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h"
+#include "../../sunnypilot/selfdrive/ui/qt/sidebar.h"
+#define OnroadWindow OnroadWindowSP
+#define LayoutWidget LayoutWidgetSP
+#define Sidebar SidebarSP
+#define ElidedLabel ElidedLabelSP
+#else
+#include "selfdrive/ui/qt/widgets/controls.h"
+#include "selfdrive/ui/qt/onroad/onroad_home.h"
+#include "selfdrive/ui/qt/sidebar.h"
+#endif
+
class OffroadHome : public QFrame {
Q_OBJECT
@@ -58,13 +69,12 @@ protected:
void mousePressEvent(QMouseEvent* e) override;
void mouseDoubleClickEvent(QMouseEvent* e) override;
-private:
Sidebar *sidebar;
OffroadHome *home;
OnroadWindow *onroad;
BodyWindow *body;
QStackedLayout *slayout;
-private slots:
- void updateState(const UIState &s);
+protected slots:
+ virtual void updateState(const UIState &s);
};
diff --git a/selfdrive/ui/qt/network/networking.cc b/selfdrive/ui/qt/network/networking.cc
index 066dc3ca7e..0f101a35c3 100644
--- a/selfdrive/ui/qt/network/networking.cc
+++ b/selfdrive/ui/qt/network/networking.cc
@@ -8,8 +8,14 @@
#include "selfdrive/ui/qt/qt_window.h"
#include "selfdrive/ui/qt/util.h"
+
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h"
+#include "../../sunnypilot/selfdrive/ui/qt/widgets/scrollview.h"
+#else
#include "selfdrive/ui/qt/widgets/controls.h"
#include "selfdrive/ui/qt/widgets/scrollview.h"
+#endif
static const int ICON_WIDTH = 49;
diff --git a/selfdrive/ui/qt/network/networking.h b/selfdrive/ui/qt/network/networking.h
index 4fd604039b..93d7229e94 100644
--- a/selfdrive/ui/qt/network/networking.h
+++ b/selfdrive/ui/qt/network/networking.h
@@ -8,6 +8,17 @@
#include "selfdrive/ui/qt/widgets/ssh_keys.h"
#include "selfdrive/ui/qt/widgets/toggle.h"
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h"
+#define ButtonControl ButtonControlSP
+#define ElidedLabel ElidedLabelSP
+#define LabelControl LabelControlSP
+#define ListWidget ListWidgetSP
+#define ToggleControl ToggleControlSP
+#else
+#include "selfdrive/ui/qt/widgets/controls.h"
+#endif
+
class WifiItem : public QWidget {
Q_OBJECT
public:
diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc
index 710ce75d1b..ac43c35d72 100644
--- a/selfdrive/ui/qt/offroad/developer_panel.cc
+++ b/selfdrive/ui/qt/offroad/developer_panel.cc
@@ -2,9 +2,14 @@
#include "selfdrive/ui/qt/offroad/developer_panel.h"
#include "selfdrive/ui/qt/widgets/ssh_keys.h"
-#include "selfdrive/ui/qt/widgets/controls.h"
#include "common/util.h"
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h"
+#else
+#include "selfdrive/ui/qt/widgets/controls.h"
+#endif
+
DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) {
// SSH keys
addItem(new SshToggle());
diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h
index fe38612e57..824660458b 100644
--- a/selfdrive/ui/qt/offroad/developer_panel.h
+++ b/selfdrive/ui/qt/offroad/developer_panel.h
@@ -1,6 +1,10 @@
#pragma once
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h"
+#else
#include "selfdrive/ui/qt/offroad/settings.h"
+#endif
class DeveloperPanel : public ListWidget {
Q_OBJECT
diff --git a/selfdrive/ui/qt/offroad/onboarding.h b/selfdrive/ui/qt/offroad/onboarding.h
index a1b6895ba0..0f6dbb5063 100644
--- a/selfdrive/ui/qt/offroad/onboarding.h
+++ b/selfdrive/ui/qt/offroad/onboarding.h
@@ -71,6 +71,7 @@ public slots:
private:
void showEvent(QShowEvent *event) override;
+protected:
QPushButton *accept_btn;
signals:
@@ -97,10 +98,10 @@ class OnboardingWindow : public QStackedWidget {
public:
explicit OnboardingWindow(QWidget *parent = 0);
inline void showTrainingGuide() { setCurrentIndex(1); }
- inline bool completed() const { return accepted_terms && training_done; }
+ virtual inline bool completed() const { return accepted_terms && training_done; }
-private:
- void updateActiveScreen();
+protected:
+ virtual void updateActiveScreen();
Params params;
bool accepted_terms = false, training_done = false;
diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc
index d4aab53f5a..93dcd793c9 100644
--- a/selfdrive/ui/qt/offroad/settings.cc
+++ b/selfdrive/ui/qt/offroad/settings.cc
@@ -100,7 +100,9 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
}
// Toggles with confirmation dialogs
+#ifndef SUNNYPILOT
toggles["ExperimentalMode"]->setActiveIcon("../assets/img_experimental.svg");
+#endif
toggles["ExperimentalMode"]->setConfirmation(true, true);
toggles["ExperimentalLongitudinalEnabled"]->setConfirmation(true, false);
@@ -354,6 +356,7 @@ void SettingsWindow::setCurrentPanel(int index, const QString ¶m) {
}
SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) {
+ RETURN_IF_SUNNYPILOT
// setup two main layouts
sidebar_widget = new QWidget;
diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h
index de0528ee0a..9c4acac582 100644
--- a/selfdrive/ui/qt/offroad/settings.h
+++ b/selfdrive/ui/qt/offroad/settings.h
@@ -10,9 +10,21 @@
#include
#include
-#include "selfdrive/ui/ui.h"
#include "selfdrive/ui/qt/util.h"
+
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/ui.h"
+#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h"
+#define ListWidget ListWidgetSP
+#define ParamControl ParamControlSP
+#define ButtonControl ButtonControlSP
+#define ButtonParamControl ButtonParamControlSP
+#define ToggleControl ToggleControlSP
+#define LabelControl LabelControlSP
+#else
+#include "selfdrive/ui/ui.h"
#include "selfdrive/ui/qt/widgets/controls.h"
+#endif
// ********** settings window + top-level panels **********
class SettingsWindow : public QFrame {
@@ -30,7 +42,7 @@ signals:
void reviewTrainingGuide();
void expandToggleDescription(const QString ¶m);
-private:
+protected:
QPushButton *sidebar_alert_widget;
QWidget *sidebar_widget;
QButtonGroup *nav_btns;
@@ -45,12 +57,12 @@ public:
signals:
void reviewTrainingGuide();
-private slots:
+protected slots:
void poweroff();
void reboot();
void updateCalibDescription();
-private:
+protected:
Params params;
ButtonControl *pair_device;
};
@@ -64,15 +76,15 @@ public:
public slots:
void expandToggleDescription(const QString ¶m);
-private slots:
- void updateState(const UIState &s);
+protected slots:
+ virtual void updateState(const UIState &s);
-private:
+protected:
Params params;
std::map toggles;
ButtonParamControl *long_personality_setting;
- void updateToggles();
+ virtual void updateToggles();
};
class SoftwarePanel : public ListWidget {
@@ -80,7 +92,7 @@ class SoftwarePanel : public ListWidget {
public:
explicit SoftwarePanel(QWidget* parent = nullptr);
-private:
+protected:
void showEvent(QShowEvent *event) override;
void updateLabels();
void checkForUpdates();
diff --git a/selfdrive/ui/qt/onroad/buttons.h b/selfdrive/ui/qt/onroad/buttons.h
index 9c91bc3c7b..6bf479d136 100644
--- a/selfdrive/ui/qt/onroad/buttons.h
+++ b/selfdrive/ui/qt/onroad/buttons.h
@@ -2,7 +2,11 @@
#include
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/ui.h"
+#else
#include "selfdrive/ui/ui.h"
+#endif
const int btn_size = 192;
const int img_size = (btn_size / 4) * 3;
diff --git a/selfdrive/ui/qt/onroad/model.h b/selfdrive/ui/qt/onroad/model.h
index 79547e4b83..d2303eebd6 100644
--- a/selfdrive/ui/qt/onroad/model.h
+++ b/selfdrive/ui/qt/onroad/model.h
@@ -3,7 +3,11 @@
#include
#include
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/ui.h"
+#else
#include "selfdrive/ui/ui.h"
+#endif
class ModelRenderer {
public:
diff --git a/selfdrive/ui/qt/onroad/onroad_home.cc b/selfdrive/ui/qt/onroad/onroad_home.cc
index 080f9bd50f..7db9a05a0c 100644
--- a/selfdrive/ui/qt/onroad/onroad_home.cc
+++ b/selfdrive/ui/qt/onroad/onroad_home.cc
@@ -35,8 +35,12 @@ OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent) {
alerts->raise();
setAttribute(Qt::WA_OpaquePaintEvent);
+
+ // We handle the connection of the signals on the derived class
+#ifndef SUNNYPILOT
QObject::connect(uiState(), &UIState::uiUpdate, this, &OnroadWindow::updateState);
QObject::connect(uiState(), &UIState::offroadTransition, this, &OnroadWindow::offroadTransition);
+#endif
}
void OnroadWindow::updateState(const UIState &s) {
diff --git a/selfdrive/ui/qt/onroad/onroad_home.h b/selfdrive/ui/qt/onroad/onroad_home.h
index c321d2d44f..2224ede32e 100644
--- a/selfdrive/ui/qt/onroad/onroad_home.h
+++ b/selfdrive/ui/qt/onroad/onroad_home.h
@@ -3,20 +3,24 @@
#include "selfdrive/ui/qt/onroad/alerts.h"
#include "selfdrive/ui/qt/onroad/annotated_camera.h"
+#ifdef SUNNYPILOT
+#define UIState UIStateSP
+#endif
+
class OnroadWindow : public QWidget {
Q_OBJECT
public:
OnroadWindow(QWidget* parent = 0);
-private:
+protected:
void paintEvent(QPaintEvent *event);
OnroadAlerts *alerts;
AnnotatedCameraWidget *nvg;
QColor bg = bg_colors[STATUS_DISENGAGED];
QHBoxLayout* split;
-private slots:
- void offroadTransition(bool offroad);
- void updateState(const UIState &s);
+protected slots:
+ virtual void offroadTransition(bool offroad);
+ virtual void updateState(const UIState &s);
};
diff --git a/selfdrive/ui/qt/request_repeater.h b/selfdrive/ui/qt/request_repeater.h
index c0e2758273..7c3c740b5e 100644
--- a/selfdrive/ui/qt/request_repeater.h
+++ b/selfdrive/ui/qt/request_repeater.h
@@ -2,7 +2,12 @@
#include "common/util.h"
#include "selfdrive/ui/qt/api.h"
+
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/ui.h"
+#else
#include "selfdrive/ui/ui.h"
+#endif
class RequestRepeater : public HttpRequest {
public:
diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc
index 966396edc2..866a5b62d0 100644
--- a/selfdrive/ui/qt/sidebar.cc
+++ b/selfdrive/ui/qt/sidebar.cc
@@ -110,6 +110,10 @@ void Sidebar::updateState(const UIState &s) {
void Sidebar::paintEvent(QPaintEvent *event) {
QPainter p(this);
+ paintSidebar(p);
+}
+
+void Sidebar::paintSidebar(QPainter &p) {
p.setPen(Qt::NoPen);
p.setRenderHint(QPainter::Antialiasing);
diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h
index 2091418e52..d23427336e 100644
--- a/selfdrive/ui/qt/sidebar.h
+++ b/selfdrive/ui/qt/sidebar.h
@@ -5,9 +5,14 @@
#include
#include
-#include "selfdrive/ui/ui.h"
#include "selfdrive/ui/qt/network/networking.h"
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/ui.h"
+#else
+#include "selfdrive/ui/ui.h"
+#endif
+
typedef QPair, QColor> ItemStatus;
Q_DECLARE_METATYPE(ItemStatus);
@@ -35,6 +40,7 @@ protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void drawMetric(QPainter &p, const QPair &label, QColor c, int y);
+ virtual void paintSidebar(QPainter &p);
QPixmap home_img, flag_img, settings_img;
bool onroad, flag_pressed, settings_pressed;
diff --git a/selfdrive/ui/qt/util.h b/selfdrive/ui/qt/util.h
index 2bf1a70a62..6ea53d354a 100644
--- a/selfdrive/ui/qt/util.h
+++ b/selfdrive/ui/qt/util.h
@@ -13,6 +13,12 @@
#include "cereal/gen/cpp/car.capnp.h"
#include "common/params.h"
+#ifdef SUNNYPILOT
+#define RETURN_IF_SUNNYPILOT return;
+#else
+#define RETURN_IF_SUNNYPILOT // Do nothing
+#endif
+
QString getVersion();
QString getBrand();
QString getUserAgent();
diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h
index 29aa8493c7..63b2bf8628 100644
--- a/selfdrive/ui/qt/widgets/cameraview.h
+++ b/selfdrive/ui/qt/widgets/cameraview.h
@@ -23,7 +23,12 @@
#endif
#include "msgq/visionipc/visionipc_client.h"
+
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/ui.h"
+#else
#include "selfdrive/ui/ui.h"
+#endif
const int FRAME_BUFFER_SIZE = 5;
diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc
index 40dda971f5..7dd3178200 100644
--- a/selfdrive/ui/qt/widgets/controls.cc
+++ b/selfdrive/ui/qt/widgets/controls.cc
@@ -3,7 +3,10 @@
#include
#include
+#include "selfdrive/ui/qt/util.h"
+
AbstractControl::AbstractControl(const QString &title, const QString &desc, const QString &icon, QWidget *parent) : QFrame(parent) {
+ RETURN_IF_SUNNYPILOT
QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->setMargin(0);
diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h
index aebf934f2a..9562c4ad22 100644
--- a/selfdrive/ui/qt/widgets/controls.h
+++ b/selfdrive/ui/qt/widgets/controls.h
@@ -40,7 +40,7 @@ class AbstractControl : public QFrame {
Q_OBJECT
public:
- void setDescription(const QString &desc) {
+ virtual void setDescription(const QString &desc) {
if (description) description->setText(desc);
}
@@ -52,7 +52,7 @@ public:
value->setText(val);
}
- const QString getDescription() {
+ virtual const QString getDescription() {
return description->text();
}
@@ -60,7 +60,7 @@ public:
QPixmap icon_pixmap;
public slots:
- void showDescription() {
+ virtual void showDescription() {
description->setVisible(true);
}
diff --git a/selfdrive/ui/qt/widgets/ssh_keys.h b/selfdrive/ui/qt/widgets/ssh_keys.h
index 920bd651e2..53f0afac1b 100644
--- a/selfdrive/ui/qt/widgets/ssh_keys.h
+++ b/selfdrive/ui/qt/widgets/ssh_keys.h
@@ -3,7 +3,14 @@
#include
#include "system/hardware/hw.h"
+
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h"
+#define ButtonControl ButtonControlSP
+#define ToggleControl ToggleControlSP
+#else
#include "selfdrive/ui/qt/widgets/controls.h"
+#endif
// SSH enable toggle
class SshToggle : public ToggleControl {
diff --git a/selfdrive/ui/qt/widgets/toggle.h b/selfdrive/ui/qt/widgets/toggle.h
index e7263a008f..a0fa434a4c 100644
--- a/selfdrive/ui/qt/widgets/toggle.h
+++ b/selfdrive/ui/qt/widgets/toggle.h
@@ -23,14 +23,13 @@ public:
update();
}
bool getEnabled();
- void setEnabled(bool value);
+ virtual void setEnabled(bool value);
protected:
void paintEvent(QPaintEvent*) override;
void mouseReleaseEvent(QMouseEvent*) override;
void enterEvent(QEvent*) override;
-private:
QColor circleColor;
QColor green;
bool enabled = true;
diff --git a/selfdrive/ui/qt/widgets/wifi.h b/selfdrive/ui/qt/widgets/wifi.h
index 60c865f2b8..40552a699d 100644
--- a/selfdrive/ui/qt/widgets/wifi.h
+++ b/selfdrive/ui/qt/widgets/wifi.h
@@ -4,7 +4,12 @@
#include
#include
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/ui.h"
+#define UIState UIStateSP
+#else
#include "selfdrive/ui/ui.h"
+#endif
class WiFiPromptWidget : public QFrame {
Q_OBJECT
diff --git a/selfdrive/ui/qt/window.cc b/selfdrive/ui/qt/window.cc
index e1ec916c6f..c28f3a2422 100644
--- a/selfdrive/ui/qt/window.cc
+++ b/selfdrive/ui/qt/window.cc
@@ -4,16 +4,20 @@
#include "system/hardware/hw.h"
-MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
+// We have this constructor so that we can provide custom implementations of the windows. By default (stock_ui) would receive them as nullptr, so they'll be instantiated with stock. Otherwise they'd be SP instances
+MainWindow::MainWindow(QWidget *parent, HomeWindow *hw, SettingsWindow *sw, OnboardingWindow *ow) :
+ QWidget(parent),
+ homeWindow(hw ? hw : new HomeWindow(this)),
+ settingsWindow(sw ? sw : new SettingsWindow(this)),
+ onboardingWindow(ow ? ow : new OnboardingWindow(this)) {
+
main_layout = new QStackedLayout(this);
main_layout->setMargin(0);
- homeWindow = new HomeWindow(this);
main_layout->addWidget(homeWindow);
QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings);
QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings);
- settingsWindow = new SettingsWindow(this);
main_layout->addWidget(settingsWindow);
QObject::connect(settingsWindow, &SettingsWindow::closeSettings, this, &MainWindow::closeSettings);
QObject::connect(settingsWindow, &SettingsWindow::reviewTrainingGuide, [=]() {
@@ -21,7 +25,6 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
main_layout->setCurrentWidget(onboardingWindow);
});
- onboardingWindow = new OnboardingWindow(this);
main_layout->addWidget(onboardingWindow);
QObject::connect(onboardingWindow, &OnboardingWindow::onboardingDone, [=]() {
main_layout->setCurrentWidget(homeWindow);
diff --git a/selfdrive/ui/qt/window.h b/selfdrive/ui/qt/window.h
index 05b61e1f76..2ed56e6d58 100644
--- a/selfdrive/ui/qt/window.h
+++ b/selfdrive/ui/qt/window.h
@@ -11,15 +11,18 @@ class MainWindow : public QWidget {
Q_OBJECT
public:
- explicit MainWindow(QWidget *parent = 0);
+ explicit MainWindow(QWidget *parent = 0) : MainWindow(parent, nullptr, nullptr, nullptr) {}
+
+protected:
+ explicit MainWindow(QWidget *parent, HomeWindow *hw = nullptr, SettingsWindow *sw = nullptr, OnboardingWindow *ow = nullptr);
+ HomeWindow *homeWindow;
+ SettingsWindow *settingsWindow;
+ OnboardingWindow *onboardingWindow;
+ virtual void closeSettings();
private:
bool eventFilter(QObject *obj, QEvent *event) override;
void openSettings(int index = 0, const QString ¶m = "");
- void closeSettings();
QStackedLayout *main_layout;
- HomeWindow *homeWindow;
- SettingsWindow *settingsWindow;
- OnboardingWindow *onboardingWindow;
};
diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py
index 2c4d8d7037..7ffd66ed9a 100644
--- a/selfdrive/ui/tests/test_ui/run.py
+++ b/selfdrive/ui/tests/test_ui/run.py
@@ -160,7 +160,7 @@ def setup_offroad_alert(click, pm: PubMaster):
# Toggle between settings and home to refresh the offroad alert widget
setup_settings_device(click, pm)
- click(240, 216)
+ click(100, 100)
def setup_update_available(click, pm: PubMaster):
Params().put_bool("UpdateAvailable", True)
@@ -170,7 +170,7 @@ def setup_update_available(click, pm: PubMaster):
Params().put("UpdaterNewReleaseNotes", release_notes + "\n")
setup_settings_device(click, pm)
- click(240, 216)
+ click(100, 100)
def setup_pair_device(click, pm: PubMaster):
click(1950, 435)
diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc
index 804584ea3c..53f8bfcd36 100644
--- a/selfdrive/ui/ui.cc
+++ b/selfdrive/ui/ui.cc
@@ -9,16 +9,17 @@
#include "common/swaglog.h"
#include "common/util.h"
#include "common/watchdog.h"
+#include "qt/util.h"
#include "system/hardware/hw.h"
#define BACKLIGHT_DT 0.05
#define BACKLIGHT_TS 10.00
-static void update_sockets(UIState *s) {
+void update_sockets(UIState *s) {
s->sm->update(0);
}
-static void update_state(UIState *s) {
+void update_state(UIState *s) {
SubMaster &sm = *(s->sm);
UIScene &scene = s->scene;
@@ -111,6 +112,7 @@ UIState::UIState(QObject *parent) : QObject(parent) {
prime_state = new PrimeState(this);
language = QString::fromStdString(Params().get("LanguageSetting"));
+ RETURN_IF_SUNNYPILOT
// update timer
timer = new QTimer(this);
QObject::connect(timer, &QTimer::timeout, this, &UIState::update);
@@ -118,6 +120,7 @@ UIState::UIState(QObject *parent) : QObject(parent) {
}
void UIState::update() {
+ RETURN_IF_SUNNYPILOT
update_sockets(this);
update_state(this);
updateStatus();
@@ -131,8 +134,9 @@ void UIState::update() {
Device::Device(QObject *parent) : brightness_filter(BACKLIGHT_OFFROAD, BACKLIGHT_TS, BACKLIGHT_DT), QObject(parent) {
setAwake(true);
resetInteractiveTimeout();
-
+#ifndef SUNNYPILOT
QObject::connect(uiState(), &UIState::uiUpdate, this, &Device::update);
+#endif
}
void Device::update(const UIState &s) {
@@ -198,6 +202,7 @@ void Device::updateWakefulness(const UIState &s) {
setAwake(s.scene.ignition || interactive_timeout > 0);
}
+#ifndef SUNNYPILOT
UIState *uiState() {
static UIState ui_state;
return &ui_state;
@@ -207,3 +212,4 @@ Device *device() {
static Device _device;
return &_device;
}
+#endif
diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h
index 52f76dc611..21e74af1d2 100644
--- a/selfdrive/ui/ui.h
+++ b/selfdrive/ui/ui.h
@@ -71,7 +71,7 @@ class UIState : public QObject {
public:
UIState(QObject* parent = 0);
- void updateStatus();
+ virtual void updateStatus();
inline bool engaged() const {
return scene.started && (*sm)["selfdriveState"].getSelfdriveState().getEnabled();
}
@@ -86,15 +86,19 @@ signals:
void uiUpdate(const UIState &s);
void offroadTransition(bool offroad);
-private slots:
- void update();
+protected slots:
+ virtual void update();
+
+protected:
+ QTimer *timer;
private:
- QTimer *timer;
bool started_prev = false;
};
+#ifndef SUNNYPILOT
UIState *uiState();
+#endif
// device management class
class Device : public QObject {
@@ -107,7 +111,7 @@ public:
offroad_brightness = std::clamp(brightness, 0, 100);
}
-private:
+protected:
bool awake = false;
int interactive_timeout = 0;
bool ignition_on = false;
@@ -130,5 +134,10 @@ public slots:
void update(const UIState &s);
};
+#ifndef SUNNYPILOT
Device *device();
+#endif
+
void ui_update_params(UIState *s);
+void update_state(UIState *s);
+void update_sockets(UIState *s);
diff --git a/sunnypilot/selfdrive/assets/offroad/icon_home.svg b/sunnypilot/selfdrive/assets/offroad/icon_home.svg
new file mode 100644
index 0000000000..ca90cc7bf6
--- /dev/null
+++ b/sunnypilot/selfdrive/assets/offroad/icon_home.svg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1673f8d46251a05787b60346193852991739345506dc7e9b106dfb370d3611ed
+size 489
diff --git a/sunnypilot/selfdrive/assets/offroad/icon_software.png b/sunnypilot/selfdrive/assets/offroad/icon_software.png
new file mode 100644
index 0000000000..70915e2906
--- /dev/null
+++ b/sunnypilot/selfdrive/assets/offroad/icon_software.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:01fd38c0d5b05c3ce4ed07bfe4cd47a3ea61f68f24e6cc3058190d90d8200f9b
+size 4785
diff --git a/sunnypilot/selfdrive/assets/offroad/icon_toggle.png b/sunnypilot/selfdrive/assets/offroad/icon_toggle.png
new file mode 100644
index 0000000000..51906798b9
--- /dev/null
+++ b/sunnypilot/selfdrive/assets/offroad/icon_toggle.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:11336d2f65100bc0dc2a53e09efae41ae85817b27e1a5bf134cc995e631c5b52
+size 4073
diff --git a/sunnypilot/selfdrive/ui/.gitignore b/sunnypilot/selfdrive/ui/.gitignore
new file mode 100644
index 0000000000..e047b1f5dd
--- /dev/null
+++ b/sunnypilot/selfdrive/ui/.gitignore
@@ -0,0 +1,2 @@
+moc_*
+*.moc
diff --git a/sunnypilot/selfdrive/ui/SConscript b/sunnypilot/selfdrive/ui/SConscript
new file mode 100644
index 0000000000..421744f257
--- /dev/null
+++ b/sunnypilot/selfdrive/ui/SConscript
@@ -0,0 +1,21 @@
+widgets_src = [
+ "../../sunnypilot/selfdrive/ui/ui.cc",
+ "../../sunnypilot/selfdrive/ui/qt/window.cc",
+ "../../sunnypilot/selfdrive/ui/qt/widgets/controls.cc",
+ "../../sunnypilot/selfdrive/ui/qt/widgets/drive_stats.cc",
+ "../../sunnypilot/selfdrive/ui/qt/widgets/scrollview.cc",
+ "../../sunnypilot/selfdrive/ui/qt/widgets/toggle.cc",
+]
+
+qt_src = [
+ "../../sunnypilot/selfdrive/ui/qt/home.cc",
+ "../../sunnypilot/selfdrive/ui/qt/sidebar.cc",
+ "../../sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc",
+ "../../sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc",
+ "../../sunnypilot/selfdrive/ui/qt/onroad/onroad_home.cc",
+]
+
+sp_widgets_src = widgets_src
+sp_qt_src = qt_src
+
+Export('sp_widgets_src', 'sp_qt_src')
diff --git a/sunnypilot/selfdrive/ui/qt/home.cc b/sunnypilot/selfdrive/ui/qt/home.cc
new file mode 100644
index 0000000000..8cdd178a46
--- /dev/null
+++ b/sunnypilot/selfdrive/ui/qt/home.cc
@@ -0,0 +1,19 @@
+/**
+ * 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.
+ */
+
+#include "../../sunnypilot/selfdrive/ui/qt/home.h"
+
+HomeWindowSP::HomeWindowSP(QWidget *parent) : HomeWindow(parent) {
+}
+
+void HomeWindowSP::updateState(const UIState &s) {
+ HomeWindow::updateState(s);
+}
+
+void HomeWindowSP::mousePressEvent(QMouseEvent *e) {
+ HomeWindow::mousePressEvent(e);
+}
diff --git a/sunnypilot/selfdrive/ui/qt/home.h b/sunnypilot/selfdrive/ui/qt/home.h
new file mode 100644
index 0000000000..b4129126d3
--- /dev/null
+++ b/sunnypilot/selfdrive/ui/qt/home.h
@@ -0,0 +1,38 @@
+/**
+ * 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.
+ */
+
+#pragma once
+
+#include
+#include
+
+#include "common/params.h"
+#include "selfdrive/ui/qt/body.h"
+#include "selfdrive/ui/qt/widgets/offroad_alerts.h"
+#include "../../sunnypilot/selfdrive/ui/ui.h"
+#include "selfdrive/ui/qt/home.h"
+
+#ifdef SUNNYPILOT
+#include "../../sunnypilot/selfdrive/ui/qt/sidebar.h"
+#define OnroadWindow OnroadWindowSP
+#else
+#include "selfdrive/ui/qt/sidebar.h"
+#include "selfdrive/ui/qt/onroad/onroad_home.h"
+#endif
+
+class HomeWindowSP : public HomeWindow {
+ Q_OBJECT
+
+public:
+ explicit HomeWindowSP(QWidget *parent = 0);
+
+protected:
+ void mousePressEvent(QMouseEvent *e) override;
+
+private slots:
+ void updateState(const UIState &s) override;
+};
diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc b/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc
new file mode 100644
index 0000000000..2615743f27
--- /dev/null
+++ b/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc
@@ -0,0 +1,102 @@
+/**
+ * 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.
+ */
+
+#include "../../sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h"
+
+#include
+
+#include
+#include
+
+#include "common/util.h"
+#include "common/params.h"
+#include "selfdrive/ui/qt/util.h"
+#include "selfdrive/ui/qt/widgets/input.h"
+#include "selfdrive/ui/qt/widgets/scrollview.h"
+
+void TermsPageSP::showEvent(QShowEvent *event) {
+ // late init, building QML widget takes 200ms
+ if (layout()) {
+ return;
+ }
+
+ QVBoxLayout *main_layout = new QVBoxLayout(this);
+ main_layout->setContentsMargins(45, 35, 45, 45);
+ main_layout->setSpacing(0);
+
+ QLabel *title = new QLabel(tr("Terms & Conditions"));
+ title->setStyleSheet("font-size: 90px; font-weight: 600;");
+ main_layout->addWidget(title);
+
+ QLabel *text = new QLabel(this);
+ text->setTextFormat(Qt::RichText);
+ text->setWordWrap(true);
+ std::string tc_text = sunnypilot_tc ? "../assets/offroad/sp_tc.html" : "../assets/offroad/tc.html";
+ text->setText(QString::fromStdString(util::read_file(tc_text)));
+ text->setStyleSheet("font-size:50px; font-weight: 200; color: #C9C9C9; background-color:#1B1B1B; padding:50px 50px;");
+ ScrollView *scroll = new ScrollView(text, this);
+
+ main_layout->addSpacing(30);
+ main_layout->addWidget(scroll);
+ main_layout->addSpacing(50);
+
+ QHBoxLayout *buttons = new QHBoxLayout;
+ buttons->setMargin(0);
+ buttons->setSpacing(45);
+ main_layout->addLayout(buttons);
+
+ QPushButton *decline_btn = new QPushButton(tr("Decline"));
+ buttons->addWidget(decline_btn);
+ QObject::connect(decline_btn, &QPushButton::clicked, this, &TermsPage::declinedTerms);
+
+ accept_btn = new QPushButton(tr("Scroll to accept"));
+ accept_btn->setEnabled(false);
+ accept_btn->setStyleSheet(R"(
+ QPushButton {
+ background-color: #465BEA;
+ }
+ QPushButton:pressed {
+ background-color: #3049F4;
+ }
+ QPushButton:disabled {
+ background-color: #4F4F4F;
+ }
+ )");
+ buttons->addWidget(accept_btn);
+ QObject::connect(accept_btn, &QPushButton::clicked, this, &TermsPage::acceptedTerms);
+ QScrollBar *scroll_bar = scroll->verticalScrollBar();
+ connect(scroll_bar, &QScrollBar::valueChanged, this, [this, scroll_bar](int value) {
+ if (value == scroll_bar->maximum()) {
+ enableAccept();
+ }
+ });
+}
+
+void OnboardingWindowSP::updateActiveScreen() {
+ if (accepted_terms && training_done && !accepted_terms_sp) {
+ setCurrentIndex(3);
+ } else {
+ OnboardingWindow::updateActiveScreen();
+ }
+}
+
+OnboardingWindowSP::OnboardingWindowSP(QWidget *parent) : OnboardingWindow(parent) {
+ std::string current_terms_version_sp = params.get("TermsVersionSunnypilot");
+ accepted_terms_sp = params.get("HasAcceptedTermsSP") == current_terms_version_sp;
+ LOGD("accepted_terms_sp: %s", params.get("HasAcceptedTermsSP").c_str());
+
+ auto *terms_sp = new TermsPageSP(true, parent);
+ addWidget(terms_sp); // index = 3
+ connect(terms_sp, &TermsPageSP::acceptedTerms, [=]() {
+ params.put("HasAcceptedTermsSP", current_terms_version_sp);
+ accepted_terms_sp = true;
+ updateActiveScreen();
+ });
+ connect(terms_sp, &TermsPageSP::declinedTerms, [=]() { setCurrentIndex(2); });
+
+ OnboardingWindowSP::updateActiveScreen();
+}
diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h b/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h
new file mode 100644
index 0000000000..626428478c
--- /dev/null
+++ b/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h
@@ -0,0 +1,35 @@
+/**
+ * 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.
+ */
+
+#pragma once
+
+#include "selfdrive/ui/qt/offroad/onboarding.h"
+
+#include "selfdrive/ui/qt/qt_window.h"
+
+class TermsPageSP : public TermsPage {
+ Q_OBJECT
+
+public:
+ explicit TermsPageSP(bool sunnypilot = false, QWidget *parent = 0) : TermsPage(parent), sunnypilot_tc(sunnypilot) {}
+
+private:
+ bool sunnypilot_tc = false;
+ void showEvent(QShowEvent *event) override;
+};
+
+class OnboardingWindowSP : public OnboardingWindow {
+ Q_OBJECT
+
+public:
+ explicit OnboardingWindowSP(QWidget *parent = 0);
+ inline bool completed() const override { return accepted_terms && accepted_terms_sp && training_done; }
+
+private:
+ bool accepted_terms_sp = false;
+ void updateActiveScreen() override;
+};
diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc b/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc
new file mode 100644
index 0000000000..cf446c6c3f
--- /dev/null
+++ b/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc
@@ -0,0 +1,146 @@
+/**
+ * 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.
+ */
+
+#include "../../sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h"
+
+#include "selfdrive/ui/qt/network/networking.h"
+#include "../../sunnypilot/selfdrive/ui/qt/widgets/scrollview.h"
+#include "selfdrive/ui/qt/offroad/developer_panel.h"
+
+TogglesPanelSP::TogglesPanelSP(SettingsWindow *parent) : TogglesPanel(parent) {
+ QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &TogglesPanelSP::updateState);
+}
+
+void TogglesPanelSP::updateState(const UIStateSP &s) {
+ TogglesPanel::updateState(s);
+}
+
+SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) {
+ // setup two main layouts
+ sidebar_widget = new QWidget;
+ QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget);
+ panel_widget = new QStackedWidget();
+
+ // setup layout for close button
+ QVBoxLayout *close_btn_layout = new QVBoxLayout;
+ close_btn_layout->setContentsMargins(0, 0, 0, 20);
+
+ // close button
+ QPushButton *close_btn = new QPushButton(tr("×"));
+ close_btn->setStyleSheet(R"(
+ QPushButton {
+ font-size: 140px;
+ padding-bottom: 20px;
+ border-radius: 76px;
+ background-color: #292929;
+ font-weight: 400;
+ }
+ QPushButton:pressed {
+ background-color: #3B3B3B;
+ }
+ )");
+ close_btn->setFixedSize(152, 152);
+ close_btn_layout->addWidget(close_btn, 0, Qt::AlignLeft);
+ QObject::connect(close_btn, &QPushButton::clicked, this, &SettingsWindow::closeSettings);
+
+ // setup buttons widget
+ QWidget *buttons_widget = new QWidget;
+ QVBoxLayout *buttons_layout = new QVBoxLayout(buttons_widget);
+ buttons_layout->setMargin(0);
+ buttons_layout->addSpacing(10);
+
+ // setup panels
+ DevicePanel *device = new DevicePanel(this);
+ QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide);
+
+ TogglesPanelSP *toggles = new TogglesPanelSP(this);
+ QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
+
+ auto networking = new Networking(this);
+ QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType);
+
+ QList panels = {
+ PanelInfo(" " + tr("Device"), device, "../../sunnypilot/selfdrive/assets/offroad/icon_home.svg"),
+ PanelInfo(" " + tr("Network"), networking, "../assets/offroad/icon_network.png"),
+ PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"),
+ PanelInfo(" " + tr("Software"), new SoftwarePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"),
+ PanelInfo(" " + tr("Developer"), new DeveloperPanel(this), "../assets/offroad/icon_shell.png"),
+ };
+
+ nav_btns = new QButtonGroup(this);
+ for (auto &[name, panel, icon] : panels) {
+ QPushButton *btn = new QPushButton(name);
+ btn->setCheckable(true);
+ btn->setChecked(nav_btns->buttons().size() == 0);
+ btn->setIcon(QIcon(QPixmap(icon)));
+ btn->setIconSize(QSize(70, 70));
+ btn->setStyleSheet(R"(
+ QPushButton {
+ border-radius: 20px;
+ width: 400px;
+ height: 98px;
+ color: #bdbdbd;
+ border: none;
+ background: none;
+ font-size: 50px;
+ font-weight: 500;
+ text-align: left;
+ padding-left: 22px;
+ }
+ QPushButton:checked {
+ background-color: #696868;
+ color: white;
+ }
+ QPushButton:pressed {
+ color: #ADADAD;
+ }
+ )");
+ btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+ nav_btns->addButton(btn);
+ buttons_layout->addWidget(btn, 0, Qt::AlignLeft | Qt::AlignBottom);
+
+ const int lr_margin = (name != (" " + tr("Network")) || (name != (" " + tr("sunnypilot")))) ? 50 : 0; // Network and sunnypilot panel handles its own margins
+ panel->setContentsMargins(lr_margin, 25, lr_margin, 25);
+
+ ScrollViewSP *panel_frame = new ScrollViewSP(panel, this);
+ panel_widget->addWidget(panel_frame);
+
+ QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() {
+ btn->setChecked(true);
+ panel_widget->setCurrentWidget(w);
+ });
+ }
+ sidebar_layout->setContentsMargins(50, 50, 25, 50);
+
+ // main settings layout, sidebar + main panel
+ QHBoxLayout *main_layout = new QHBoxLayout(this);
+
+ // add layout for close button
+ sidebar_layout->addLayout(close_btn_layout);
+
+ // add layout for buttons scrolling
+ ScrollViewSP *buttons_scrollview = new ScrollViewSP(buttons_widget, this);
+ sidebar_layout->addWidget(buttons_scrollview);
+
+ sidebar_widget->setFixedWidth(500);
+ main_layout->addWidget(sidebar_widget);
+ main_layout->addWidget(panel_widget);
+
+ setStyleSheet(R"(
+ * {
+ color: white;
+ font-size: 50px;
+ }
+ SettingsWindow {
+ background-color: black;
+ }
+ QStackedWidget, ScrollViewSP {
+ background-color: black;
+ border-radius: 30px;
+ }
+ )");
+}
diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h b/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h
new file mode 100644
index 0000000000..5678a3171b
--- /dev/null
+++ b/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h
@@ -0,0 +1,39 @@
+/**
+ * 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.
+ */
+
+#pragma once
+
+#include