mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-24 01:33:46 +08:00
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:
@@ -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}},
|
||||
|
||||
|
||||
@@ -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 ¤t, QWidget *parent) : DialogBase(parent) {
|
||||
TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList<TreeFolder> &items,
|
||||
const QString ¤t, 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 ¤t, QWidget *parent) {
|
||||
TreeOptionDialog d(prompt_text, items, current, parent);
|
||||
QString TreeOptionDialog::getSelection(const QString &prompt_text, const QList<TreeFolder> &items,
|
||||
const QString ¤t, 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;
|
||||
}
|
||||
|
||||
@@ -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 ¤t, QWidget *parent = nullptr);
|
||||
static QString getSelection(const QString &prompt_text, const QList<QPair<QString, QStringList>> &items, const QString ¤t, QWidget *parent = nullptr);
|
||||
explicit TreeOptionDialog(const QString &prompt_text, const QList<TreeFolder> &items, const QString ¤t, const QString &favParam, QWidget *parent = nullptr);
|
||||
static QString getSelection(const QString &prompt_text, const QList<TreeFolder> &items, const QString ¤t, 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;
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user