UI: Model Folders (#1031)

* model folders.. yay

* scroll expanded item in view

---------

Co-authored-by: DevTekVE <devtekve@gmail.com>
This commit is contained in:
Nayan
2025-07-19 09:52:54 -04:00
committed by GitHub
parent 3faf709387
commit 7c1f3f646c
3 changed files with 212 additions and 14 deletions

View File

@@ -2,6 +2,7 @@
#include <QPushButton>
#include <QButtonGroup>
#include <QScroller>
#include "system/hardware/hw.h"
#include "selfdrive/ui/qt/util.h"
@@ -334,3 +335,141 @@ QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStrin
}
return "";
}
TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList<QPair<QString, QStringList>> &items,
const QString &current, QWidget *parent) : DialogBase(parent) {
QFrame *container = new QFrame(this);
container->setStyleSheet(R"(
QFrame { background-color: #1B1B1B; }
#confirm_btn[enabled="false"] { background-color: #2B2B2B; }
#confirm_btn:enabled { background-color: #465BEA; }
#confirm_btn:enabled:pressed { background-color: #3049F4; }
QTreeWidget {
background-color: transparent;
border: none;
}
QTreeWidget::item {
height: 135;
padding: 0px 50px;
margin: 5px;
text-align: left;
font-size: 55px;
font-weight: 300;
border-radius: 10px;
background-color: #4F4F4F;
color: white;
}
QTreeWidget::item:selected {
background-color: #465BEA;
}
QTreeWidget::branch {
background-color: transparent;
}
)");
QVBoxLayout *main_layout = new QVBoxLayout(container);
main_layout->setContentsMargins(55, 50, 55, 50);
QLabel *title = new QLabel(prompt_text, this);
title->setStyleSheet("font-size: 70px; font-weight: 500;");
main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop);
main_layout->addSpacing(25);
treeWidget = new QTreeWidget(this);
treeWidget->setHeaderHidden(true);
treeWidget->setIndentation(50);
treeWidget->setExpandsOnDoubleClick(false); // Disable double-click expansion
treeWidget->setAnimated(true);
treeWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
treeWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
treeWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
treeWidget->setDragEnabled(false);
treeWidget->setMouseTracking(true);
// Connect single-click to expand/collapse
QObject::connect(treeWidget, &QTreeWidget::itemClicked, [=](QTreeWidgetItem *item, int) {
if (item->childCount() > 0) {
item->setExpanded(!item->isExpanded());
treeWidget->scrollToItem(item->child(0), QAbstractItemView::EnsureVisible);
}
});
QScroller::grabGesture(treeWidget->viewport(), QScroller::LeftMouseButtonGesture);
// Populate tree
QListIterator<QPair<QString, QStringList>> iter(items);
while (iter.hasNext()) {
QPair currItem = iter.next();
if (currItem.first.isEmpty()) {
for (const QString &item : currItem.second) {
QTreeWidgetItem *topLevel = new QTreeWidgetItem();
topLevel->setText(0, item);
topLevel->setFlags(topLevel->flags() | Qt::ItemIsSelectable);
treeWidget->addTopLevelItem(topLevel);
if (item == current) {
topLevel->setSelected(true);
}
}
} else {
QTreeWidgetItem *folderItem = new QTreeWidgetItem(treeWidget);
folderItem->setIcon(0, QIcon(QPixmap("../assets/icons/menu.png")));
folderItem->setText(0, " " + currItem.first);
folderItem->setFlags(folderItem->flags() | Qt::ItemIsAutoTristate);
folderItem->setFlags(folderItem->flags() & ~Qt::ItemIsSelectable);
for (const QString &item : currItem.second)
{
QTreeWidgetItem *childItem = new QTreeWidgetItem(folderItem);
childItem->setText(0, item);
childItem->setFlags(childItem->flags() | Qt::ItemIsSelectable);
if (item == current) {
childItem->setSelected(true);
folderItem->setExpanded(true);
}
}
}
}
confirm_btn = new QPushButton(tr("Select"));
confirm_btn->setObjectName("confirm_btn");
confirm_btn->setEnabled(false);
QObject::connect(treeWidget, &QTreeWidget::itemSelectionChanged, [=]() {
QList<QTreeWidgetItem*> selectedItems = treeWidget->selectedItems();
if (!selectedItems.isEmpty()) {
selection = selectedItems.first()->text(0);
confirm_btn->setEnabled(selection != current);
}
});
ScrollView *scroll_view = new ScrollView(treeWidget, this);
scroll_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
main_layout->addWidget(scroll_view);
main_layout->addSpacing(35);
// cancel + confirm buttons
QHBoxLayout *blayout = new QHBoxLayout;
main_layout->addLayout(blayout);
blayout->setSpacing(50);
QPushButton *cancel_btn = new QPushButton(tr("Cancel"));
QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject);
QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept);
blayout->addWidget(cancel_btn);
blayout->addWidget(confirm_btn);
QVBoxLayout *outer_layout = new QVBoxLayout(this);
outer_layout->setContentsMargins(50, 50, 50, 50);
outer_layout->addWidget(container);
}
QString TreeOptionDialog::getSelection(const QString &prompt_text, const QList<QPair<QString, QStringList>> &items,
const QString &current, QWidget *parent) {
TreeOptionDialog d(prompt_text, items, current, parent);
if (d.exec()) {
return d.selection;
}
return "";
}

View File

@@ -6,6 +6,7 @@
#include <QString>
#include <QVBoxLayout>
#include <QWidget>
#include <QTreeWidget>
#include "selfdrive/ui/qt/widgets/keyboard.h"
@@ -69,3 +70,16 @@ public:
static QString getSelection(const QString &prompt_text, const QStringList &l, const QString &current, QWidget *parent);
QString selection;
};
class TreeOptionDialog : public DialogBase {
Q_OBJECT
public:
explicit TreeOptionDialog(const QString &prompt_text, const QList<QPair<QString, QStringList>> &items, const QString &current, QWidget *parent = nullptr);
static QString getSelection(const QString &prompt_text, const QList<QPair<QString, QStringList>> &items, const QString &current, QWidget *parent = nullptr);
QString selection;
private:
QTreeWidget *treeWidget;
QPushButton *confirm_btn;
};

View File

@@ -244,28 +244,73 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
currentModelLblBtn->setEnabled(false);
currentModelLblBtn->setValue(tr("Fetching models..."));
// Create mapping of bundle indices to display names
QMap<uint32_t, QString> index_to_bundle;
struct ModelEntry {
QString folder;
QString displayName;
int index;
};
QList<ModelEntry> sortedModels;
QSet<QString> modelFolders;
const auto bundles = model_manager.getAvailableBundles();
for (const auto &bundle: bundles) {
index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundle.getDisplayName()));
for (const auto &bundle : bundles) {
auto overrides = bundle.getOverrides();
QString gen;
for (const auto &override : overrides) {
if (override.getKey() == "folder") {
gen = QString::fromStdString(override.getValue().cStr());
}
}
modelFolders.insert(gen);
sortedModels.append(ModelEntry{
gen,
QString::fromStdString(bundle.getDisplayName()),
static_cast<int>(bundle.getIndex())
});
}
// Sort bundles by index in descending order
QStringList bundleNames;
// Add "Default" as the first option
bundleNames.append(DEFAULT_MODEL);
std::sort(sortedModels.begin(), sortedModels.end(),
[](const ModelEntry &a, const ModelEntry &b) {
return a.index > b.index;
});
auto indices = index_to_bundle.keys();
std::sort(indices.begin(), indices.end(), std::greater<uint32_t>());
for (const auto &index: indices) {
bundleNames.append(index_to_bundle[index]);
// Create a list of folder-maxIndex pairs for sorting
QList<QPair<QString, int>> folderMaxIndices;
for (const auto &folder : modelFolders) {
int maxIndex = -1;
for (const auto &model : sortedModels) {
if (model.folder == folder) {
maxIndex = std::max(maxIndex, model.index);
}
}
folderMaxIndices.append(qMakePair(folder, maxIndex));
}
// Sort folders by their highest model index
std::sort(folderMaxIndices.begin(), folderMaxIndices.end(),
[](const QPair<QString, int> &a, const QPair<QString, int> &b) {
return a.second > b.second;
});
// Create the final items list using sorted folders
QList<QPair<QString, QStringList>> items;
for (const auto &folderPair : folderMaxIndices) {
QStringList folderModels;
for (const auto &model : sortedModels) {
if (model.folder == folderPair.first) {
folderModels.append(model.displayName);
}
}
items.append(qMakePair(folderPair.first, folderModels));
}
items.insert(0, qMakePair(QString(""), QStringList{DEFAULT_MODEL}));
currentModelLblBtn->setValue(GetActiveModelInternalName());
const QString selectedBundleName = MultiOptionDialog::getSelection(
tr("Select a Model"), bundleNames, GetActiveModelName(), this);
const QString selectedBundleName = TreeOptionDialog::getSelection(
tr("Select a Model"), items, GetActiveModelName(), this);
if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) {
return;