mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-02 20:12:07 +08:00
Dates and Favorites
This commit is contained in:
@@ -407,6 +407,10 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"ModelToDownload", CLEAR_ON_MANAGER_START},
|
||||
{"ModelUI", PERSISTENT},
|
||||
{"ModelVersions", PERSISTENT},
|
||||
{"ModelReleasedDates", PERSISTENT},
|
||||
{"CommunityFavorites", PERSISTENT},
|
||||
{"UserFavorites", PERSISTENT},
|
||||
{"SortModelsByDate", PERSISTENT},
|
||||
{"NavigationUI", PERSISTENT},
|
||||
{"NextMapSpeedLimit", CLEAR_ON_MANAGER_START},
|
||||
{"NewLongAPI", PERSISTENT},
|
||||
|
||||
@@ -296,7 +296,10 @@ class ModelManager:
|
||||
params.put("AvailableModels", ",".join(self.available_models))
|
||||
params.put("AvailableModelNames", ",".join([model["name"] for model in model_info]))
|
||||
params.put("AvailableModelSeries", ",".join(self.model_series))
|
||||
params.put("CommunityFavorites", ",".join([model["id"] for model in model_info if model.get("community_favorite", False)]))
|
||||
params.put("ModelReleasedDates", ",".join([model.get("released", "2023-01-01") for model in model_info]))
|
||||
params.put("ModelVersions", ",".join(self.model_versions))
|
||||
params.put("CommunityFavorites", ",".join([model["id"] for model in model_info if model.get("community_favorite", False)]))
|
||||
params.put("AvailableModelSeries", ",".join(self.model_series))
|
||||
print("Models list updated successfully")
|
||||
|
||||
|
||||
@@ -148,6 +148,8 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("AvailableModelNames", "", 1, ""),
|
||||
("AvailableModelSeries", "", 1, ""),
|
||||
("AvailableModels", "", 1, ""),
|
||||
("CommunityFavorites", "", 1, ""),
|
||||
("UserFavorites", "", 0, ""),
|
||||
("BigMap", "0", 2, "0"),
|
||||
("BlacklistedModels", "", 2, ""),
|
||||
("BlindSpotMetrics", "1", 3, "0"),
|
||||
@@ -284,8 +286,10 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("Model", DEFAULT_MODEL, 1, DEFAULT_MODEL),
|
||||
("ModelDrivesAndScores", "", 2, ""),
|
||||
("ModelRandomizer", "0", 2, "0"),
|
||||
("ModelReleasedDates", "", 1, ""),
|
||||
("ModelUI", "1", 2, "0"),
|
||||
("ModelVersions", "", 2, ""),
|
||||
("SortModelsByDate", "0", 2, "0"),
|
||||
("NavigationUI", "1", 1, "0"),
|
||||
("NavSettingLeftSide", "0", 0, "0"),
|
||||
("NavSettingTime24h", "0", 0, "0"),
|
||||
@@ -828,8 +832,11 @@ class FrogPilotVariables:
|
||||
toggle.available_models = params.get("AvailableModels", encoding="utf-8") or ""
|
||||
toggle.available_model_names = params.get("AvailableModelNames", encoding="utf-8") or ""
|
||||
toggle.available_model_series = params.get("AvailableModelSeries", encoding="utf-8") or ""
|
||||
toggle.community_favorites = params.get("CommunityFavorites", encoding="utf-8") or ""
|
||||
toggle.model_released_dates = params.get("ModelReleasedDates", encoding="utf-8") or ""
|
||||
toggle.model_versions = params.get("ModelVersions", encoding="utf-8") or ""
|
||||
toggle.available_model_series = params.get("AvailableModelSeries", encoding="utf-8") or ""
|
||||
toggle.sort_models_by_date = params.get_bool("SortModelsByDate") if tuning_level >= level["SortModelsByDate"] else default.get_bool("SortModelsByDate")
|
||||
toggle.user_favorites = params.get("UserFavorites", encoding="utf-8") or ""
|
||||
downloaded_models = [model for model in toggle.available_models.split(",") if any(MODELS_PATH.glob(f"{model}*"))]
|
||||
toggle.model_randomizer = downloaded_models and (params.get_bool("ModelRandomizer") if tuning_level >= level["ModelRandomizer"] else default.get_bool("ModelRandomizer"))
|
||||
if toggle.available_models and toggle.available_model_names and downloaded_models and toggle.model_versions:
|
||||
|
||||
@@ -1,19 +1,74 @@
|
||||
#include "frogpilot/ui/qt/offroad/expandable_multi_option_dialog.h"
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QButtonGroup>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
#include <QHBoxLayout>
|
||||
#include <QSpacerItem>
|
||||
#include <QLayout>
|
||||
#include <QLayoutItem>
|
||||
#include <QGridLayout>
|
||||
#include <QPoint>
|
||||
#include <QSize>
|
||||
#include <QSizePolicy>
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
#include <QSignalBlocker>
|
||||
#include <QScroller>
|
||||
#include <QPointer>
|
||||
#include <QObject>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "selfdrive/ui/qt/widgets/scrollview.h"
|
||||
|
||||
ExpandableMultiOptionDialog::ExpandableMultiOptionDialog(const QString &prompt_text,
|
||||
const QMap<QString, QStringList> &seriesToModels,
|
||||
const QString ¤t, QWidget *parent)
|
||||
: DialogBase(parent), seriesToModels(seriesToModels) {
|
||||
const QMap<QString, QStringList> &seriesToModels,
|
||||
const QString ¤t, QWidget *parent,
|
||||
const QStringList &userFavorites,
|
||||
const QStringList &communityFavorites,
|
||||
const QMap<QString, QString> &modelReleasedDates,
|
||||
const QMap<QString, QString> &modelFileToNameMap,
|
||||
const QString &initialSortMode)
|
||||
: DialogBase(parent), seriesToModels(seriesToModels), currentSortMode(initialSortMode.isEmpty() ? QString("alphabetical") : initialSortMode),
|
||||
userFavorites(userFavorites), communityFavorites(communityFavorites), modelReleasedDates(modelReleasedDates),
|
||||
modelFileToNameMap(modelFileToNameMap), currentSelection(current) {
|
||||
|
||||
baseSeriesToModels = seriesToModels;
|
||||
|
||||
for (auto it = this->modelFileToNameMap.constBegin(); it != this->modelFileToNameMap.constEnd(); ++it) {
|
||||
modelNameToFileMap.insert(it.value(), it.key());
|
||||
}
|
||||
|
||||
for (auto it = seriesToModels.constBegin(); it != seriesToModels.constEnd(); ++it) {
|
||||
const QStringList &models = it.value();
|
||||
for (const QString &modelName : models) {
|
||||
if (modelName.isEmpty() || modelNameToFileMap.contains(modelName)) {
|
||||
continue;
|
||||
}
|
||||
this->modelFileToNameMap.insert(modelName, modelName);
|
||||
modelNameToFileMap.insert(modelName, modelName);
|
||||
}
|
||||
}
|
||||
|
||||
currentSelectionKey = modelNameToFileMap.value(currentSelection);
|
||||
if (!currentSelectionKey.isEmpty()) {
|
||||
selectionKey = currentSelectionKey;
|
||||
selection = this->modelFileToNameMap.value(currentSelectionKey, currentSelection);
|
||||
currentSelection = selection;
|
||||
} else {
|
||||
selectionKey.clear();
|
||||
selection.clear();
|
||||
currentSelection.clear();
|
||||
}
|
||||
|
||||
if (currentSortMode != "alphabetical" && currentSortMode != "date" &&
|
||||
currentSortMode != "favorites" && currentSortMode != "date_oldest") {
|
||||
currentSortMode = "alphabetical";
|
||||
}
|
||||
|
||||
QFrame *container = new QFrame(this);
|
||||
container->setStyleSheet(R"(
|
||||
@@ -50,6 +105,43 @@ ExpandableMultiOptionDialog::ExpandableMultiOptionDialog(const QString &prompt_t
|
||||
padding-left: 80px;
|
||||
}
|
||||
QPushButton.series-header:hover { background-color: #404040; }
|
||||
QPushButton.favorite-button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 60px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
min-width: 80px;
|
||||
max-width: 80px;
|
||||
}
|
||||
QPushButton.favorite-button:hover { background-color: #404040; }
|
||||
QComboBox {
|
||||
background-color: #4F4F4F;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
font-size: 50px;
|
||||
color: white;
|
||||
min-width: 200px;
|
||||
}
|
||||
QComboBox:hover { background-color: #5A5A5A; }
|
||||
QComboBox::drop-down {
|
||||
border: none;
|
||||
width: 50px;
|
||||
}
|
||||
QComboBox::down-arrow {
|
||||
image: url("../../frogpilot/assets/toggle_icons/icon_dropdown.png");
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #4F4F4F;
|
||||
border: 2px solid #FFFFFF;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
selection-background-color: #465BEA;
|
||||
font-size: 50px;
|
||||
}
|
||||
)");
|
||||
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(container);
|
||||
@@ -60,85 +152,103 @@ ExpandableMultiOptionDialog::ExpandableMultiOptionDialog(const QString &prompt_t
|
||||
main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop);
|
||||
main_layout->addSpacing(25);
|
||||
|
||||
QWidget *listWidget = new QWidget(this);
|
||||
QVBoxLayout *listLayout = new QVBoxLayout(listWidget);
|
||||
listLayout->setSpacing(10);
|
||||
// Sort controls - simple cycling button
|
||||
QHBoxLayout *sortLayout = new QHBoxLayout();
|
||||
sortLayout->setContentsMargins(0, 0, 0, 0);
|
||||
sortLayout->setSpacing(20);
|
||||
sortLayout->addStretch(); // Push to the right
|
||||
|
||||
QButtonGroup *group = new QButtonGroup(listWidget);
|
||||
group->setExclusive(true);
|
||||
QLabel *sortLabel = new QLabel(tr("Sort by:"), this);
|
||||
sortLabel->setStyleSheet("font-size: 50px; color: white;");
|
||||
sortLayout->addWidget(sortLabel);
|
||||
|
||||
QPushButton *confirm_btn = new QPushButton(tr("Select"));
|
||||
confirm_btn->setObjectName("confirm_btn");
|
||||
confirm_btn->setEnabled(false);
|
||||
|
||||
ScrollView *scroll_view = new ScrollView(listWidget, this);
|
||||
scroll_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
|
||||
// Create series headers and their expandable content
|
||||
for (const QString &series : seriesToModels.keys()) {
|
||||
// Series header button
|
||||
QPushButton *seriesHeader = new QPushButton("▶ " + series);
|
||||
seriesHeader->setProperty("class", "series-header");
|
||||
seriesHeader->setCheckable(false);
|
||||
seriesExpanded[series] = false;
|
||||
|
||||
QObject::connect(seriesHeader, &QPushButton::clicked, [this, series, seriesHeader, scroll_view]() {
|
||||
toggleSeries(series, seriesHeader, scroll_view);
|
||||
});
|
||||
|
||||
listLayout->addWidget(seriesHeader);
|
||||
|
||||
// Container for series models (initially hidden)
|
||||
QWidget *seriesContainer = new QWidget();
|
||||
QVBoxLayout *seriesLayout = new QVBoxLayout(seriesContainer);
|
||||
seriesLayout->setContentsMargins(20, 0, 0, 0);
|
||||
seriesLayout->setSpacing(10);
|
||||
seriesContainer->hide();
|
||||
|
||||
// Add models for this series
|
||||
for (const QString &model : seriesToModels[series]) {
|
||||
QPushButton *modelButton = new QPushButton(model);
|
||||
modelButton->setCheckable(true);
|
||||
modelButton->setChecked(model == current);
|
||||
modelButton->setProperty("class", "model-option");
|
||||
|
||||
QObject::connect(modelButton, &QPushButton::toggled, [=](bool checked) mutable {
|
||||
if (checked) {
|
||||
selection = model;
|
||||
confirm_btn->setEnabled(true);
|
||||
// Manually apply selected style
|
||||
modelButton->setStyleSheet("QPushButton {"
|
||||
"background-color: #465BEA;"
|
||||
"border: 3px solid #FFFFFF;"
|
||||
"color: white;"
|
||||
"font-weight: 500;"
|
||||
"height: 135;"
|
||||
"padding: 0px 50px;"
|
||||
"text-align: left;"
|
||||
"font-size: 55px;"
|
||||
"border-radius: 10px;"
|
||||
"}");
|
||||
} else {
|
||||
if (selection == model) {
|
||||
confirm_btn->setEnabled(false);
|
||||
}
|
||||
// Reset to default style
|
||||
modelButton->setStyleSheet("");
|
||||
}
|
||||
});
|
||||
|
||||
group->addButton(modelButton);
|
||||
seriesLayout->addWidget(modelButton);
|
||||
QPushButton *sortButton = new QPushButton(tr("Alphabetical"), this);
|
||||
sortButton->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #4F4F4F;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 10px;
|
||||
padding: 10px 20px;
|
||||
font-size: 50px;
|
||||
color: white;
|
||||
min-width: 250px;
|
||||
text-align: center;
|
||||
}
|
||||
QPushButton:hover { background-color: #5A5A5A; }
|
||||
)");
|
||||
|
||||
seriesWidgets[series] = seriesContainer;
|
||||
listLayout->addWidget(seriesContainer);
|
||||
// Set initial button text based on sort mode
|
||||
if (currentSortMode == "date") {
|
||||
sortButton->setText(tr("Date (Newest)"));
|
||||
} else if (currentSortMode == "date_oldest") {
|
||||
sortButton->setText(tr("Date (Oldest)"));
|
||||
} else if (currentSortMode == "favorites") {
|
||||
sortButton->setText(tr("Favorites First"));
|
||||
} else {
|
||||
sortButton->setText(tr("Alphabetical"));
|
||||
}
|
||||
|
||||
// Add stretch to keep buttons spaced correctly
|
||||
listLayout->addStretch(1);
|
||||
QWidget *sortWidget = new QWidget(container);
|
||||
sortWidget->setLayout(sortLayout);
|
||||
sortWidget->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
|
||||
sortLayout->setSizeConstraint(QLayout::SetFixedSize);
|
||||
sortWidget->setStyleSheet("background: transparent;");
|
||||
|
||||
main_layout->addWidget(scroll_view);
|
||||
sortLayout->addWidget(sortButton);
|
||||
|
||||
auto updateSortOverlayGeometry = [sortWidget, sortLayout]() {
|
||||
if (!sortWidget) return;
|
||||
const QSize hint = sortLayout->sizeHint();
|
||||
sortWidget->setFixedSize(hint);
|
||||
};
|
||||
updateSortOverlayGeometry();
|
||||
|
||||
QObject::connect(sortButton, &QPushButton::clicked, [this, sortButton, updateSortOverlayGeometry]() {
|
||||
if (currentSortMode == "alphabetical") {
|
||||
currentSortMode = "date";
|
||||
sortButton->setText(tr("Date (Newest)"));
|
||||
} else if (currentSortMode == "date") {
|
||||
currentSortMode = "date_oldest";
|
||||
sortButton->setText(tr("Date (Oldest)"));
|
||||
} else if (currentSortMode == "date_oldest") {
|
||||
currentSortMode = "favorites";
|
||||
sortButton->setText(tr("Favorites First"));
|
||||
} else {
|
||||
currentSortMode = "alphabetical";
|
||||
sortButton->setText(tr("Alphabetical"));
|
||||
}
|
||||
updateSortOverlayGeometry();
|
||||
updateSorting();
|
||||
});
|
||||
|
||||
listWidgetContainer = new QWidget(this);
|
||||
listLayout = new QVBoxLayout(listWidgetContainer);
|
||||
listLayout->setSpacing(10);
|
||||
listLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
confirmButton = new QPushButton(tr("Select"));
|
||||
confirmButton->setObjectName("confirm_btn");
|
||||
confirmButton->setEnabled(!selectionKey.isEmpty());
|
||||
|
||||
scrollView = new ScrollView(listWidgetContainer, this);
|
||||
scrollView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
if (scrollView->viewport()) {
|
||||
scrollView->viewport()->setAttribute(Qt::WA_AcceptTouchEvents, true);
|
||||
}
|
||||
|
||||
QWidget *listContainer = new QWidget(container);
|
||||
QGridLayout *overlayLayout = new QGridLayout(listContainer);
|
||||
overlayLayout->setContentsMargins(0, 0, 0, 0);
|
||||
overlayLayout->setSpacing(0);
|
||||
overlayLayout->addWidget(scrollView, 0, 0);
|
||||
overlayLayout->setRowStretch(0, 1);
|
||||
overlayLayout->setColumnStretch(0, 1);
|
||||
overlayLayout->addWidget(sortWidget, 0, 0, Qt::AlignRight | Qt::AlignTop);
|
||||
|
||||
// Create series headers and their expandable content
|
||||
rebuildModelList(seriesToModels.keys(), seriesToModels);
|
||||
|
||||
main_layout->addWidget(listContainer);
|
||||
main_layout->addSpacing(35);
|
||||
|
||||
// Cancel + confirm buttons
|
||||
@@ -148,18 +258,25 @@ ExpandableMultiOptionDialog::ExpandableMultiOptionDialog(const QString &prompt_t
|
||||
|
||||
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);
|
||||
QObject::connect(confirmButton, &QPushButton::clicked, this, &ConfirmationDialog::accept);
|
||||
blayout->addWidget(cancel_btn);
|
||||
blayout->addWidget(confirm_btn);
|
||||
blayout->addWidget(confirmButton);
|
||||
|
||||
QVBoxLayout *outer_layout = new QVBoxLayout(this);
|
||||
outer_layout->setContentsMargins(50, 50, 50, 50);
|
||||
outer_layout->addWidget(container);
|
||||
|
||||
// Initial sorting
|
||||
updateSorting();
|
||||
}
|
||||
|
||||
void ExpandableMultiOptionDialog::toggleSeries(const QString &series, QPushButton *headerButton, ScrollView *scrollView) {
|
||||
void ExpandableMultiOptionDialog::toggleSeries(const QString &series, QPushButton *headerButton) {
|
||||
if (!headerButton) return;
|
||||
|
||||
QWidget *container = seriesWidgets.value(series, nullptr);
|
||||
if (!container) return;
|
||||
|
||||
bool expanded = seriesExpanded[series];
|
||||
QWidget *container = seriesWidgets[series];
|
||||
QString seriesName = series;
|
||||
|
||||
if (expanded) {
|
||||
@@ -171,21 +288,18 @@ void ExpandableMultiOptionDialog::toggleSeries(const QString &series, QPushButto
|
||||
seriesExpanded[series] = true;
|
||||
headerButton->setText("▼ " + seriesName);
|
||||
|
||||
// Auto-scroll to show expanded content
|
||||
// Auto-scroll to place the series at the top of the viewport when expanded
|
||||
if (scrollView) {
|
||||
QTimer::singleShot(50, [container, scrollView]() {
|
||||
QRect containerRect = container->geometry();
|
||||
QScrollBar *vScrollBar = scrollView->verticalScrollBar();
|
||||
if (vScrollBar) {
|
||||
int currentValue = vScrollBar->value();
|
||||
int containerBottom = containerRect.bottom();
|
||||
int viewportHeight = scrollView->viewport()->height();
|
||||
|
||||
// If container extends beyond viewport, scroll to show it
|
||||
if (containerBottom > currentValue + viewportHeight) {
|
||||
int targetValue = containerBottom - viewportHeight + 50; // Add some padding
|
||||
vScrollBar->setValue(targetValue);
|
||||
}
|
||||
QPointer<QPushButton> headerPtr(headerButton);
|
||||
QPointer<ScrollView> scrollPtr(scrollView);
|
||||
QTimer::singleShot(50, [headerPtr, scrollPtr]() {
|
||||
if (!scrollPtr || !headerPtr) return;
|
||||
QWidget *contents = scrollPtr->widget();
|
||||
if (!contents) return;
|
||||
if (QScrollBar *vScrollBar = scrollPtr->verticalScrollBar()) {
|
||||
QPoint headerTop = headerPtr->mapTo(contents, QPoint(0, 0));
|
||||
int targetValue = qMax(headerTop.y() - 20, 0);
|
||||
vScrollBar->setValue(targetValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -196,11 +310,457 @@ void ExpandableMultiOptionDialog::toggleSeries(const QString &series, QPushButto
|
||||
}
|
||||
|
||||
QString ExpandableMultiOptionDialog::getSelection(const QString &prompt_text,
|
||||
const QMap<QString, QStringList> &seriesToModels,
|
||||
const QString ¤t, QWidget *parent) {
|
||||
ExpandableMultiOptionDialog d = ExpandableMultiOptionDialog(prompt_text, seriesToModels, current, parent);
|
||||
const QMap<QString, QStringList> &seriesToModels,
|
||||
const QString ¤t, QWidget *parent,
|
||||
const QStringList &userFavorites,
|
||||
const QStringList &communityFavorites,
|
||||
const QMap<QString, QString> &modelReleasedDates,
|
||||
const QMap<QString, QString> &modelFileToNameMap,
|
||||
const QString &initialSortMode) {
|
||||
ExpandableMultiOptionDialog d(prompt_text, seriesToModels, current, parent,
|
||||
userFavorites, communityFavorites, modelReleasedDates, modelFileToNameMap, initialSortMode);
|
||||
if (d.exec()) {
|
||||
return d.selection;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
QStringList ExpandableMultiOptionDialog::getUserFavorites() const {
|
||||
QStringList filteredFavorites;
|
||||
for (const QString &fav : userFavorites) {
|
||||
if (modelFileToNameMap.contains(fav) && !filteredFavorites.contains(fav)) {
|
||||
filteredFavorites.append(fav);
|
||||
}
|
||||
}
|
||||
return filteredFavorites;
|
||||
}
|
||||
|
||||
void ExpandableMultiOptionDialog::stopActiveScroll() {
|
||||
if (!scrollView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QScroller *scroller = QScroller::scroller(scrollView->viewport())) {
|
||||
if (scroller->state() == QScroller::Scrolling) {
|
||||
scroller->stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExpandableMultiOptionDialog::stopActiveScrollForInteraction() {
|
||||
if (!scrollView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QScroller *scroller = QScroller::scroller(scrollView->viewport())) {
|
||||
const QScroller::State state = scroller->state();
|
||||
if (state == QScroller::Scrolling || state == QScroller::Dragging || state == QScroller::Pressed) {
|
||||
scroller->stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExpandableMultiOptionDialog::createModelButton(const QString &modelKey, const QString &modelName, const QString &displayName,
|
||||
QVBoxLayout *layout) {
|
||||
QString effectiveKey = modelKey.isEmpty() ? modelName : modelKey;
|
||||
if (effectiveKey.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!modelFileToNameMap.contains(effectiveKey)) {
|
||||
const QString storedName = !modelName.isEmpty() ? modelName : displayName;
|
||||
modelFileToNameMap.insert(effectiveKey, storedName);
|
||||
}
|
||||
|
||||
if (!modelName.isEmpty()) {
|
||||
modelNameToFileMap.insert(modelName, effectiveKey);
|
||||
}
|
||||
|
||||
QWidget *modelWidget = new QWidget();
|
||||
QHBoxLayout *modelLayout = new QHBoxLayout(modelWidget);
|
||||
modelLayout->setContentsMargins(0, 0, 0, 0);
|
||||
modelLayout->setSpacing(10);
|
||||
|
||||
// Star button
|
||||
QPushButton *starButton = new QPushButton();
|
||||
starButton->setProperty("class", "favorite-button");
|
||||
starButton->setCheckable(true);
|
||||
starButton->setCursor(Qt::PointingHandCursor);
|
||||
starButton->setFocusPolicy(Qt::NoFocus);
|
||||
|
||||
// Check if this model is a favorite
|
||||
bool isCommunityFav = communityFavorites.contains(effectiveKey);
|
||||
bool isUserFav = userFavorites.contains(effectiveKey);
|
||||
bool isFavorite = isCommunityFav || isUserFav;
|
||||
|
||||
starButton->setChecked(isFavorite);
|
||||
starButton->setText(isFavorite ? QString::fromUtf16(u"\u2665") : QString::fromUtf16(u"\u2661"));
|
||||
|
||||
QObject::connect(starButton, &QPushButton::clicked, [this, effectiveKey]() {
|
||||
stopActiveScrollForInteraction();
|
||||
toggleFavorite(effectiveKey);
|
||||
});
|
||||
|
||||
favoriteButtons[effectiveKey].append(starButton);
|
||||
modelLayout->addWidget(starButton);
|
||||
|
||||
// Model button
|
||||
QPushButton *modelButton = new QPushButton(displayName);
|
||||
modelButton->setCheckable(true);
|
||||
modelButton->setProperty("class", "model-option");
|
||||
modelButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
modelButton->setCursor(Qt::PointingHandCursor);
|
||||
modelButton->setFocusPolicy(Qt::NoFocus);
|
||||
modelButton->setProperty("modelKey", effectiveKey);
|
||||
modelButton->setProperty("modelName", modelName);
|
||||
|
||||
modelButtons[effectiveKey].append(modelButton);
|
||||
if (selectionKey == effectiveKey && currentSelectionButton.isNull()) {
|
||||
currentSelectionButton = modelButton;
|
||||
}
|
||||
modelLayout->addWidget(modelButton);
|
||||
|
||||
const QString resolvedSelection = modelFileToNameMap.value(effectiveKey, !modelName.isEmpty() ? modelName : displayName);
|
||||
|
||||
QObject::connect(modelButton, &QPushButton::clicked, this, [this, effectiveKey, modelButton, resolvedSelection]() {
|
||||
stopActiveScrollForInteraction();
|
||||
selectionKey = effectiveKey;
|
||||
currentSelectionKey = effectiveKey;
|
||||
selection = resolvedSelection;
|
||||
currentSelection = resolvedSelection;
|
||||
currentSelectionButton = modelButton;
|
||||
if (confirmButton) {
|
||||
confirmButton->setEnabled(true);
|
||||
}
|
||||
|
||||
updateButtonStyles();
|
||||
});
|
||||
|
||||
layout->addWidget(modelWidget);
|
||||
}
|
||||
|
||||
void ExpandableMultiOptionDialog::toggleFavorite(const QString &modelKey) {
|
||||
// Update local state
|
||||
if (modelKey.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (userFavorites.contains(modelKey)) {
|
||||
userFavorites.removeAll(modelKey);
|
||||
} else {
|
||||
userFavorites.append(modelKey);
|
||||
}
|
||||
|
||||
updateSorting();
|
||||
}
|
||||
|
||||
void ExpandableMultiOptionDialog::updateSorting() {
|
||||
const QString favoritesSeriesName = QStringLiteral("♥ Favorites");
|
||||
QMap<QString, QStringList> newSeriesToModels;
|
||||
QStringList orderedSeries;
|
||||
QSet<QString> validSeries;
|
||||
QSet<QString> favoriteModelKeys;
|
||||
QSet<QString> availableModelKeys;
|
||||
displayOverrides.clear();
|
||||
|
||||
const bool sortByDate = (currentSortMode == "date" || currentSortMode == "date_oldest");
|
||||
const bool sortDateNewestFirst = (currentSortMode == "date");
|
||||
|
||||
for (auto it = baseSeriesToModels.constBegin(); it != baseSeriesToModels.constEnd(); ++it) {
|
||||
const QStringList &models = it.value();
|
||||
for (const QString &modelName : models) {
|
||||
const QString modelKey = modelNameToFileMap.value(modelName, modelName);
|
||||
if (!modelKey.isEmpty()) {
|
||||
availableModelKeys.insert(modelKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSortMode == "favorites") {
|
||||
QStringList favoritesList;
|
||||
|
||||
for (const QString &modelKey : communityFavorites) {
|
||||
if (availableModelKeys.contains(modelKey)) {
|
||||
const QString modelName = modelFileToNameMap.value(modelKey);
|
||||
favoritesList.append(modelName);
|
||||
favoriteModelKeys.insert(modelKey);
|
||||
displayOverrides.insert(modelKey, tr("%1 (Community Fav)").arg(modelName));
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &modelKey : userFavorites) {
|
||||
if (availableModelKeys.contains(modelKey) && !favoriteModelKeys.contains(modelKey)) {
|
||||
favoritesList.append(modelFileToNameMap.value(modelKey));
|
||||
favoriteModelKeys.insert(modelKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (!favoritesList.isEmpty()) {
|
||||
std::sort(favoritesList.begin(), favoritesList.end());
|
||||
newSeriesToModels.insert(favoritesSeriesName, favoritesList);
|
||||
orderedSeries.append(favoritesSeriesName);
|
||||
validSeries.insert(favoritesSeriesName);
|
||||
seriesExpanded.insert(favoritesSeriesName, true);
|
||||
} else {
|
||||
seriesExpanded.remove(favoritesSeriesName);
|
||||
}
|
||||
} else {
|
||||
seriesExpanded.remove(favoritesSeriesName);
|
||||
}
|
||||
|
||||
struct SeriesInfo {
|
||||
QString name;
|
||||
QStringList models;
|
||||
QString newestDate;
|
||||
QString oldestDate;
|
||||
};
|
||||
|
||||
QVector<SeriesInfo> seriesInfos;
|
||||
|
||||
for (auto it = baseSeriesToModels.constBegin(); it != baseSeriesToModels.constEnd(); ++it) {
|
||||
QString series = it.key();
|
||||
QStringList models = it.value();
|
||||
|
||||
if (sortByDate) {
|
||||
std::sort(models.begin(), models.end(), [this, sortDateNewestFirst](const QString &a, const QString &b) {
|
||||
QString keyA = modelNameToFileMap.value(a, a);
|
||||
QString keyB = modelNameToFileMap.value(b, b);
|
||||
QString dateA = modelReleasedDates.value(keyA, QStringLiteral("1970-01-01"));
|
||||
QString dateB = modelReleasedDates.value(keyB, QStringLiteral("1970-01-01"));
|
||||
if (dateA == dateB) {
|
||||
return a < b;
|
||||
}
|
||||
return sortDateNewestFirst ? (dateA > dateB) : (dateA < dateB);
|
||||
});
|
||||
} else {
|
||||
std::sort(models.begin(), models.end());
|
||||
}
|
||||
|
||||
if (currentSortMode == "favorites" && !favoriteModelKeys.isEmpty()) {
|
||||
QStringList filteredModels;
|
||||
for (const QString &modelName : models) {
|
||||
QString key = modelNameToFileMap.value(modelName, modelName);
|
||||
if (!favoriteModelKeys.contains(key)) {
|
||||
filteredModels.append(modelName);
|
||||
}
|
||||
}
|
||||
models = filteredModels;
|
||||
}
|
||||
|
||||
if (models.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString newestDate = QStringLiteral("1970-01-01");
|
||||
QString oldestDate = QStringLiteral("1970-01-01");
|
||||
bool hasDate = false;
|
||||
for (const QString &modelName : models) {
|
||||
const QString key = modelNameToFileMap.value(modelName, modelName);
|
||||
const QString date = modelReleasedDates.value(key, QStringLiteral("1970-01-01"));
|
||||
if (!hasDate) {
|
||||
newestDate = date;
|
||||
oldestDate = date;
|
||||
hasDate = true;
|
||||
} else {
|
||||
if (date > newestDate) {
|
||||
newestDate = date;
|
||||
}
|
||||
if (date < oldestDate) {
|
||||
oldestDate = date;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDate) {
|
||||
oldestDate = QStringLiteral("1970-01-01");
|
||||
}
|
||||
|
||||
seriesInfos.push_back({series, models, newestDate, oldestDate});
|
||||
newSeriesToModels.insert(series, models);
|
||||
}
|
||||
|
||||
if (sortByDate) {
|
||||
std::sort(seriesInfos.begin(), seriesInfos.end(), [sortDateNewestFirst](const SeriesInfo &a, const SeriesInfo &b) {
|
||||
if (sortDateNewestFirst) {
|
||||
if (a.newestDate == b.newestDate) {
|
||||
return a.name < b.name;
|
||||
}
|
||||
return a.newestDate > b.newestDate;
|
||||
} else {
|
||||
if (a.oldestDate == b.oldestDate) {
|
||||
return a.name < b.name;
|
||||
}
|
||||
return a.oldestDate < b.oldestDate;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
std::sort(seriesInfos.begin(), seriesInfos.end(), [](const SeriesInfo &a, const SeriesInfo &b) {
|
||||
return a.name < b.name;
|
||||
});
|
||||
}
|
||||
|
||||
for (const SeriesInfo &info : seriesInfos) {
|
||||
orderedSeries.append(info.name);
|
||||
validSeries.insert(info.name);
|
||||
}
|
||||
|
||||
for (auto it = seriesExpanded.begin(); it != seriesExpanded.end(); ) {
|
||||
if (!validSeries.contains(it.key())) {
|
||||
it = seriesExpanded.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
rebuildModelList(orderedSeries, newSeriesToModels);
|
||||
refreshFavoriteIcons();
|
||||
}
|
||||
|
||||
void ExpandableMultiOptionDialog::rebuildModelList(const QStringList &orderedSeries, const QMap<QString, QStringList> &newSeriesToModels) {
|
||||
if (!listLayout) return;
|
||||
|
||||
stopActiveScroll();
|
||||
|
||||
while (QLayoutItem *item = listLayout->takeAt(0)) {
|
||||
if (QWidget *w = item->widget()) {
|
||||
delete w;
|
||||
} else if (QLayout *layout = item->layout()) {
|
||||
delete layout;
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
|
||||
seriesWidgets.clear();
|
||||
modelButtons.clear();
|
||||
favoriteButtons.clear();
|
||||
currentSelectionButton = nullptr;
|
||||
|
||||
for (const QString &series : orderedSeries) {
|
||||
const QStringList models = newSeriesToModels.value(series);
|
||||
if (models.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QPushButton *seriesHeader = new QPushButton("▶ " + series);
|
||||
seriesHeader->setProperty("class", "series-header");
|
||||
seriesHeader->setCheckable(false);
|
||||
|
||||
bool expanded = seriesExpanded.value(series, false);
|
||||
seriesExpanded.insert(series, expanded);
|
||||
|
||||
QObject::connect(seriesHeader, &QPushButton::clicked, [this, series, seriesHeader]() {
|
||||
toggleSeries(series, seriesHeader);
|
||||
});
|
||||
|
||||
QWidget *seriesContainer = new QWidget();
|
||||
QVBoxLayout *seriesLayout = new QVBoxLayout(seriesContainer);
|
||||
seriesLayout->setContentsMargins(20, 0, 0, 0);
|
||||
seriesLayout->setSpacing(10);
|
||||
|
||||
for (const QString &modelName : models) {
|
||||
QString modelKey = modelNameToFileMap.value(modelName, modelName);
|
||||
if (!modelFileToNameMap.contains(modelKey)) {
|
||||
modelFileToNameMap.insert(modelKey, modelName);
|
||||
}
|
||||
QString displayName = displayOverrides.value(modelKey, modelName);
|
||||
createModelButton(modelKey, modelName, displayName, seriesLayout);
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
seriesContainer->show();
|
||||
seriesHeader->setText("▼ " + series);
|
||||
} else {
|
||||
seriesContainer->hide();
|
||||
seriesHeader->setText("▶ " + series);
|
||||
}
|
||||
|
||||
seriesWidgets.insert(series, seriesContainer);
|
||||
|
||||
listLayout->addWidget(seriesHeader);
|
||||
listLayout->addWidget(seriesContainer);
|
||||
}
|
||||
|
||||
listLayout->addStretch(1);
|
||||
|
||||
seriesToModels = newSeriesToModels;
|
||||
|
||||
listWidgetContainer->updateGeometry();
|
||||
listWidgetContainer->adjustSize();
|
||||
if (scrollView && scrollView->widget()) {
|
||||
scrollView->widget()->updateGeometry();
|
||||
scrollView->widget()->adjustSize();
|
||||
}
|
||||
|
||||
updateButtonStyles();
|
||||
}
|
||||
|
||||
void ExpandableMultiOptionDialog::refreshFavoriteIcons() {
|
||||
for (auto it = favoriteButtons.begin(); it != favoriteButtons.end(); ++it) {
|
||||
const QString &modelKey = it.key();
|
||||
const QList<QPushButton*> &buttons = it.value();
|
||||
bool isCommunityFav = communityFavorites.contains(modelKey);
|
||||
bool isUserFav = userFavorites.contains(modelKey);
|
||||
bool isFavorite = isCommunityFav || isUserFav;
|
||||
|
||||
for (QPushButton *button : buttons) {
|
||||
if (!button) continue;
|
||||
button->setChecked(isFavorite);
|
||||
button->setText(isFavorite ? QString::fromUtf16(u"\u2665") : QString::fromUtf16(u"\u2661"));
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmButton && !selectionKey.isEmpty()) {
|
||||
confirmButton->setEnabled(true);
|
||||
}
|
||||
|
||||
updateButtonStyles();
|
||||
}
|
||||
|
||||
void ExpandableMultiOptionDialog::updateButtonStyles() {
|
||||
const QString selectedKey = selectionKey;
|
||||
const QString selectedStyle = QStringLiteral(
|
||||
"QPushButton {"
|
||||
"background-color: #465BEA;"
|
||||
"border: 3px solid #FFFFFF;"
|
||||
"color: white;"
|
||||
"font-weight: 500;"
|
||||
"height: 135;"
|
||||
"padding: 0px 50px;"
|
||||
"text-align: left;"
|
||||
"font-size: 55px;"
|
||||
"border-radius: 10px;"
|
||||
"}");
|
||||
|
||||
if (selectedKey.isEmpty()) {
|
||||
currentSelectionButton = nullptr;
|
||||
}
|
||||
|
||||
QPushButton *explicitButton = currentSelectionButton.data();
|
||||
if (explicitButton && explicitButton->property("modelKey").toString() != selectedKey) {
|
||||
explicitButton = nullptr;
|
||||
}
|
||||
|
||||
for (auto it = modelButtons.begin(); it != modelButtons.end(); ++it) {
|
||||
const QString &modelKey = it.key();
|
||||
const QList<QPushButton*> &buttons = it.value();
|
||||
const bool keyMatches = (!selectedKey.isEmpty() && modelKey == selectedKey);
|
||||
bool activatedForKey = false;
|
||||
|
||||
for (QPushButton *button : buttons) {
|
||||
if (!button) continue;
|
||||
|
||||
bool isActive = false;
|
||||
if (explicitButton) {
|
||||
isActive = (button == explicitButton);
|
||||
} else if (keyMatches && !activatedForKey) {
|
||||
isActive = true;
|
||||
activatedForKey = true;
|
||||
currentSelectionButton = button;
|
||||
}
|
||||
|
||||
QSignalBlocker blocker(button);
|
||||
button->setChecked(isActive);
|
||||
button->setStyleSheet(isActive ? selectedStyle : QString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,23 +6,71 @@
|
||||
#include <QWidget>
|
||||
#include <QMap>
|
||||
#include <QList>
|
||||
#include <QPointer>
|
||||
#include <QComboBox>
|
||||
#include <QMenu>
|
||||
|
||||
#include "selfdrive/ui/qt/widgets/input.h"
|
||||
#include "selfdrive/ui/qt/widgets/scrollview.h"
|
||||
|
||||
class QPushButton;
|
||||
class ExpandableMultiOptionDialog : public DialogBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ExpandableMultiOptionDialog(const QString &prompt_text, const QMap<QString, QStringList> &seriesToModels,
|
||||
const QString ¤t, QWidget *parent);
|
||||
const QString ¤t, QWidget *parent,
|
||||
const QStringList &userFavorites = QStringList(),
|
||||
const QStringList &communityFavorites = QStringList(),
|
||||
const QMap<QString, QString> &modelReleasedDates = QMap<QString, QString>(),
|
||||
const QMap<QString, QString> &modelFileToNameMap = QMap<QString, QString>(),
|
||||
const QString &initialSortMode = "alphabetical");
|
||||
static QString getSelection(const QString &prompt_text, const QMap<QString, QStringList> &seriesToModels,
|
||||
const QString ¤t, QWidget *parent);
|
||||
const QString ¤t, QWidget *parent,
|
||||
const QStringList &userFavorites = QStringList(),
|
||||
const QStringList &communityFavorites = QStringList(),
|
||||
const QMap<QString, QString> &modelReleasedDates = QMap<QString, QString>(),
|
||||
const QMap<QString, QString> &modelFileToNameMap = QMap<QString, QString>(),
|
||||
const QString &initialSortMode = QString());
|
||||
QString selection;
|
||||
|
||||
QString getCurrentSortMode() const { return currentSortMode; }
|
||||
QStringList getUserFavorites() const;
|
||||
|
||||
private:
|
||||
void toggleSeries(const QString &series, QPushButton *headerButton, ScrollView *scrollView);
|
||||
void toggleSeries(const QString &series, QPushButton *headerButton);
|
||||
void toggleFavorite(const QString &modelKey);
|
||||
void updateSorting();
|
||||
void rebuildModelList(const QStringList &orderedSeries, const QMap<QString, QStringList> &newSeriesToModels);
|
||||
void createModelButton(const QString &modelKey, const QString &modelName, const QString &displayName,
|
||||
QVBoxLayout *layout);
|
||||
void refreshFavoriteIcons();
|
||||
void updateButtonStyles();
|
||||
void stopActiveScroll();
|
||||
void stopActiveScrollForInteraction();
|
||||
|
||||
QMap<QString, QStringList> seriesToModels;
|
||||
QMap<QString, QStringList> baseSeriesToModels;
|
||||
QMap<QString, QWidget*> seriesWidgets;
|
||||
QMap<QString, bool> seriesExpanded;
|
||||
};
|
||||
QMap<QString, QList<QPushButton*>> modelButtons;
|
||||
QMap<QString, QList<QPushButton*>> favoriteButtons;
|
||||
|
||||
QStringList userFavorites;
|
||||
QStringList communityFavorites;
|
||||
QMap<QString, QString> modelReleasedDates;
|
||||
QMap<QString, QString> modelFileToNameMap;
|
||||
QMap<QString, QString> modelNameToFileMap;
|
||||
QMap<QString, QString> displayOverrides;
|
||||
|
||||
QString currentSortMode;
|
||||
QString currentSelection;
|
||||
QString currentSelectionKey;
|
||||
QString selectionKey;
|
||||
|
||||
ScrollView *scrollView = nullptr;
|
||||
QVBoxLayout *listLayout = nullptr;
|
||||
QPushButton *confirmButton = nullptr;
|
||||
QWidget *listWidgetContainer = nullptr;
|
||||
QPointer<QPushButton> currentSelectionButton;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "frogpilot/ui/qt/offroad/model_settings.h"
|
||||
#include "frogpilot/ui/qt/offroad/expandable_multi_option_dialog.h"
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QPushButton>
|
||||
#include <QDialog>
|
||||
#include <algorithm>
|
||||
|
||||
FrogPilotModelPanel::FrogPilotModelPanel(FrogPilotSettingsWindow *parent) : FrogPilotListWidget(parent), parent(parent) {
|
||||
QStackedLayout *modelLayout = new QStackedLayout();
|
||||
@@ -41,56 +44,72 @@ FrogPilotModelPanel::FrogPilotModelPanel(FrogPilotSettingsWindow *parent) : Frog
|
||||
if (param == "DeleteModel") {
|
||||
deleteModelButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DELETE ALL")});
|
||||
QObject::connect(deleteModelButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) {
|
||||
QStringList deletableModels;
|
||||
for (const QString &file : modelDir.entryList(QDir::Files)) {
|
||||
QString base = QFileInfo(file).baseName();
|
||||
for (const QString &modelKey : modelFileToNameMapProcessed.keys()) {
|
||||
if (base.startsWith(modelKey)) {
|
||||
QString modelName = modelFileToNameMapProcessed.value(modelKey);
|
||||
if (!deletableModels.contains(modelName)) {
|
||||
deletableModels.append(modelName);
|
||||
}
|
||||
}
|
||||
}
|
||||
QMap<QString, QString> deletableModelsMap = getDeletableModelDisplayNames();
|
||||
noModelsDownloaded = deletableModelsMap.isEmpty();
|
||||
|
||||
if (noModelsDownloaded) {
|
||||
return;
|
||||
}
|
||||
deletableModels.removeAll(processModelName(currentModel));
|
||||
deletableModels.removeAll(modelFileToNameMapProcessed.value(QString::fromStdString(params_default.get("Model"))));
|
||||
deletableModels.removeAll("Space Lab");
|
||||
noModelsDownloaded = deletableModels.isEmpty();
|
||||
|
||||
if (id == 0) {
|
||||
// Group deletable models by series
|
||||
// Group deletable models by series and keep a lookup for selected names
|
||||
QMap<QString, QStringList> deletableSeriesToModels;
|
||||
for (const QString &modelName : deletableModels) {
|
||||
QString modelKey = modelFileToNameMapProcessed.key(modelName);
|
||||
QString series = modelSeriesMap.value(modelKey, "Custom Series");
|
||||
deletableSeriesToModels[series].append(modelName);
|
||||
QMap<QString, QString> displayNameToKey;
|
||||
QMap<QString, QString> deletableFileToNameMap;
|
||||
for (auto it = deletableModelsMap.constBegin(); it != deletableModelsMap.constEnd(); ++it) {
|
||||
const QString &modelKey = it.key();
|
||||
const QString &displayName = it.value();
|
||||
QString series = modelSeriesMap.value(modelKey, tr("Custom Series"));
|
||||
deletableSeriesToModels[series].append(displayName);
|
||||
displayNameToKey.insert(displayName, modelKey);
|
||||
deletableFileToNameMap.insert(modelKey, displayName);
|
||||
}
|
||||
|
||||
// Sort models within each series
|
||||
for (QString &series : deletableSeriesToModels.keys()) {
|
||||
deletableSeriesToModels[series].sort();
|
||||
QStringList &models = deletableSeriesToModels[series];
|
||||
models.removeDuplicates();
|
||||
std::sort(models.begin(), models.end());
|
||||
}
|
||||
|
||||
QString modelToDelete = ExpandableMultiOptionDialog::getSelection(tr("Select a driving model to delete"), deletableSeriesToModels, "", this);
|
||||
if (!modelToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Are you sure you want to delete the \"%1\" model?").arg(modelToDelete), tr("Delete"), this)) {
|
||||
QString modelFile = modelFileToNameMapProcessed.key(modelToDelete);
|
||||
for (const QString &file : modelDir.entryList(QDir::Files)) {
|
||||
QString base = QFileInfo(file).baseName();
|
||||
if (base.startsWith(modelFile)) {
|
||||
QFile::remove(modelDir.filePath(file));
|
||||
QString savedSortMode = QString::fromStdString(params.get("ModelSortMode"));
|
||||
if (savedSortMode.isEmpty()) savedSortMode = "alphabetical";
|
||||
|
||||
QString modelToDelete = ExpandableMultiOptionDialog::getSelection(tr("Select a driving model to delete"), deletableSeriesToModels, "", this,
|
||||
QStringList(), QStringList(), QMap<QString, QString>(),
|
||||
deletableFileToNameMap, savedSortMode);
|
||||
if (!modelToDelete.isEmpty()) {
|
||||
QString modelKey = displayNameToKey.value(modelToDelete);
|
||||
if (modelKey.isEmpty()) {
|
||||
QString processedName = processModelName(modelToDelete);
|
||||
for (auto it = deletableModelsMap.constBegin(); it != deletableModelsMap.constEnd(); ++it) {
|
||||
if (processModelName(it.value()) == processedName) {
|
||||
modelKey = it.key();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allModelsDownloaded = false;
|
||||
if (!modelKey.isEmpty() && ConfirmationDialog::confirm(tr("Are you sure you want to delete the \"%1\" model?").arg(modelToDelete), tr("Delete"), this)) {
|
||||
for (const QString &file : modelDir.entryList(QDir::Files)) {
|
||||
QString base = QFileInfo(file).baseName();
|
||||
if (base.startsWith(modelKey)) {
|
||||
QFile::remove(modelDir.filePath(file));
|
||||
}
|
||||
}
|
||||
|
||||
allModelsDownloaded = false;
|
||||
noModelsDownloaded = getDeletableModelDisplayNames().isEmpty();
|
||||
deleteModelButton->setEnabled(!(allModelsDownloading || modelDownloading || noModelsDownloaded));
|
||||
}
|
||||
}
|
||||
} else if (id == 1) {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to delete all of your downloaded driving models?"), tr("Delete"), this)) {
|
||||
const QList<QString> deletableKeys = deletableModelsMap.keys();
|
||||
for (const QString &file : modelDir.entryList(QDir::Files)) {
|
||||
QString base = QFileInfo(file).baseName();
|
||||
for (const QString &modelKey : modelFileToNameMapProcessed.keys()) {
|
||||
QString modelName = modelFileToNameMapProcessed.value(modelKey);
|
||||
if (deletableModels.contains(modelName) && base.startsWith(modelKey)) {
|
||||
for (const QString &modelKey : deletableKeys) {
|
||||
if (base.startsWith(modelKey)) {
|
||||
QFile::remove(modelDir.filePath(file));
|
||||
break;
|
||||
}
|
||||
@@ -99,6 +118,7 @@ FrogPilotModelPanel::FrogPilotModelPanel(FrogPilotSettingsWindow *parent) : Frog
|
||||
|
||||
allModelsDownloaded = false;
|
||||
noModelsDownloaded = true;
|
||||
deleteModelButton->setEnabled(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -106,66 +126,70 @@ FrogPilotModelPanel::FrogPilotModelPanel(FrogPilotSettingsWindow *parent) : Frog
|
||||
} else if (param == "DownloadModel") {
|
||||
downloadModelButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DOWNLOAD"), tr("DOWNLOAD ALL")});
|
||||
QObject::connect(downloadModelButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) {
|
||||
auto isInstalled = [this](const QString &key) {
|
||||
bool has_thneed = false;
|
||||
bool has_policy_meta = false;
|
||||
bool has_policy_tg = false;
|
||||
bool has_vision_meta = false;
|
||||
bool has_vision_tg = false;
|
||||
|
||||
for (const QString &file : modelDir.entryList(QDir::Files)) {
|
||||
QFileInfo fi(modelDir.filePath(file));
|
||||
const QString base = fi.baseName();
|
||||
const QString ext = fi.suffix();
|
||||
if (!(base.startsWith(key) || base.startsWith(key + "_"))) continue;
|
||||
|
||||
if (ext == "thneed") {
|
||||
// Classic model (WD-40 etc.)
|
||||
has_thneed = true;
|
||||
} else if (ext == "pkl") {
|
||||
// TinyGrad bundle uses these four exact suffixes
|
||||
if (base.contains("_driving_policy_metadata")) has_policy_meta = true;
|
||||
else if (base.contains("_driving_policy_tinygrad")) has_policy_tg = true;
|
||||
else if (base.contains("_driving_vision_metadata")) has_vision_meta = true;
|
||||
else if (base.contains("_driving_vision_tinygrad")) has_vision_tg = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Classic models: any matching .thneed counts as installed
|
||||
if (has_thneed) return true;
|
||||
// TinyGrad models: require all four policy/vision files to be present
|
||||
return has_policy_meta && has_policy_tg && has_vision_meta && has_vision_tg;
|
||||
};
|
||||
if (id == 0) {
|
||||
if (modelDownloading) {
|
||||
params_memory.putBool("CancelModelDownload", true);
|
||||
|
||||
cancellingDownload = true;
|
||||
} else {
|
||||
QStringList downloadableModels = availableModelNames;
|
||||
for (const QString &modelKey : modelFileToNameMap.keys()) {
|
||||
QString modelName = modelFileToNameMap.value(modelKey);
|
||||
if (isInstalled(modelKey)) {
|
||||
downloadableModels.removeAll(modelName);
|
||||
}
|
||||
}
|
||||
downloadableModels.removeAll("Space Lab 👀📡");
|
||||
allModelsDownloaded = downloadableModels.isEmpty();
|
||||
} else {
|
||||
QMap<QString, QStringList> downloadableSeriesToModels;
|
||||
QStringList downloadableModelNames;
|
||||
|
||||
// Group downloadable models by series
|
||||
QMap<QString, QStringList> downloadableSeriesToModels;
|
||||
for (const QString &modelName : downloadableModels) {
|
||||
QString modelKey = modelFileToNameMap.key(modelName);
|
||||
QString series = modelSeriesMap.value(modelKey, "Custom Series");
|
||||
downloadableSeriesToModels[series].append(modelName);
|
||||
for (auto it = modelFileToNameMap.constBegin(); it != modelFileToNameMap.constEnd(); ++it) {
|
||||
const QString &modelKey = it.key();
|
||||
const QString &modelName = it.value();
|
||||
if (modelName.isEmpty() || isModelInstalled(modelKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sort models within each series
|
||||
for (QString &series : downloadableSeriesToModels.keys()) {
|
||||
downloadableSeriesToModels[series].sort();
|
||||
QString series = modelSeriesMap.value(modelKey, tr("Custom Series"));
|
||||
downloadableSeriesToModels[series].append(modelName);
|
||||
if (!downloadableModelNames.contains(modelName)) {
|
||||
downloadableModelNames.append(modelName);
|
||||
}
|
||||
}
|
||||
|
||||
QString modelToDownload = ExpandableMultiOptionDialog::getSelection(tr("Select a driving model to download"), downloadableSeriesToModels, "", this);
|
||||
allModelsDownloaded = downloadableModelNames.isEmpty();
|
||||
if (allModelsDownloaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (QString &series : downloadableSeriesToModels.keys()) {
|
||||
QStringList &models = downloadableSeriesToModels[series];
|
||||
models.removeDuplicates();
|
||||
std::sort(models.begin(), models.end());
|
||||
}
|
||||
|
||||
QStringList userFavorites = QString::fromStdString(params.get("UserFavorites")).split(",");
|
||||
userFavorites.removeAll("");
|
||||
|
||||
QStringList communityFavorites = QString::fromStdString(params.get("CommunityFavorites")).split(",");
|
||||
communityFavorites.removeAll("");
|
||||
|
||||
QString savedSortMode = QString::fromStdString(params.get("ModelSortMode"));
|
||||
if (savedSortMode.isEmpty()) savedSortMode = "alphabetical";
|
||||
|
||||
ExpandableMultiOptionDialog dialog(
|
||||
tr("Select a driving model to download"),
|
||||
downloadableSeriesToModels,
|
||||
"",
|
||||
this,
|
||||
userFavorites,
|
||||
communityFavorites,
|
||||
modelReleasedDates,
|
||||
modelFileToNameMap,
|
||||
savedSortMode);
|
||||
|
||||
int dialogResult = dialog.exec();
|
||||
|
||||
QString sortMode = dialog.getCurrentSortMode();
|
||||
QStringList newUserFavs = dialog.getUserFavorites();
|
||||
params.put("ModelSortMode", sortMode.toStdString());
|
||||
params.put("UserFavorites", newUserFavs.join(",").toStdString());
|
||||
userFavorites = newUserFavs;
|
||||
|
||||
if (dialogResult == QDialog::Accepted) {
|
||||
QString modelToDownload = dialog.selection;
|
||||
if (!modelToDownload.isEmpty()) {
|
||||
QString modelKey = modelFileToNameMap.key(modelToDownload);
|
||||
params_memory.put("ModelToDownload", modelKey.toStdString());
|
||||
@@ -182,15 +206,16 @@ FrogPilotModelPanel::FrogPilotModelPanel(FrogPilotSettingsWindow *parent) : Frog
|
||||
}
|
||||
}
|
||||
}
|
||||
params_memory.put("ModelDownloadProgress", "Downloading...");
|
||||
params_memory.put("ModelDownloadProgress", "Downloading...");
|
||||
|
||||
downloadModelButton->setText(0, tr("CANCEL"));
|
||||
downloadModelButton->setText(0, tr("CANCEL"));
|
||||
|
||||
downloadModelButton->setValue("Downloading...");
|
||||
downloadModelButton->setValue("Downloading...");
|
||||
|
||||
downloadModelButton->setVisibleButton(1, false);
|
||||
downloadModelButton->setVisibleButton(1, false);
|
||||
|
||||
modelDownloading = true;
|
||||
modelDownloading = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (id == 1) {
|
||||
@@ -308,57 +333,32 @@ FrogPilotModelPanel::FrogPilotModelPanel(FrogPilotSettingsWindow *parent) : Frog
|
||||
} else if (param == "SelectModel") {
|
||||
selectModelButton = new ButtonControl(title, tr("SELECT"), desc);
|
||||
QObject::connect(selectModelButton, &ButtonControl::clicked, [this]() {
|
||||
auto isInstalled = [this](const QString &key) {
|
||||
bool has_thneed = false;
|
||||
bool has_policy_meta = false;
|
||||
bool has_policy_tg = false;
|
||||
bool has_vision_meta = false;
|
||||
bool has_vision_tg = false;
|
||||
// Group models by series for the enhanced dialog
|
||||
QMap<QString, QStringList> seriesToModels;
|
||||
QMap<QString, QString> installedModelFileToNameMap;
|
||||
QMap<QString, QString> installedReleasedDates;
|
||||
|
||||
for (const QString &file : modelDir.entryList(QDir::Files)) {
|
||||
QFileInfo fi(modelDir.filePath(file));
|
||||
const QString base = fi.baseName();
|
||||
const QString ext = fi.suffix();
|
||||
if (!(base.startsWith(key) || base.startsWith(key + "_"))) continue;
|
||||
|
||||
if (ext == "thneed") {
|
||||
// Classic model (WD-40 etc.)
|
||||
has_thneed = true;
|
||||
} else if (ext == "pkl") {
|
||||
// TinyGrad bundle uses these four exact suffixes
|
||||
if (base.contains("_driving_policy_metadata")) has_policy_meta = true;
|
||||
else if (base.contains("_driving_policy_tinygrad")) has_policy_tg = true;
|
||||
else if (base.contains("_driving_vision_metadata")) has_vision_meta = true;
|
||||
else if (base.contains("_driving_vision_tinygrad")) has_vision_tg = true;
|
||||
}
|
||||
// Add all available models by series
|
||||
for (const QString &modelKey : modelFileToNameMap.keys()) {
|
||||
if (!isModelInstalled(modelKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Classic models: any matching .thneed counts as installed
|
||||
if (has_thneed) return true;
|
||||
// TinyGrad models: require all four policy/vision files to be present
|
||||
return has_policy_meta && has_policy_tg && has_vision_meta && has_vision_tg;
|
||||
};
|
||||
// Group models by series
|
||||
QMap<QString, QStringList> seriesToModels;
|
||||
for (const QString &modelKey : modelFileToNameMap.keys()) {
|
||||
QString modelName = modelFileToNameMap.value(modelKey);
|
||||
if (modelName.contains("(Default)")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isInstalled(modelKey)) {
|
||||
QString series = modelSeriesMap.value(modelKey, "Dom Forgot To Label Me");
|
||||
seriesToModels[series].append(modelName);
|
||||
installedModelFileToNameMap.insert(modelKey, modelName);
|
||||
if (modelReleasedDates.contains(modelKey)) {
|
||||
installedReleasedDates.insert(modelKey, modelReleasedDates.value(modelKey));
|
||||
}
|
||||
|
||||
QString series = modelSeriesMap.value(modelKey, "Custom Series");
|
||||
seriesToModels[series].append(modelName);
|
||||
}
|
||||
|
||||
// Add Space Lab to Custom Series
|
||||
QString spaceLabName = modelFileToNameMap.value("space-lab");
|
||||
if (isInstalled("space-lab")) {
|
||||
seriesToModels["Custom Series"].append(spaceLabName);
|
||||
}
|
||||
|
||||
// Sort models within each series
|
||||
// Sort models alphabetically within each series
|
||||
for (QString &series : seriesToModels.keys()) {
|
||||
seriesToModels[series].sort();
|
||||
}
|
||||
@@ -371,50 +371,62 @@ FrogPilotModelPanel::FrogPilotModelPanel(FrogPilotSettingsWindow *parent) : Frog
|
||||
seriesToModels[defaultSeries].prepend(defaultModelName);
|
||||
}
|
||||
|
||||
QString modelToSelect = ExpandableMultiOptionDialog::getSelection(tr("Select a model - 🗺️ = Navigation | 📡 = Radar | 👀 = VOACC"), seriesToModels, currentModel, this);
|
||||
if (!modelToSelect.isEmpty()) {
|
||||
currentModel = modelToSelect;
|
||||
// Prepare favorites and dates for the enhanced dialog
|
||||
QStringList userFavs = QString::fromStdString(params.get("UserFavorites")).split(",");
|
||||
userFavs.removeAll("");
|
||||
|
||||
params.put("Model", modelFileToNameMap.key(modelToSelect).toStdString());
|
||||
// Sync ModelVersion with the selected model if known
|
||||
{
|
||||
QString modelKey = modelFileToNameMap.key(modelToSelect);
|
||||
QFile vf("/data/models/.model_versions.json");
|
||||
if (vf.open(QIODevice::ReadOnly)) {
|
||||
auto doc = QJsonDocument::fromJson(vf.readAll());
|
||||
if (doc.isObject()) {
|
||||
auto obj = doc.object();
|
||||
if (obj.contains(modelKey)) {
|
||||
params.put("ModelVersion", obj.value(modelKey).toString().toStdString());
|
||||
QStringList communityFavs = QString::fromStdString(params.get("CommunityFavorites")).split(",");
|
||||
communityFavs.removeAll("");
|
||||
|
||||
// Create dialog instance to access sort mode and favorites after selection
|
||||
QString savedSortMode = QString::fromStdString(params.get("ModelSortMode"));
|
||||
if (savedSortMode.isEmpty()) savedSortMode = "alphabetical";
|
||||
|
||||
ExpandableMultiOptionDialog dialog(tr("Select a model - 🗺️ = Navigation | 📡 = Radar | 👀 = VOACC"),
|
||||
seriesToModels, currentModel, this,
|
||||
userFavs, communityFavs, installedReleasedDates, installedModelFileToNameMap, savedSortMode);
|
||||
|
||||
int dialogResult = dialog.exec();
|
||||
|
||||
// Persist sort mode and user favorites even if no selection was made
|
||||
QString sortMode = dialog.getCurrentSortMode();
|
||||
QStringList newUserFavs = dialog.getUserFavorites();
|
||||
params.put("ModelSortMode", sortMode.toStdString());
|
||||
params.put("UserFavorites", newUserFavs.join(",").toStdString());
|
||||
|
||||
if (dialogResult == QDialog::Accepted) {
|
||||
QString modelToSelect = dialog.selection;
|
||||
if (!modelToSelect.isEmpty()) {
|
||||
currentModel = modelToSelect;
|
||||
|
||||
params.put("Model", modelFileToNameMap.key(modelToSelect).toStdString());
|
||||
// Sync ModelVersion with the selected model if known
|
||||
{
|
||||
QString modelKey = modelFileToNameMap.key(modelToSelect);
|
||||
QFile vf("/data/models/.model_versions.json");
|
||||
if (vf.open(QIODevice::ReadOnly)) {
|
||||
auto doc = QJsonDocument::fromJson(vf.readAll());
|
||||
if (doc.isObject()) {
|
||||
auto obj = doc.object();
|
||||
if (obj.contains(modelKey)) {
|
||||
params.put("ModelVersion", obj.value(modelKey).toString().toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateFrogPilotToggles();
|
||||
updateFrogPilotToggles();
|
||||
|
||||
if (started) {
|
||||
if (FrogPilotConfirmationDialog::toggleReboot(this)) {
|
||||
Hardware::reboot();
|
||||
}
|
||||
}
|
||||
selectModelButton->setValue(modelToSelect);
|
||||
|
||||
QStringList deletableModels;
|
||||
for (const QString &file : modelDir.entryList(QDir::Files)) {
|
||||
QString base = QFileInfo(file).baseName();
|
||||
for (const QString &modelKey : modelFileToNameMapProcessed.keys()) {
|
||||
if (base.startsWith(modelKey)) {
|
||||
QString modelName = modelFileToNameMapProcessed.value(modelKey);
|
||||
if (!deletableModels.contains(modelName)) {
|
||||
deletableModels.append(modelName);
|
||||
}
|
||||
if (started) {
|
||||
if (FrogPilotConfirmationDialog::toggleReboot(this)) {
|
||||
Hardware::reboot();
|
||||
}
|
||||
}
|
||||
selectModelButton->setValue(modelToSelect);
|
||||
|
||||
noModelsDownloaded = getDeletableModelDisplayNames().isEmpty();
|
||||
deleteModelButton->setEnabled(!(allModelsDownloading || modelDownloading || noModelsDownloaded));
|
||||
}
|
||||
deletableModels.removeAll(processModelName(currentModel));
|
||||
deletableModels.removeAll(modelFileToNameMapProcessed.value(QString::fromStdString(params_default.get("Model"))));
|
||||
noModelsDownloaded = deletableModels.isEmpty();
|
||||
}
|
||||
});
|
||||
modelToggle = selectModelButton;
|
||||
@@ -465,6 +477,87 @@ FrogPilotModelPanel::FrogPilotModelPanel(FrogPilotSettingsWindow *parent) : Frog
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotModelPanel::updateState);
|
||||
}
|
||||
|
||||
bool FrogPilotModelPanel::isModelInstalled(const QString &key) const {
|
||||
if (key.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool has_thneed = false;
|
||||
bool has_policy_meta = false;
|
||||
bool has_policy_tg = false;
|
||||
bool has_vision_meta = false;
|
||||
bool has_vision_tg = false;
|
||||
bool foundAny = false;
|
||||
|
||||
for (const QString &file : modelDir.entryList(QDir::Files)) {
|
||||
QFileInfo fi(modelDir.filePath(file));
|
||||
const QString base = fi.baseName();
|
||||
const QString ext = fi.suffix();
|
||||
|
||||
if (!(base.startsWith(key) || base.startsWith(key + "_"))) continue;
|
||||
|
||||
foundAny = true;
|
||||
|
||||
if (ext == "thneed") {
|
||||
has_thneed = true;
|
||||
} else if (ext == "pkl") {
|
||||
if (base.contains("_driving_policy_metadata")) {
|
||||
has_policy_meta = true;
|
||||
} else if (base.contains("_driving_policy_tinygrad")) {
|
||||
has_policy_tg = true;
|
||||
} else if (base.contains("_driving_vision_metadata")) {
|
||||
has_vision_meta = true;
|
||||
} else if (base.contains("_driving_vision_tinygrad")) {
|
||||
has_vision_tg = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (has_thneed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (has_policy_meta && has_policy_tg && has_vision_meta && has_vision_tg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return foundAny;
|
||||
}
|
||||
|
||||
QMap<QString, QString> FrogPilotModelPanel::getDeletableModelDisplayNames() {
|
||||
QMap<QString, QString> deletable;
|
||||
|
||||
QString defaultModelKey = QString::fromStdString(params_default.get("Model"));
|
||||
QString defaultModelName = modelFileToNameMap.value(defaultModelKey);
|
||||
QString processedDefault = processModelName(defaultModelName);
|
||||
QString processedCurrent = processModelName(currentModel);
|
||||
|
||||
for (auto it = modelFileToNameMap.constBegin(); it != modelFileToNameMap.constEnd(); ++it) {
|
||||
const QString &modelKey = it.key();
|
||||
const QString &displayName = it.value();
|
||||
if (displayName.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isModelInstalled(modelKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString processedName = processModelName(displayName);
|
||||
if (!processedCurrent.isEmpty() && processedName == processedCurrent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!processedDefault.isEmpty() && processedName == processedDefault) {
|
||||
continue;
|
||||
}
|
||||
|
||||
deletable.insert(modelKey, displayName);
|
||||
}
|
||||
|
||||
return deletable;
|
||||
}
|
||||
|
||||
void FrogPilotModelPanel::showEvent(QShowEvent *event) {
|
||||
FrogPilotUIState &fs = *frogpilotUIState();
|
||||
UIState &s = *uiState();
|
||||
@@ -478,6 +571,9 @@ void FrogPilotModelPanel::showEvent(QShowEvent *event) {
|
||||
QStringList availableModels = QString::fromStdString(params.get("AvailableModels")).split(",");
|
||||
availableModelNames = QString::fromStdString(params.get("AvailableModelNames")).split(",");
|
||||
availableModelSeries = QString::fromStdString(params.get("AvailableModelSeries")).split(",");
|
||||
QStringList releasedDatesParam = QString::fromStdString(params.get("ModelReleasedDates")).split(",");
|
||||
QStringList communityFavsParam = QString::fromStdString(params.get("CommunityFavorites")).split(",");
|
||||
QStringList userFavsParam = QString::fromStdString(params.get("UserFavorites")).split(",");
|
||||
|
||||
// Build a simple model->version map for quick lookups elsewhere
|
||||
{
|
||||
@@ -497,78 +593,54 @@ void FrogPilotModelPanel::showEvent(QShowEvent *event) {
|
||||
modelFileToNameMap.clear();
|
||||
modelFileToNameMapProcessed.clear();
|
||||
modelSeriesMap.clear();
|
||||
int size = qMin(qMin(availableModels.size(), availableModelNames.size()), availableModelSeries.size());
|
||||
modelReleasedDates.clear();
|
||||
int size = qMin(availableModels.size(), availableModelNames.size());
|
||||
for (int i = 0; i < size; ++i) {
|
||||
modelFileToNameMap.insert(availableModels[i], availableModelNames[i]);
|
||||
modelFileToNameMapProcessed.insert(availableModels[i], processModelName(availableModelNames[i]));
|
||||
modelSeriesMap.insert(availableModels[i], availableModelSeries[i]);
|
||||
}
|
||||
modelFileToNameMap.insert("space-lab", "Space Lab 👀📡");
|
||||
modelFileToNameMapProcessed.insert("space-lab", "Space Lab");
|
||||
modelSeriesMap.insert("space-lab", "Dom Forgot To Label Me");
|
||||
|
||||
auto isInstalled = [this](const QString &key) {
|
||||
bool has_thneed = false;
|
||||
bool has_policy_meta = false;
|
||||
bool has_policy_tg = false;
|
||||
bool has_vision_meta = false;
|
||||
bool has_vision_tg = false;
|
||||
|
||||
for (const QString &file : modelDir.entryList(QDir::Files)) {
|
||||
QFileInfo fi(modelDir.filePath(file));
|
||||
const QString base = fi.baseName();
|
||||
const QString ext = fi.suffix();
|
||||
if (!(base.startsWith(key) || base.startsWith(key + "_"))) continue;
|
||||
|
||||
if (ext == "thneed") {
|
||||
// Classic model (WD-40 etc.)
|
||||
has_thneed = true;
|
||||
} else if (ext == "pkl") {
|
||||
// TinyGrad bundle uses these four exact suffixes
|
||||
if (base.contains("_driving_policy_metadata")) has_policy_meta = true;
|
||||
else if (base.contains("_driving_policy_tinygrad")) has_policy_tg = true;
|
||||
else if (base.contains("_driving_vision_metadata")) has_vision_meta = true;
|
||||
else if (base.contains("_driving_vision_tinygrad")) has_vision_tg = true;
|
||||
}
|
||||
const QString modelKey = availableModels[i].trimmed();
|
||||
const QString modelName = availableModelNames[i].trimmed();
|
||||
if (modelKey.isEmpty() || modelName.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Classic models: any matching .thneed counts as installed
|
||||
if (has_thneed) return true;
|
||||
// TinyGrad models: require all four policy/vision files to be present
|
||||
return has_policy_meta && has_policy_tg && has_vision_meta && has_vision_tg;
|
||||
};
|
||||
QStringList downloadableModels = availableModelNames;
|
||||
for (const QString &modelKey : modelFileToNameMap.keys()) {
|
||||
QString modelName = modelFileToNameMap.value(modelKey);
|
||||
if (isInstalled(modelKey)) {
|
||||
downloadableModels.removeAll(modelName);
|
||||
QString series;
|
||||
if (i < availableModelSeries.size()) {
|
||||
series = availableModelSeries[i].trimmed();
|
||||
}
|
||||
if (series.isEmpty()) {
|
||||
series = tr("Custom Series");
|
||||
}
|
||||
}
|
||||
allModelsDownloaded = downloadableModels.isEmpty();
|
||||
|
||||
QStringList deletableModels;
|
||||
for (const QString &file : modelDir.entryList(QDir::Files)) {
|
||||
QString base = QFileInfo(file).baseName();
|
||||
for (const QString &modelKey : modelFileToNameMapProcessed.keys()) {
|
||||
if (base.startsWith(modelKey)) {
|
||||
QString modelName = modelFileToNameMapProcessed.value(modelKey);
|
||||
if (!deletableModels.contains(modelName)) {
|
||||
deletableModels.append(modelName);
|
||||
}
|
||||
modelFileToNameMap.insert(modelKey, modelName);
|
||||
modelFileToNameMapProcessed.insert(modelKey, processModelName(modelName));
|
||||
modelSeriesMap.insert(modelKey, series);
|
||||
|
||||
if (i < releasedDatesParam.size()) {
|
||||
const QString released = releasedDatesParam[i].trimmed();
|
||||
if (!released.isEmpty()) {
|
||||
this->modelReleasedDates.insert(modelKey, released);
|
||||
}
|
||||
}
|
||||
}
|
||||
deletableModels.removeAll(processModelName(currentModel));
|
||||
deletableModels.removeAll(modelFileToNameMapProcessed.value(QString::fromStdString(params_default.get("Model"))));
|
||||
noModelsDownloaded = deletableModels.isEmpty();
|
||||
allModelsDownloaded = true;
|
||||
for (auto it = modelFileToNameMap.constBegin(); it != modelFileToNameMap.constEnd(); ++it) {
|
||||
if (it.value().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (!isModelInstalled(it.key())) {
|
||||
allModelsDownloaded = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QString modelKey = QString::fromStdString(params.get("Model"));
|
||||
if (!isInstalled(modelKey)) {
|
||||
if (!isModelInstalled(modelKey)) {
|
||||
modelKey = QString::fromStdString(params_default.get("Model"));
|
||||
}
|
||||
currentModel = modelFileToNameMap.value(modelKey);
|
||||
selectModelButton->setValue(currentModel);
|
||||
|
||||
noModelsDownloaded = getDeletableModelDisplayNames().isEmpty();
|
||||
|
||||
bool parked = !s.scene.started || fs.frogpilot_scene.parked || fs.frogpilot_toggles.value("frogs_go_moo").toBool();
|
||||
|
||||
deleteModelButton->setEnabled(!(allModelsDownloading || modelDownloading || noModelsDownloaded));
|
||||
@@ -666,9 +738,7 @@ void FrogPilotModelPanel::updateToggles() {
|
||||
|
||||
if (key == "ManageBlacklistedModels" || key == "ManageScores") {
|
||||
setVisible &= params.getBool("ModelRandomizer");
|
||||
}
|
||||
|
||||
else if (key == "SelectModel") {
|
||||
} else if (key == "SelectModel") {
|
||||
setVisible &= !params.getBool("ModelRandomizer");
|
||||
} else if (key == "StopDistance") {
|
||||
setVisible &= (tuningLevel == 3); // Only visible in developer tuning level
|
||||
|
||||
@@ -20,6 +20,8 @@ private:
|
||||
void updateModelLabels(FrogPilotListWidget *labelsList);
|
||||
void updateState(const UIState &s, const FrogPilotUIState &fs);
|
||||
void updateToggles();
|
||||
bool isModelInstalled(const QString &key) const;
|
||||
QMap<QString, QString> getDeletableModelDisplayNames();
|
||||
|
||||
bool allModelsDownloaded;
|
||||
bool allModelsDownloading;
|
||||
@@ -55,10 +57,12 @@ private:
|
||||
|
||||
QMap<QString, QString> modelFileToNameMap;
|
||||
QMap<QString, QString> modelFileToNameMapProcessed;
|
||||
QMap<QString, QString> modelReleasedDates;
|
||||
QMap<QString, QString> modelSeriesMap;
|
||||
|
||||
QString currentModel;
|
||||
|
||||
|
||||
QStringList availableModelNames;
|
||||
QStringList availableModelSeries;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user