diff --git a/opendbc_repo b/opendbc_repo index 7c55504b9..01595bfd2 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 7c55504b96508d180f4ad135e20a9d083004c5fc +Subproject commit 01595bfd20204585c80f5b1c60d04a4c152f1780 diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc new file mode 100644 index 000000000..930d1d815 --- /dev/null +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -0,0 +1,713 @@ +#include +#include +#include +#include +#include + +#include + +#include "common/watchdog.h" +#include "common/util.h" +#include "selfdrive/ui/qt/network/networking.h" +#include "selfdrive/ui/qt/offroad/settings.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/widgets/prime.h" +#include "selfdrive/ui/qt/widgets/scrollview.h" +#include "selfdrive/ui/qt/offroad/developer_panel.h" +#include "selfdrive/ui/qt/offroad/firehose.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +InfiniteCableTogglesPanel::InfiniteCableTogglesPanel(SettingsWindow *parent) : ListWidget(parent) { + std::vector> toggle_defs{ + { + "EnableCurvatureController", + tr("VW: Lateral Correction (Recommended)"), + tr("Enables curvature PID post-processing additionally to QFK curvature offset
"), + "../assets/icons/chffr_wheel.png", + false, + }, + { + "EnableLongComfortMode", + tr("VW: Longitudinal Comfort Mode"), + tr("Enables longitudinal jerk and accel deviation limit control for safe and comfortable driving
"), + "../assets/icons/chffr_wheel.png", + false, + }, + { + "EnableSpeedLimitControl", + tr("VW: Speed Limit Control"), + tr("Enables setting maximum speed by speed limit detection
"), + "../assets/icons/speed_limit.png", + false, + }, + { + "EnableSpeedLimitPredicative", + tr("VW: Predicative Speed Limit (pACC)"), + tr("Enables setting predicative speed limit
"), + "../assets/icons/speed_limit.png", + false, + }, + { + "EnableSLPredReactToSL", + tr("VW: Predicative - Reaction to Speed Limits"), + tr("Enables reaction to speed limits as predicative speed limit
"), + "../assets/icons/speed_limit.png", + false, + }, + { + "EnableSLPredReactToCurves", + tr("VW: Predicative - Reaction to Curves"), + tr("Enables reaction to curves as predicative speed limit (Max Speed as per lateral ISO limits)
"), + "../assets/icons/speed_limit.png", + false, + }, + { + "BatteryDetails", + tr("VW MEB: Display Battery Details"), + tr("Display battery detail panel"), + "../assets/icons/capslock-fill.png", + false, + }, + { + "ForceRHDForBSM", + tr("VW: Force RHD for BSM"), + tr("Switch BSM detection side to RHD. Driver is on the right side."), + "../assets/icons/eye_closed.png", + false, + }, + { + "EnableSmoothSteer", + tr("Steer Smoothing"), + tr("Enables S-curving on lateral control for smoother steering
"), + "../assets/icons/chffr_wheel.png", + false, + }, + { + "DarkMode", + tr("Dark Mode"), + tr("Force brightness to a minimal value"), + "../assets/icons/eye_closed.png", + false, + }, + { + "DisableScreenTimer", + tr("Onroad Screen Timeout"), + tr("The onroad screen is turned of after 10 seconds. It will be temporarily enabled on alerts"), + "../assets/icons/eye_closed.png", + false, + }, + { + "EnableAngleOffset", + tr("Enable Steer Angle Offset (Testing)"), + tr("Enables the use of manual steer angle offset selection as start value for params detection."), + "../assets/icons/eye_closed.png", + false, + }, + }; + + for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) { + auto toggle = new ParamControl(param, title, desc, icon, this); + + bool locked = params.getBool((param + "Lock").toStdString()); + toggle->setEnabled(!locked); + + if (needs_restart && !locked) { + toggle->setDescription(toggle->getDescription() + tr(" Changing this setting will restart openpilot if the car is powered on.")); + + QObject::connect(uiState(), &UIState::engagedChanged, [toggle](bool engaged) { + toggle->setEnabled(!engaged); + }); + + QObject::connect(toggle, &ParamControl::toggleFlipped, [=](bool state) { + params.putBool("OnroadCycleRequested", true); + }); + } + + addItem(toggle); + toggles[param.toStdString()] = toggle; + } + + steer_offset_control = new OptionControlSP( + "AngleOffsetDegree", + tr("Steering Angle Offset"), + tr("Adjust steering angle offset params learner start value manually (in degrees). Taking effect only for onroad transition. Is refreshed with the actual learned value and saved."), + "", {-1800, 1800}, 10, false, nullptr, true, true); + + float stored_val = QString::fromStdString(params.get("AngleOffsetDegree")).toFloat(); + steer_offset_control->setLabel(QString::number(stored_val, 'f', 1) + "°"); + steer_offset_control->showDescription(); + addItem(steer_offset_control); + + bool enable_angle_offset = params.getBool("EnableAngleOffset"); + steer_offset_control->setVisible(enable_angle_offset); + + connect(uiStateSP(), &UIStateSP::uiUpdate, this, [=]() { + bool enable_steer_offset_control = params.getBool("EnableAngleOffset"); + steer_offset_control->setVisible(enable_steer_offset_control); + }); + + QObject::connect(steer_offset_control, &OptionControlSP::updateLabels, [=]() { + float val = QString::fromStdString(params.get("AngleOffsetDegree")).toFloat(); + steer_offset_control->setLabel(QString::number(val, 'f', 1) + "°"); + }); +} + +void InfiniteCableTogglesPanel::showEvent(QShowEvent *event) { + ListWidget::showEvent(event); + if (steer_offset_control) { + float val = QString::fromStdString(params.get("AngleOffsetDegree")).toFloat(); + steer_offset_control->setLabel(QString::number(val, 'f', 1) + "°"); + } +} + +void InfiniteCableTogglesPanel::expandToggleDescription(const QString ¶m) { + toggles[param.toStdString()]->showDescription(); +} + +void InfiniteCableTogglesPanel::scrollToToggle(const QString ¶m) { + if (auto it = toggles.find(param.toStdString()); it != toggles.end()) { + auto scroll_area = qobject_cast(parent()->parent()); + if (scroll_area) { + scroll_area->ensureWidgetVisible(it->second); + } + } +} + +TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { + // param, title, desc, icon, restart needed + std::vector> toggle_defs{ + { + "OpenpilotEnabledToggle", + tr("Enable sunnypilot"), + tr("Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature."), + "../assets/icons/chffr_wheel.png", + true, + }, + { + "ExperimentalMode", + tr("Experimental Mode"), + "", + "../assets/icons/experimental_white.svg", + false, + }, + { + "DisengageOnAccelerator", + tr("Disengage on Accelerator Pedal"), + tr("When enabled, pressing the accelerator pedal will disengage sunnypilot."), + "../assets/icons/disengage_on_accelerator.svg", + false, + }, + { + "IsLdwEnabled", + tr("Enable Lane Departure Warnings"), + tr("Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h)."), + "../assets/icons/warning.png", + false, + }, + { + "AlwaysOnDM", + tr("Always-On Driver Monitoring"), + tr("Enable driver monitoring even when sunnypilot is not engaged."), + "../assets/icons/monitoring.png", + false, + }, + { + "RecordFront", + tr("Record and Upload Driver Camera"), + tr("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), + "../assets/icons/monitoring.png", + true, + }, + { + "RecordAudio", + tr("Record and Upload Microphone Audio"), + tr("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."), + "../assets/icons/microphone.png", + true, + }, + { + "IsMetric", + tr("Use Metric System"), + tr("Display speed in km/h instead of mph."), + "../assets/icons/metric.png", + false, + }, + }; + + + std::vector longi_button_texts{tr("Aggressive"), tr("Standard"), tr("Relaxed")}; + long_personality_setting = new ButtonParamControl("LongitudinalPersonality", tr("Driving Personality"), + tr("Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. " + "In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " + "your steering wheel distance button."), + "../assets/icons/speed_limit.png", + longi_button_texts); + + // set up uiState update for personality setting + QObject::connect(uiState(), &UIState::uiUpdate, this, &TogglesPanel::updateState); + + for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) { + auto toggle = new ParamControl(param, title, desc, icon, this); + + bool locked = params.getBool((param + "Lock").toStdString()); + toggle->setEnabled(!locked); + + if (needs_restart && !locked) { + toggle->setDescription(toggle->getDescription() + tr(" Changing this setting will restart openpilot if the car is powered on.")); + + QObject::connect(uiState(), &UIState::engagedChanged, [toggle](bool engaged) { + toggle->setEnabled(!engaged); + }); + + QObject::connect(toggle, &ParamControl::toggleFlipped, [=](bool state) { + params.putBool("OnroadCycleRequested", true); + }); + } + + addItem(toggle); + toggles[param.toStdString()] = toggle; + + // insert longitudinal personality after NDOG toggle + if (param == "DisengageOnAccelerator") { + addItem(long_personality_setting); + } + } + + // Toggles with confirmation dialogs +#ifndef SUNNYPILOT + toggles["ExperimentalMode"]->setActiveIcon("../assets/icons/experimental.svg"); +#endif + toggles["ExperimentalMode"]->setConfirmation(true, true); +} + +void TogglesPanel::updateState(const UIState &s) { + const SubMaster &sm = *(s.sm); + + if (sm.updated("selfdriveState")) { + auto personality = sm["selfdriveState"].getSelfdriveState().getPersonality(); + if (personality != s.scene.personality && s.scene.started && isVisible()) { + long_personality_setting->setCheckedButton(static_cast(personality)); + } + uiState()->scene.personality = personality; + } +} + +void TogglesPanel::expandToggleDescription(const QString ¶m) { + toggles[param.toStdString()]->showDescription(); +} + +void TogglesPanel::scrollToToggle(const QString ¶m) { + if (auto it = toggles.find(param.toStdString()); it != toggles.end()) { + auto scroll_area = qobject_cast(parent()->parent()); + if (scroll_area) { + scroll_area->ensureWidgetVisible(it->second); + } + } +} + +void TogglesPanel::showEvent(QShowEvent *event) { + updateToggles(); +} + +void TogglesPanel::updateToggles() { + auto experimental_mode_toggle = toggles["ExperimentalMode"]; + const QString e2e_description = QString("%1
" + "

%2


" + "%3
" + "

%4


" + "%5
") + .arg(tr("sunnypilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. Experimental features are listed below:")) + .arg(tr("End-to-End Longitudinal Control")) + .arg(tr("Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. " + "Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " + "mistakes should be expected.")) + .arg(tr("New Driving Visualization")) + .arg(tr("The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner.")); + + const bool is_release = params.getBool("IsReleaseBranch"); + auto cp_bytes = params.get("CarParamsPersistent"); + if (!cp_bytes.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + + if (hasLongitudinalControl(CP)) { + // normal description and toggle + experimental_mode_toggle->setEnabled(true); + experimental_mode_toggle->setDescription(e2e_description); + long_personality_setting->setEnabled(true); + } else { + // no long for now + experimental_mode_toggle->setEnabled(false); + long_personality_setting->setEnabled(false); + params.remove("ExperimentalMode"); + + const QString unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control."); + + QString long_desc = unavailable + " " + \ + tr("sunnypilot longitudinal control may come in a future update."); + if (CP.getAlphaLongitudinalAvailable()) { + if (is_release) { + long_desc = unavailable + " " + tr("An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches."); + } else { + long_desc = tr("Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode."); + } + } + experimental_mode_toggle->setDescription("" + long_desc + "

" + e2e_description); + } + + experimental_mode_toggle->refresh(); + } else { + experimental_mode_toggle->setDescription(e2e_description); + } +} + +DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { + setSpacing(50); + addItem(new LabelControl(tr("Dongle ID"), getDongleId().value_or(tr("N/A")))); + addItem(new LabelControl(tr("Serial"), params.get("HardwareSerial").c_str())); + + pair_device = new ButtonControl(tr("Pair Device"), tr("PAIR"), + tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); + connect(pair_device, &ButtonControl::clicked, [=]() { + PairingPopup popup(this); + popup.exec(); + }); + addItem(pair_device); + + QObject::connect(uiState()->prime_state, &PrimeState::changed, [this] (PrimeState::Type type) { + pair_device->setVisible(type == PrimeState::PRIME_TYPE_UNPAIRED); + }); + +#ifndef SUNNYPILOT + // offroad-only buttons + + auto dcamBtn = new ButtonControl(tr("Driver Camera"), tr("PREVIEW"), + tr("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)")); + connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); }); + addItem(dcamBtn); +#endif + + resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); + connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription); + connect(resetCalibBtn, &ButtonControl::clicked, [&]() { + if (!uiState()->engaged()) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), tr("Reset"), this)) { + // Check engaged again in case it changed while the dialog was open + if (!uiState()->engaged()) { + params.remove("CalibrationParams"); + params.remove("LiveTorqueParameters"); + params.remove("LiveParameters"); + params.remove("LiveParametersV2"); + params.remove("LiveDelay"); + params.putBool("OnroadCycleRequested", true); + updateCalibDescription(); + } + } + } else { + ConfirmationDialog::alert(tr("Disengage to Reset Calibration"), this); + } + }); + addItem(resetCalibBtn); + +#ifndef SUNNYPILOT + auto retrainingBtn = new ButtonControl(tr("Review Training Guide"), tr("REVIEW"), tr("Review the rules, features, and limitations of sunnypilot")); + connect(retrainingBtn, &ButtonControl::clicked, [=]() { + if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) { + emit reviewTrainingGuide(); + } + }); + addItem(retrainingBtn); + + if (Hardware::TICI()) { + auto regulatoryBtn = new ButtonControl(tr("Regulatory"), tr("VIEW"), ""); + connect(regulatoryBtn, &ButtonControl::clicked, [=]() { + const std::string txt = util::read_file("../assets/offroad/fcc.html"); + ConfirmationDialog::rich(QString::fromStdString(txt), this); + }); + addItem(regulatoryBtn); + } + + auto translateBtn = new ButtonControl(tr("Change Language"), tr("CHANGE"), ""); + connect(translateBtn, &ButtonControl::clicked, [=]() { + QMap langs = getSupportedLanguages(); + QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), langs.key(uiState()->language), this); + if (!selection.isEmpty()) { + // put language setting, exit Qt UI, and trigger fast restart + params.put("LanguageSetting", langs[selection].toStdString()); + qApp->exit(18); + watchdog_kick(0); + } + }); + addItem(translateBtn); +#endif + + QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { + for (auto btn : findChildren()) { + if (btn != pair_device && btn != resetCalibBtn) { + btn->setEnabled(offroad); + } + } + }); + +#ifndef SUNNYPILOT + // power buttons + QHBoxLayout *power_layout = new QHBoxLayout(); + power_layout->setSpacing(30); + + QPushButton *reboot_btn = new QPushButton(tr("Reboot")); + reboot_btn->setObjectName("reboot_btn"); + power_layout->addWidget(reboot_btn); + QObject::connect(reboot_btn, &QPushButton::clicked, this, &DevicePanel::reboot); + + QPushButton *poweroff_btn = new QPushButton(tr("Power Off")); + poweroff_btn->setObjectName("poweroff_btn"); + power_layout->addWidget(poweroff_btn); + QObject::connect(poweroff_btn, &QPushButton::clicked, this, &DevicePanel::poweroff); + + if (!Hardware::PC()) { + connect(uiState(), &UIState::offroadTransition, poweroff_btn, &QPushButton::setVisible); + } + + setStyleSheet(R"( + #reboot_btn { height: 120px; border-radius: 15px; background-color: #393939; } + #reboot_btn:pressed { background-color: #4a4a4a; } + #poweroff_btn { height: 120px; border-radius: 15px; background-color: #E22C2C; } + #poweroff_btn:pressed { background-color: #FF2424; } + )"); + addItem(power_layout); +#endif +} + +void DevicePanel::updateCalibDescription() { + QString desc = tr("sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."); + std::string calib_bytes = params.get("CalibrationParams"); + if (!calib_bytes.empty()) { + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(calib_bytes.data(), calib_bytes.size())); + auto calib = cmsg.getRoot().getLiveCalibration(); + if (calib.getCalStatus() != cereal::LiveCalibrationData::Status::UNCALIBRATED) { + double pitch = calib.getRpyCalib()[1] * (180 / M_PI); + double yaw = calib.getRpyCalib()[2] * (180 / M_PI); + desc += tr(" Your device is pointed %1° %2 and %3° %4.") + .arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? tr("down") : tr("up"), + QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? tr("left") : tr("right")); + } + } catch (kj::Exception) { + qInfo() << "invalid CalibrationParams"; + } + } + + int lag_perc = 0; + std::string lag_bytes = params.get("LiveDelay"); + if (!lag_bytes.empty()) { + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size())); + lag_perc = cmsg.getRoot().getLiveDelay().getCalPerc(); + } catch (kj::Exception) { + qInfo() << "invalid LiveDelay"; + } + } + if (lag_perc < 100) { + desc += tr("\n\nSteering lag calibration is %1% complete.").arg(lag_perc); + } else { + desc += tr("\n\nSteering lag calibration is complete."); + } + + std::string torque_bytes = params.get("LiveTorqueParameters"); + if (!torque_bytes.empty()) { + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(torque_bytes.data(), torque_bytes.size())); + auto torque = cmsg.getRoot().getLiveTorqueParameters(); + // don't add for non-torque cars + if (torque.getUseParams()) { + int torque_perc = torque.getCalPerc(); + if (torque_perc < 100) { + desc += tr(" Steering torque response calibration is %1% complete.").arg(torque_perc); + } else { + desc += tr(" Steering torque response calibration is complete."); + } + } + } catch (kj::Exception) { + qInfo() << "invalid LiveTorqueParameters"; + } + } + + desc += "\n\n"; + desc += tr("openpilot is continuously calibrating, resetting is rarely required. " + "Resetting calibration will restart openpilot if the car is powered on."); + resetCalibBtn->setDescription(desc); +} + +void DevicePanel::reboot() { + if (!uiState()->engaged()) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reboot?"), tr("Reboot"), this)) { + // Check engaged again in case it changed while the dialog was open + if (!uiState()->engaged()) { + params.putBool("DoReboot", true); + } + } + } else { + ConfirmationDialog::alert(tr("Disengage to Reboot"), this); + } +} + +void DevicePanel::poweroff() { + if (!uiState()->engaged()) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to power off?"), tr("Power Off"), this)) { + // Check engaged again in case it changed while the dialog was open + if (!uiState()->engaged()) { + params.putBool("DoShutdown", true); + } + } + } else { + ConfirmationDialog::alert(tr("Disengage to Power Off"), this); + } +} + +void SettingsWindow::showEvent(QShowEvent *event) { + setCurrentPanel(0); +} + +void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { + if (!param.isEmpty()) { + // Check if param ends with "Panel" to determine if it's a panel name + if (param.endsWith("Panel")) { + QString panelName = param; + panelName.chop(5); // Remove "Panel" suffix + + // Find the panel by name + for (int i = 0; i < nav_btns->buttons().size(); i++) { + bool panel_trimmed = false; +#ifdef SUNNYPILOT + panel_trimmed = nav_btns->buttons()[i]->text().trimmed() == tr(panelName.toStdString().c_str()); +#endif + if ((nav_btns->buttons()[i]->text() == tr(panelName.toStdString().c_str())) || panel_trimmed) { + index = i; + break; + } + } + } else { + emit expandToggleDescription(param); + emit scrollToToggle(param); + } + } + + panel_widget->setCurrentIndex(index); + nav_btns->buttons()[index]->setChecked(true); +} + +SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { +#ifndef SUNNYPILOT + // setup two main layouts + sidebar_widget = new QWidget; + QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget); + panel_widget = new QStackedWidget(); + + // close button + QPushButton *close_btn = new QPushButton(tr("×")); + close_btn->setStyleSheet(R"( + QPushButton { + font-size: 140px; + padding-bottom: 20px; + border-radius: 100px; + background-color: #292929; + font-weight: 400; + } + QPushButton:pressed { + background-color: #3B3B3B; + } + )"); + close_btn->setFixedSize(200, 200); + sidebar_layout->addSpacing(45); + sidebar_layout->addWidget(close_btn, 0, Qt::AlignCenter); + QObject::connect(close_btn, &QPushButton::clicked, this, &SettingsWindow::closeSettings); + + // setup panels + DevicePanel *device = new DevicePanel(this); + QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide); + QObject::connect(device, &DevicePanel::showDriverView, this, &SettingsWindow::showDriverView); + + InfiniteCableTogglesPanel *ictoggles = new InfiniteCableTogglesPanel(this); + QObject::connect(this, &SettingsWindow::expandToggleDescription, ictoggles, &InfiniteCableTogglesPanel::expandToggleDescription); + QObject::connect(this, &SettingsWindow::scrollToToggle, ictoggles, &InfiniteCableTogglesPanel::scrollToToggle); + + TogglesPanel *toggles = new TogglesPanel(this); + QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); + QObject::connect(this, &SettingsWindow::scrollToToggle, toggles, &TogglesPanel::scrollToToggle); + + auto networking = new Networking(this); + QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType); + + QList> panels = { + {tr("Device"), device}, + {tr("Network"), networking}, + {tr("infiniteCableCustom"), ictoggles}, + {tr("Toggles"), toggles}, + {tr("Software"), new SoftwarePanel(this)}, + {tr("Firehose"), new FirehosePanel(this)}, + {tr("Developer"), new DeveloperPanel(this)}, + }; + + nav_btns = new QButtonGroup(this); + for (auto &[name, panel] : panels) { + QPushButton *btn = new QPushButton(name); + btn->setCheckable(true); + btn->setChecked(nav_btns->buttons().size() == 0); + btn->setStyleSheet(R"( + QPushButton { + color: grey; + border: none; + background: none; + font-size: 65px; + font-weight: 500; + } + QPushButton:checked { + color: white; + } + QPushButton:pressed { + color: #ADADAD; + } + )"); + btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + nav_btns->addButton(btn); + sidebar_layout->addWidget(btn, 0, Qt::AlignRight); + + const int lr_margin = name != tr("Network") ? 50 : 0; // Network panel handles its own margins + panel->setContentsMargins(lr_margin, 25, lr_margin, 25); + + ScrollView *panel_frame = new ScrollView(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, 100, 50); + + // main settings layout, sidebar + main panel + QHBoxLayout *main_layout = new QHBoxLayout(this); + + 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, ScrollView { + background-color: #292929; + border-radius: 30px; + } + )"); +#endif +}