ui: Favorite Models (#1168)

* init model favorites

* fix fav buttons

* fix blank favs

* switch to ref

* new favs at top

* remove debug prints & add some comments

* button style

* fix current selection

* !@%#$%(@^%$#(@!%#^

* add last update date to folders
This commit is contained in:
Nayan
2025-08-31 02:27:08 -04:00
committed by GitHub
parent 3f3c293559
commit 45ee58b1f6
8 changed files with 224 additions and 44 deletions
+1
View File
@@ -167,6 +167,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ModelManager_ActiveBundle", {PERSISTENT, JSON}},
{"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}},
{"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT, "0"}},
{"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}},
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
+151 -20
View File
@@ -336,8 +336,8 @@ 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) {
TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList<TreeFolder> &items,
const QString &current, const QString &favParam, QWidget *parent) : DialogBase(parent) {
QFrame *container = new QFrame(this);
container->setStyleSheet(R"(
QFrame { background-color: #1B1B1B; }
@@ -375,6 +375,9 @@ TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList<QPair
main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop);
main_layout->addSpacing(25);
iconBlank = QIcon("../../sunnypilot/selfdrive/assets/icons/star-empty.png");
iconFilled = QIcon ("../../sunnypilot/selfdrive/assets/icons/star-filled.png");
treeWidget = new QTreeWidget(this);
treeWidget->setHeaderHidden(true);
treeWidget->setIndentation(50);
@@ -396,34 +399,49 @@ TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList<QPair
QScroller::grabGesture(treeWidget->viewport(), QScroller::LeftMouseButtonGesture);
// Create initial list of favorites from param
const QString favs = QString::fromStdString(params.get(favParam.toStdString()));
mapFavs = new QMap<QString, QList<QPushButton*>>();
favRefs = new QStringList(favs.split(";"));
for (const QString &item : *favRefs)
{
mapFavs->insert( item, {});
}
// Populate tree
QListIterator<QPair<QString, QStringList>> iter(items);
QListIterator<TreeFolder> iter(items);
while (iter.hasNext()) {
QPair currItem = iter.next();
if (currItem.first.isEmpty()) {
for (const QString &item : currItem.second) {
TreeFolder currItem = iter.next();
QString prevFolder;
QString currentFolder;
if (currItem.folder.isEmpty()) {
for (const TreeNode &item : currItem.items) {
QTreeWidgetItem *topLevel = new QTreeWidgetItem();
topLevel->setText(0, item);
topLevel->setText(0, item.displayName);
topLevel->setData(0, Qt::UserRole, item.ref);
topLevel->setFlags(topLevel->flags() | Qt::ItemIsSelectable);
treeWidget->addTopLevelItem(topLevel);
if (item == current) {
if (item.ref == current) {
topLevel->setSelected(true);
}
}
} else {
QTreeWidgetItem *folderItem = new QTreeWidgetItem(treeWidget);
QList<QTreeWidgetItem*> folders = treeWidget->findItems(currItem.folder, Qt::MatchExactly, 0);
QTreeWidgetItem *folderItem = nullptr;
if (folders.isEmpty()) {
folderItem = new QTreeWidgetItem(treeWidget);
} else {
folderItem = folders.first();
}
folderItem->setIcon(0, QIcon(QPixmap("../assets/icons/menu.png")));
folderItem->setText(0, " " + currItem.first);
folderItem->setText(0, " " + currItem.folder);
folderItem->setFlags(folderItem->flags() | Qt::ItemIsAutoTristate);
folderItem->setFlags(folderItem->flags() & ~Qt::ItemIsSelectable);
for (const QString &item : currItem.second)
for (const TreeNode item : currItem.items)
{
QTreeWidgetItem *childItem = new QTreeWidgetItem(folderItem);
childItem->setText(0, item);
childItem->setFlags(childItem->flags() | Qt::ItemIsSelectable);
if (item == current) {
QTreeWidgetItem *childItem = addChildItem(item.displayName, item.ref, folderItem);
if (item.ref == current) {
childItem->setSelected(true);
folderItem->setExpanded(true);
}
@@ -431,6 +449,39 @@ TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList<QPair
}
}
// Create favorites folder
favorites = new QTreeWidgetItem();
favorites->setIcon(0, QIcon(QPixmap("../assets/icons/menu.png")));
favorites->setText(0, " " + tr("Favorites"));
favorites->setFlags(favorites->flags() | Qt::ItemIsAutoTristate);
favorites->setFlags(favorites->flags() & ~Qt::ItemIsSelectable);
treeWidget->insertTopLevelItem(1, favorites);
// Create favorite nodes
for (int i = favRefs->size() - 1; i >= 0; --i) {
QString item = favRefs->at(i);
if (item.isEmpty()) continue;
QTreeWidgetItemIterator treeIt(treeWidget);
QTreeWidgetItem *nodeItem = nullptr;
while (*treeIt) {
if (item == (*treeIt)->data(0, Qt::UserRole).toString()) {
nodeItem = (*treeIt);
break;
}
++treeIt;
}
if (nodeItem == nullptr) continue;
QTreeWidgetItem *childItem = addChildItem(nodeItem->text(0),
nodeItem->data(0, Qt::UserRole).toString(), favorites);
if (item == current) {
treeWidget->collapseAll();
childItem->setSelected(true);
favorites->setExpanded(true);
}
}
confirm_btn = new QPushButton(tr("Select"));
confirm_btn->setObjectName("confirm_btn");
confirm_btn->setEnabled(false);
@@ -438,7 +489,7 @@ TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList<QPair
QObject::connect(treeWidget, &QTreeWidget::itemSelectionChanged, [=]() {
QList<QTreeWidgetItem*> selectedItems = treeWidget->selectedItems();
if (!selectedItems.isEmpty()) {
selection = selectedItems.first()->text(0);
selection = selectedItems.first()->data(0, Qt::UserRole).toString();
confirm_btn->setEnabled(selection != current);
}
});
@@ -465,11 +516,91 @@ TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList<QPair
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);
QString TreeOptionDialog::getSelection(const QString &prompt_text, const QList<TreeFolder> &items,
const QString &current, const QString &favParam, QWidget *parent) {
TreeOptionDialog d(prompt_text, items, current, favParam, parent);
if (d.exec()) {
return d.selection;
}
return "";
}
/**
* Handles the addition or removal of items from the "favorites" list based on the provided reference identifier.
*
* @param displayName The text label associated with the item to be added or removed in the favorites.
* @param ref A unique reference key identifying the item.
* @param btn A pointer to the QPushButton associated with the item. The button's icon is updated to indicate
* whether the item is currently favorited or not.
*
* If the item is already in the favorites, it is removed from the list, its associated buttons have their
* icons reset, and the favorites tree is updated accordingly. If the item is not in the favorites, it is
* added to the list, a new associated button is created, and the favorites tree is updated. The current
* state of the favorites is stored in the Params object as a semicolon-separated string.
*/
void TreeOptionDialog::handleFavorites(const QString &displayName, const QString &ref, QPushButton *btn) {
if (mapFavs->keys().contains(ref)) { // Remove from favorites
for (auto *itemBtn:mapFavs->value(ref))
{
itemBtn->setIcon(iconBlank);
}
mapFavs->remove(ref);
favRefs->removeAll(ref);
for (int i = 0; i < favorites->childCount(); ++i) {
QTreeWidgetItem* child = favorites->child(i);
if (child && child->data(0, Qt::UserRole).toString() == ref) {
favorites->removeChild(child);
}
}
} else { // Add to favorites
QPushButton *favBtn = new QPushButton();
btn->setIcon(iconFilled);
mapFavs->insert(ref, {btn, favBtn});
favRefs->append(ref);
addChildItem(displayName, ref, favorites, favBtn, true);
}
const QString favs =favRefs->join(";");
params.put("ModelManager_Favs", favs.toStdString());
}
/**
* Adds a child item to a given folder item within the QTreeWidget.
*
* @param displayName The text to display for the child item.
* @param ref A reference string that uniquely identifies the child item.
* @param folderItem The parent folder item to which the child item will be added.
* @param btn A pointer to a QPushButton associated with the child item. If nullptr, a new button will be created.
* @param addAtTop If true, the child item is added as the first child of the folder item; otherwise, it is appended to the end.
* @return A pointer to the created QTreeWidgetItem representing the child item.
*/
QTreeWidgetItem* TreeOptionDialog::addChildItem(const QString &displayName, const QString &ref, QTreeWidgetItem *folderItem, QPushButton *btn, bool addAtTop) {
QTreeWidgetItem *childItem = new QTreeWidgetItem();
if (btn == nullptr) {
btn = new QPushButton();
}
if (mapFavs->keys().contains(ref)) {
btn->setIcon(iconFilled);
(*mapFavs)[ref].append(btn);
} else {
btn->setIcon(iconBlank);
}
btn->setIconSize(QSize(100, 100));
QWidget *buttonContainer = new QWidget();
QHBoxLayout *layout = new QHBoxLayout(buttonContainer);
layout->addWidget(btn, 0, Qt::AlignRight);
childItem->setText(0, displayName);
childItem->setData(0, Qt::UserRole, ref);
childItem->setFlags(childItem->flags() | Qt::ItemIsSelectable);
if (addAtTop) {
folderItem->insertChild(0, childItem);
} else {
folderItem->addChild(childItem);
}
treeWidget->setItemWidget(childItem, 0, buttonContainer);
connect(btn, &QPushButton::clicked, btn, [=]() {
handleFavorites(displayName, ref, btn);
});
return childItem;
}
+24 -2
View File
@@ -8,9 +8,22 @@
#include <QWidget>
#include <QTreeWidget>
#include "common/params.h"
#include "selfdrive/ui/qt/widgets/keyboard.h"
struct TreeNode {
QString folder;
QString displayName;
QString ref;
int index;
};
struct TreeFolder {
QString folder;
QList<TreeNode> items;
};
class DialogBase : public QDialog {
Q_OBJECT
@@ -75,11 +88,20 @@ 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);
explicit TreeOptionDialog(const QString &prompt_text, const QList<TreeFolder> &items, const QString &current, const QString &favParam, QWidget *parent = nullptr);
static QString getSelection(const QString &prompt_text, const QList<TreeFolder> &items, const QString &current, const QString &favParam, QWidget *parent = nullptr);
void handleFavorites(const QString &displayName, const QString &ref, QPushButton* btn);
QTreeWidgetItem* addChildItem(const QString &displayName, const QString &ref, QTreeWidgetItem* folderItem, QPushButton* btn = nullptr, bool addAtTop = false);
QString selection;
private:
QTreeWidget *treeWidget;
QPushButton *confirm_btn;
Params params;
QMap<QString, QList<QPushButton*>> *mapFavs;
QStringList *favRefs;
QTreeWidgetItem *favorites;
QIcon iconBlank;
QIcon iconFilled;
};
@@ -259,6 +259,18 @@ QString ModelsPanel::GetActiveModelInternalName() {
return DEFAULT_MODEL;
}
/**
* @brief Gets the ref of the currently selected model bundle
* @return ref of the selected bundle or default model name
*/
QString ModelsPanel::GetActiveModelRef() {
if (model_manager.hasActiveBundle()) {
return QString::fromStdString(model_manager.getActiveBundle().getRef());
}
return DEFAULT_MODEL;
}
void ModelsPanel::updateModelManagerState() {
const SubMaster &sm = *(uiStateSP()->sm);
model_manager = sm["modelManagerSP"].getModelManagerSP();
@@ -272,34 +284,31 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
currentModelLblBtn->setEnabled(false);
currentModelLblBtn->setValue(tr("Fetching models..."));
struct ModelEntry {
QString folder;
QString displayName;
int index;
};
QList<ModelEntry> sortedModels;
QList<TreeNode> sortedModels;
QSet<QString> modelFolders;
QRegularExpression re("\\(([^)]*)\\)[^(]*$");
const auto bundles = model_manager.getAvailableBundles();
for (const auto &bundle : bundles) {
auto overrides = bundle.getOverrides();
QString gen;
QString folder;
for (const auto &override : overrides) {
if (override.getKey() == "folder") {
gen = QString::fromStdString(override.getValue().cStr());
folder = QString::fromStdString(override.getValue().cStr());
}
}
modelFolders.insert(gen);
sortedModels.append(ModelEntry{
gen,
modelFolders.insert(folder);
sortedModels.append(TreeNode{
folder,
QString::fromStdString(bundle.getDisplayName()),
QString::fromStdString(bundle.getRef()),
static_cast<int>(bundle.getIndex())
});
}
std::sort(sortedModels.begin(), sortedModels.end(),
[](const ModelEntry &a, const ModelEntry &b) {
[](const TreeNode &a, const TreeNode &b) {
return a.index > b.index;
});
@@ -322,37 +331,46 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
});
// Create the final items list using sorted folders
QList<QPair<QString, QStringList>> items;
QList<TreeFolder> items;
for (const auto &folderPair : folderMaxIndices) {
QStringList folderModels;
QList<TreeNode> folderModels;
QString folder = folderPair.first;
for (const auto &model : sortedModels) {
if (model.folder == folderPair.first) {
folderModels.append(model.displayName);
if (model.index == folderPair.second) {
QRegularExpressionMatch match = re.match(model.displayName);
if (match.hasMatch()) {
folder.append(" - (Updated: ").append(match.captured(1)).append(")");
}
}
folderModels.append(model);
}
}
items.append(qMakePair(folderPair.first, folderModels));
items.append(TreeFolder{folder, folderModels});
}
items.insert(0, qMakePair(QString(""), QStringList{DEFAULT_MODEL}));
items.insert(0, TreeFolder{"", {
TreeNode{"", DEFAULT_MODEL, DEFAULT_MODEL, -1}
}});
currentModelLblBtn->setValue(GetActiveModelInternalName());
const QString selectedBundleName = TreeOptionDialog::getSelection(
tr("Select a Model"), items, GetActiveModelName(), this);
const QString selectedBundleRef = TreeOptionDialog::getSelection(
tr("Select a Model"), items, GetActiveModelRef(), QString("ModelManager_Favs"), this);
if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) {
if (selectedBundleRef.isEmpty() || !canContinueOnMeteredDialog()) {
return;
}
// Handle "Stock" selection differently
if (selectedBundleName == DEFAULT_MODEL) {
if (selectedBundleRef == DEFAULT_MODEL) {
params.remove("ModelManager_ActiveBundle");
currentModelLblBtn->setValue(tr("Default"));
showResetParamsDialog();
} else {
// Find selected bundle and initiate download
for (const auto &bundle: bundles) {
if (QString::fromStdString(bundle.getDisplayName()) == selectedBundleName) {
if (QString::fromStdString(bundle.getRef()) == selectedBundleRef) {
params.put("ModelManager_DownloadIndex", std::to_string(bundle.getIndex()));
if (bundle.getGeneration() != model_manager.getActiveBundle().getGeneration()) {
showResetParamsDialog();
@@ -20,6 +20,7 @@ public:
private:
QString GetActiveModelName();
QString GetActiveModelInternalName();
QString GetActiveModelRef();
void updateModelManagerState();
void showEvent(QShowEvent *event) override;
+1
View File
@@ -66,6 +66,7 @@ class ModelParser:
model_bundle.is20hz = bundle.get("is_20hz", False)
model_bundle.minimumSelectorVersion = int(bundle["minimum_selector_version"])
model_bundle.overrides = ModelParser._parse_overrides(bundle.get("overrides", {}))
model_bundle.ref = bundle.get("ref")
return model_bundle
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3731604f80e83a1fdb7c258baf6530b81190eeec82e6443172e84a35b7a74c02
size 1088
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c2a513b7f2da004f145b7d689654cc65137f1b146d484fbce7ce727a297b62c
size 861