Merge branch 'min-feat/dev/model-selector' into full

This commit is contained in:
Rick Lan
2025-06-17 19:01:48 +08:00
8 changed files with 488 additions and 1 deletions
+1
View File
@@ -26,6 +26,7 @@ Export('widgets')
qt_libs = [widgets, qt_util] + base_libs
qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc",
"qt/offroad/model_selector.cc",
"qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc",
"qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc", "qt/offroad/dp_panel.cc",
"qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", "qt/offroad/firehose.cc",
+226
View File
@@ -0,0 +1,226 @@
// MIT Non-Commercial License
//
// Copyright (c) 2019, dragonpilot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, for non-commercial purposes only, subject to the following conditions:
//
// - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// - Commercial use (e.g., use in a product, service, or activity intended to generate revenue) is prohibited without explicit written permission from dragonpilot. Contact ricklan@gmail.com for inquiries.
// - Any project that uses the Software must visibly mention the following acknowledgment: "This project uses software from dragonpilot and is licensed under a custom license requiring permission for use."
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "selfdrive/ui/qt/offroad/model_selector.h"
// Define style constants to improve maintainability
namespace {
const QString SELECTOR_BTN_STYLE = "background-color: #00309a; font-size: 48px;";
const QString MODEL_LIST_STYLE = "font-size: 64px;";
const QString SCROLLBAR_STYLE = "width: 96px;";
const QString GROUP_HEADER_BG_COLOR = "#c8c8c8"; // Light gray
const QString GROUP_HEADER_TEXT_COLOR = "#000000"; // Black
// Role for storing the actual model name without indentation
const int ModelNameRole = Qt::UserRole;
}
ModelSelector::ModelSelector(QWidget *parent) : QWidget(parent) {
setupUI();
setupModelListPanel();
connectSignals();
}
QWidget* ModelSelector::setupUI() {
QVBoxLayout* main_layout = new QVBoxLayout(this);
main_layout->addSpacing(10);
// Selector button
QWidget* model_selector_btn_widget = new QWidget;
QHBoxLayout* model_selector_btn_layout = new QHBoxLayout();
QLabel* vehicle_model_label = new QLabel(tr("Vehicle Model:"));
vehicle_model_label->setStyleSheet("margin-right: 2px; font-size: 48px;");
model_selector_btn_layout->addWidget(vehicle_model_label);
QString model_selected = QString::fromUtf8(Params().get("dp_device_model_selected").c_str());
model_selector_btn = new QPushButton(model_selected.isEmpty() ? tr("[AUTO DETECT]") : model_selected);
model_selector_btn->setObjectName("ModelSelectorBtn");
model_selector_btn->setStyleSheet(SELECTOR_BTN_STYLE);
model_selector_btn_layout->addWidget(model_selector_btn);
model_selector_btn_layout->setAlignment(Qt::AlignCenter);
model_selector_btn_layout->setStretch(1, 1);
model_selector_btn_widget->setLayout(model_selector_btn_layout);
main_layout->addWidget(model_selector_btn_widget);
main_layout->addSpacing(10);
main_layout->addStretch(); // Add stretch to push everything to the top
setLayout(main_layout);
return model_selector_btn_widget;
}
void ModelSelector::setupModelListPanel() {
// Create model list panel
model_list_panel = new QWidget();
QVBoxLayout* model_list_layout = new QVBoxLayout(model_list_panel);
model_list_layout->setContentsMargins(50, 25, 50, 25);
model_list = new QListWidget(model_list_panel);
// Set styles using the constants
QString listStyle = QString("QListWidget { %1 } QScrollBar:vertical { %2 }")
.arg(MODEL_LIST_STYLE)
.arg(SCROLLBAR_STYLE);
model_list->setStyleSheet(listStyle);
model_list->setFixedHeight(750);
model_list_layout->addWidget(model_list);
model_list_frame = new ScrollView(model_list_panel, nullptr);
}
void ModelSelector::loadModelList() {
if (model_list->count() > 0) {
// If list is already populated, just update the selection
updateCurrentSelection();
return;
}
// Add auto-detect option
QListWidgetItem* autoDetectItem = new QListWidgetItem(tr("[AUTO DETECT]"));
autoDetectItem->setData(ModelNameRole, tr("[AUTO DETECT]"));
model_list->addItem(autoDetectItem);
Params params;
QString model_list_str = QString::fromStdString(params.get("dp_device_model_list"));
QJsonDocument document = QJsonDocument::fromJson(model_list_str.toUtf8());
if (document.isArray()) {
QJsonArray models = document.array();
for (const auto& groupValue : models) {
QJsonObject group = groupValue.toObject();
QString groupName = group["group"].toString();
// Add group header item
QListWidgetItem* groupHeader = new QListWidgetItem(groupName);
groupHeader->setFlags(Qt::NoItemFlags); // Make non-selectable
groupHeader->setBackground(QColor(GROUP_HEADER_BG_COLOR));
groupHeader->setForeground(QColor(GROUP_HEADER_TEXT_COLOR));
groupHeader->setTextAlignment(Qt::AlignCenter);
model_list->addItem(groupHeader);
// Add models in this group
QJsonArray groupModels = group["models"].toArray();
for (const auto& model : groupModels) {
QString modelName = model.toString();
// Create item with visual indentation
QListWidgetItem* modelItem = new QListWidgetItem(" " + modelName);
// Store actual model name without indentation in user role
modelItem->setData(ModelNameRole, modelName);
model_list->addItem(modelItem);
}
}
}
// Set the current selection after loading
updateCurrentSelection();
}
// New helper method to update the selection
void ModelSelector::updateCurrentSelection() {
// Get the currently selected model from params
Params params;
QString currentModel = QString::fromStdString(params.get("dp_device_model_selected"));
// If empty, select the AUTO DETECT option
if (currentModel.isEmpty()) {
model_list->setCurrentRow(0); // AUTO DETECT is the first item
return;
}
// Otherwise, find and select the matching model
for (int i = 0; i < model_list->count(); i++) {
QListWidgetItem* item = model_list->item(i);
// Only check selectable items (not group headers)
if (item->flags() & Qt::ItemIsSelectable) {
QString modelName = item->data(ModelNameRole).toString();
if (modelName == currentModel) {
model_list->setCurrentItem(item);
break;
}
}
}
}
void ModelSelector::clearModelList() {
model_list->clear();
}
void ModelSelector::updateButtonText(const QString& text) {
model_selector_btn->setText(text);
}
QWidget* ModelSelector::getModelListPanel() {
return model_list_frame;
}
void ModelSelector::setPanelWidget(QStackedWidget* panel) {
panel_widget = panel;
if (panel_widget && model_list_frame->parent() != panel_widget) {
model_list_frame->setParent(panel_widget);
panel_widget->addWidget(model_list_frame);
}
}
void ModelSelector::setNavButtonGroup(QButtonGroup* buttons) {
nav_btns = buttons;
}
void ModelSelector::connectSignals() {
connect(model_selector_btn, &QPushButton::clicked, [this]() {
if (panel_widget) {
// Load the model list when needed
loadModelList();
panel_widget->setCurrentWidget(model_list_frame);
}
emit buttonClicked();
});
connect(model_list, &QListWidget::itemClicked, [this](QListWidgetItem* item) {
// Only process clicks on selectable items (not group headers)
if (item->flags() & Qt::ItemIsSelectable) {
// Get model name from the data role rather than trimming text
QString model_name = item->data(ModelNameRole).toString();
QString param_value = (model_name == tr("[AUTO DETECT]")) ? QString() : model_name;
// Update param and button text
Params().put("dp_device_model_selected", param_value.toStdString());
updateButtonText(param_value.isEmpty() ? tr("[AUTO DETECT]") : model_name);
// Emit signal that model was selected
emit modelSelected(model_name);
// Go back to the previous panel
if (nav_btns && nav_btns->checkedButton()) {
nav_btns->checkedButton()->click();
} else if (nav_btns && nav_btns->buttons().size() > 0) {
// Default to first panel if none selected
nav_btns->buttons().first()->click();
}
}
});
if (model_list_frame) {
model_list_frame->installEventFilter(this);
}
}
bool ModelSelector::eventFilter(QObject *obj, QEvent *event) {
if (obj == model_list_frame) {
if (event->type() == QEvent::Hide) {
clearModelList();
}
}
return QWidget::eventFilter(obj, event);
}
+69
View File
@@ -0,0 +1,69 @@
// MIT Non-Commercial License
//
// Copyright (c) 2019, dragonpilot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, for non-commercial purposes only, subject to the following conditions:
//
// - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// - Commercial use (e.g., use in a product, service, or activity intended to generate revenue) is prohibited without explicit written permission from dragonpilot. Contact ricklan@gmail.com for inquiries.
// - Any project that uses the Software must visibly mention the following acknowledgment: "This project uses software from dragonpilot and is licensed under a custom license requiring permission for use."
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#pragma once
#include <QWidget>
#include <QPushButton>
#include <QListWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QListWidgetItem>
#include <QDebug>
#include <QStackedWidget>
#include <QButtonGroup> // Add this include for QButtonGroup
#include <QEvent>
#include "common/params.h"
#include "selfdrive/ui/qt/widgets/scrollview.h"
class ModelSelector : public QWidget {
Q_OBJECT
public:
explicit ModelSelector(QWidget *parent = nullptr);
// Get the model list panel widget
QWidget* getModelListPanel();
// Set the panel widget to switch to when selecting models
void setPanelWidget(QStackedWidget* panel);
// Set the button group to return to after model selection
void setNavButtonGroup(QButtonGroup* nav_btns);
signals:
void buttonClicked();
void modelSelected(const QString& model_name);
private:
QPushButton* model_selector_btn;
QListWidget* model_list;
QWidget* model_list_panel;
ScrollView* model_list_frame;
QStackedWidget* panel_widget = nullptr;
QButtonGroup* nav_btns = nullptr;
QWidget* setupUI();
void setupModelListPanel();
void loadModelList();
void updateCurrentSelection();
void clearModelList();
void connectSignals();
void updateButtonText(const QString& text);
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
};
+21 -1
View File
@@ -16,6 +16,7 @@
#include "selfdrive/ui/qt/offroad/developer_panel.h"
#include "selfdrive/ui/qt/offroad/firehose.h"
#include "selfdrive/ui/qt/offroad/dp_panel.h"
#include "selfdrive/ui/qt/offroad/model_selector.h"
TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
// param, title, desc, icon, restart needed
@@ -482,7 +483,26 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) {
sidebar_widget->setFixedWidth(500);
main_layout->addWidget(sidebar_widget);
main_layout->addWidget(panel_widget);
// Create right column with model selector on top and panel_widget below
QWidget* right_column = new QWidget(this);
QVBoxLayout* right_layout = new QVBoxLayout(right_column);
right_layout->setContentsMargins(0, 0, 0, 0);
right_layout->setSpacing(20); // Space between model selector and panel
// Create the ModelSelector button at the top of right column
ModelSelector* model_selector = new ModelSelector(this);
right_layout->addWidget(model_selector);
// Set up panel widget and nav button references
model_selector->setPanelWidget(panel_widget);
model_selector->setNavButtonGroup(nav_btns);
// Add panel_widget below the model selector
right_layout->addWidget(panel_widget, 1); // Give panel_widget stretch priority
// Add right column to main layout
main_layout->addWidget(right_column);
setStyleSheet(R"(
* {