From 74308fcaa5cfc97a291710316f0f15b4b22fee2d Mon Sep 17 00:00:00 2001 From: Tayou Date: Mon, 24 Nov 2025 15:23:18 +0100 Subject: [PATCH 1/9] add category selector to icon picker dialog it uses some regex shenanigans for this, probably not ideal, idk if theres a good way to filter the icons without adding extra metadata or storing them in subfolders Signed-off-by: Tayou --- launcher/ui/dialogs/IconPickerDialog.cpp | 50 +++++++++++++++++--- launcher/ui/dialogs/IconPickerDialog.h | 9 ++++ launcher/ui/dialogs/IconPickerDialog.ui | 59 +++++++++++++++++++++++- 3 files changed, 110 insertions(+), 8 deletions(-) diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index b56e95dba..dfeea20a1 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -35,9 +35,25 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui ui->setupUi(this); setWindowModality(Qt::WindowModal); - searchBar = new QLineEdit(this); - searchBar->setPlaceholderText(tr("Search...")); - ui->verticalLayout->insertWidget(0, searchBar); + static const QString context_text[] = { + tr("All"), + tr("Modern"), + tr("Legacy"), + tr("Modpacks"), + }; + static const Context context_id[] = { + Any, + Modern, + Legacy, + Modpacks, + }; + const int cnt = sizeof(context_text) / sizeof(context_text[0]); + for (int i = 0; i < cnt; ++i) { + ui->contextCombo->addItem(context_text[i], context_id[i]); + if (i == 0) { + ui->contextCombo->insertSeparator(i + 1); + } + } proxyModel = new QSortFilterProxyModel(this); proxyModel->setSourceModel(APPLICATION->icons()); @@ -45,11 +61,8 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui ui->iconView->setModel(proxyModel); auto contentsWidget = ui->iconView; - contentsWidget->setViewMode(QListView::IconMode); contentsWidget->setFlow(QListView::LeftToRight); contentsWidget->setIconSize(QSize(48, 48)); - contentsWidget->setMovement(QListView::Static); - contentsWidget->setResizeMode(QListView::Adjust); contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); contentsWidget->setSpacing(5); contentsWidget->setWordWrap(false); @@ -86,7 +99,11 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole); connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); - connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons); + connect(ui->searchLine, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons); + connect(ui->contextCombo, &QComboBox::currentIndexChanged, this, [this](int index) { + Context category = static_cast(ui->contextCombo->itemData(index).toInt()); + filterIconsByCategory(category); + }); // Prevent incorrect indices from e.g. filesystem changes connect(APPLICATION->icons(), &IconList::iconUpdated, this, [this]() { proxyModel->invalidate(); }); } @@ -182,3 +199,22 @@ void IconPickerDialog::filterIcons(const QString& query) { proxyModel->setFilterFixedString(query); } + +void IconPickerDialog::filterIconsByCategory(Context category) +{ + switch (category) { + default: + case Any: + proxyModel->setFilterRegularExpression(""); + break; + case Modern: + proxyModel->setFilterRegularExpression("^(?:ftb_logo|(?!.*_legacy$)(?!^(?:curseforge_|modrinth_|ftb_|technic_|atl_))[A-Za-z0-9._-]+)$"); + break; + case Legacy: + proxyModel->setFilterRegularExpression("^(?:[A-Za-z0-9._-]+_legacy|ftb_glow)$"); + break; + case Modpacks: + proxyModel->setFilterRegularExpression("^(?!(?:ftb_glow|ftb_logo))(?:curseforge_|modrinth_|ftb_|technic_|atl_)[A-Za-z0-9._-]*$"); + break; + } +} diff --git a/launcher/ui/dialogs/IconPickerDialog.h b/launcher/ui/dialogs/IconPickerDialog.h index db1315338..58a94bb9b 100644 --- a/launcher/ui/dialogs/IconPickerDialog.h +++ b/launcher/ui/dialogs/IconPickerDialog.h @@ -41,6 +41,14 @@ class IconPickerDialog : public QDialog { QLineEdit* searchBar; QSortFilterProxyModel* proxyModel; + enum Context { + Any, + Modern, + Legacy, + Modpacks, + }; + Q_ENUM(Context) + private slots: void selectionChanged(QItemSelection, QItemSelection); void activated(QModelIndex); @@ -49,4 +57,5 @@ class IconPickerDialog : public QDialog { void removeSelectedIcon(); void openFolder(); void filterIcons(const QString& text); + void filterIconsByCategory(Context); }; diff --git a/launcher/ui/dialogs/IconPickerDialog.ui b/launcher/ui/dialogs/IconPickerDialog.ui index c548edfb7..948e7043f 100644 --- a/launcher/ui/dialogs/IconPickerDialog.ui +++ b/launcher/ui/dialogs/IconPickerDialog.ui @@ -15,7 +15,64 @@ - + + + + + + + Icon category + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Search Icons... + + + true + + + + + + + + + + + + 60 + 60 + + + + QListView::Static + + + QListView::Adjust + + + QListView::IconMode + + + true + + From 0c4c8703a363d3a0c8dd1f5095330eb04b50f626 Mon Sep 17 00:00:00 2001 From: Tayou Date: Mon, 24 Nov 2025 18:06:09 +0100 Subject: [PATCH 2/9] rename IconPickerCategory and make public Signed-off-by: Tayou --- launcher/ui/dialogs/IconPickerDialog.cpp | 6 +++--- launcher/ui/dialogs/IconPickerDialog.h | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index dfeea20a1..49b396b5e 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -41,7 +41,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui tr("Legacy"), tr("Modpacks"), }; - static const Context context_id[] = { + static const IconPickerCategory context_id[] = { Any, Modern, Legacy, @@ -101,7 +101,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); connect(ui->searchLine, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons); connect(ui->contextCombo, &QComboBox::currentIndexChanged, this, [this](int index) { - Context category = static_cast(ui->contextCombo->itemData(index).toInt()); + IconPickerCategory category = static_cast(ui->contextCombo->itemData(index).toInt()); filterIconsByCategory(category); }); // Prevent incorrect indices from e.g. filesystem changes @@ -200,7 +200,7 @@ void IconPickerDialog::filterIcons(const QString& query) proxyModel->setFilterFixedString(query); } -void IconPickerDialog::filterIconsByCategory(Context category) +void IconPickerDialog::filterIconsByCategory(IconPickerCategory category) { switch (category) { default: diff --git a/launcher/ui/dialogs/IconPickerDialog.h b/launcher/ui/dialogs/IconPickerDialog.h index 58a94bb9b..b2d707ae8 100644 --- a/launcher/ui/dialogs/IconPickerDialog.h +++ b/launcher/ui/dialogs/IconPickerDialog.h @@ -32,6 +32,14 @@ class IconPickerDialog : public QDialog { int execWithSelection(QString selection); QString selectedIconKey; + enum IconPickerCategory { + Any, + Modern, + Legacy, + Modpacks, + }; + Q_ENUM(IconPickerCategory) + protected: virtual bool eventFilter(QObject*, QEvent*); @@ -41,14 +49,6 @@ class IconPickerDialog : public QDialog { QLineEdit* searchBar; QSortFilterProxyModel* proxyModel; - enum Context { - Any, - Modern, - Legacy, - Modpacks, - }; - Q_ENUM(Context) - private slots: void selectionChanged(QItemSelection, QItemSelection); void activated(QModelIndex); @@ -57,5 +57,5 @@ class IconPickerDialog : public QDialog { void removeSelectedIcon(); void openFolder(); void filterIcons(const QString& text); - void filterIconsByCategory(Context); + void filterIconsByCategory(IconPickerCategory); }; From bbd8c1e745916bea65343dc1ac1f012902b0f0e1 Mon Sep 17 00:00:00 2001 From: Tayou Date: Tue, 25 Nov 2025 15:27:18 +0100 Subject: [PATCH 3/9] use custom QSortFilterProxyModel impl Signed-off-by: Tayou --- launcher/ui/dialogs/IconPickerDialog.cpp | 83 +++++++++++++++++++----- launcher/ui/dialogs/IconPickerDialog.h | 1 + 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index 49b396b5e..051d8495f 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -30,6 +30,71 @@ #include "icons/IconList.h" #include "icons/IconUtils.h" +class IconProxyModel : public QSortFilterProxyModel +{ +public: + explicit IconProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) + { + } + + void setCategory(IconPickerDialog::IconPickerCategory category) + { + if (m_category == category) + return; + m_category = category; + invalidateFilter(); + } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override + { + if (!QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent)) + return false; + + if (m_category == IconPickerDialog::Any) + return true; + + auto model = static_cast(sourceModel()); + QModelIndex index = model->index(source_row, 0, source_parent); + QString key = model->data(index, Qt::UserRole).toString(); + const MMCIcon* icon = model->icon(key); + + if (!icon) + return false; + + bool isModpack = false; + bool isBuiltin = icon->isBuiltIn(); + bool isLegacy = isBuiltin && icon->name().endsWith("_legacy", Qt::CaseInsensitive); + + if (!isBuiltin) { + const QString& name = icon->name(); + if (name.startsWith("curseforge_", Qt::CaseInsensitive) || + name.startsWith("modrinth_", Qt::CaseInsensitive) || + name.startsWith("ftb_", Qt::CaseInsensitive) || + name.startsWith("technic_", Qt::CaseInsensitive) || + name.startsWith("atl_", Qt::CaseInsensitive)) { + isModpack = true; + } + } + + switch (m_category) { + case IconPickerDialog::Legacy: + return isBuiltin && isLegacy; + case IconPickerDialog::Modpacks: + return isModpack; + case IconPickerDialog::Modern: + return isBuiltin && !isLegacy; + case IconPickerDialog::Custom: + return !isBuiltin && !isModpack; + default: + return true; + } + } + +private: + IconPickerDialog::IconPickerCategory m_category = IconPickerDialog::Any; +}; + IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui::IconPickerDialog) { ui->setupUi(this); @@ -40,12 +105,14 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui tr("Modern"), tr("Legacy"), tr("Modpacks"), + tr("Custom"), }; static const IconPickerCategory context_id[] = { Any, Modern, Legacy, Modpacks, + Custom, }; const int cnt = sizeof(context_text) / sizeof(context_text[0]); for (int i = 0; i < cnt; ++i) { @@ -202,19 +269,5 @@ void IconPickerDialog::filterIcons(const QString& query) void IconPickerDialog::filterIconsByCategory(IconPickerCategory category) { - switch (category) { - default: - case Any: - proxyModel->setFilterRegularExpression(""); - break; - case Modern: - proxyModel->setFilterRegularExpression("^(?:ftb_logo|(?!.*_legacy$)(?!^(?:curseforge_|modrinth_|ftb_|technic_|atl_))[A-Za-z0-9._-]+)$"); - break; - case Legacy: - proxyModel->setFilterRegularExpression("^(?:[A-Za-z0-9._-]+_legacy|ftb_glow)$"); - break; - case Modpacks: - proxyModel->setFilterRegularExpression("^(?!(?:ftb_glow|ftb_logo))(?:curseforge_|modrinth_|ftb_|technic_|atl_)[A-Za-z0-9._-]*$"); - break; - } + static_cast(proxyModel)->setCategory(category); } diff --git a/launcher/ui/dialogs/IconPickerDialog.h b/launcher/ui/dialogs/IconPickerDialog.h index b2d707ae8..063f9b905 100644 --- a/launcher/ui/dialogs/IconPickerDialog.h +++ b/launcher/ui/dialogs/IconPickerDialog.h @@ -37,6 +37,7 @@ class IconPickerDialog : public QDialog { Modern, Legacy, Modpacks, + Custom, }; Q_ENUM(IconPickerCategory) From 28eba8ed430b94be39d5b00fe5afcebab7b8f795 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 15 May 2026 13:01:41 +0100 Subject: [PATCH 4/9] Fix memory leak and crash with data packs modal Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/WorldListPage.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index e56e9c731..24c880429 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -259,6 +259,7 @@ void WorldListPage::on_actionData_Packs_triggered() dialog->setLayout(layout); + dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->exec(); APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); From fa61e58cd976740e6c815f03bda9ea5ce5718ff8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 28 May 2026 09:20:57 +0100 Subject: [PATCH 5/9] Replace exec with open and handle saving geometry in signal listener Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/WorldListPage.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 24c880429..71c370ab2 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -121,7 +121,7 @@ void WorldListPage::openedImpl() ui->toolBar->removeAction(ui->actionJoin); } - auto const setting_name = QString("WideBarVisibility_%1").arg(id()); + const auto setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); @@ -260,9 +260,11 @@ void WorldListPage::on_actionData_Packs_triggered() dialog->setLayout(layout); dialog->setAttribute(Qt::WA_DeleteOnClose); - dialog->exec(); - APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); + connect(dialog, &QDialog::finished, this, + [dialog]() { APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); }); + + dialog->open(); } void WorldListPage::on_actionReset_Icon_triggered() From f6d1b29b04140fa1e43ffa44a5b5b99216b50ebe Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 1 Jun 2026 13:27:53 +0500 Subject: [PATCH 6/9] fix(LaunchController): don't remove account unless we have a new one Signed-off-by: Octol1ttle --- launcher/LaunchController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index f7ea23eb3..30ee448a2 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -333,11 +333,11 @@ bool LaunchController::reauthenticateAccount(const MinecraftAccountPtr& account, if (button == QMessageBox::StandardButton::Yes) { auto* accounts = APPLICATION->accounts(); const bool isDefault = accounts->defaultAccount() == account; - accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId()))); if (account->accountType() == AccountType::MSA) { auto newAccount = MSALoginDialog::newAccount(m_parentWidget); if (newAccount != nullptr) { + accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId()))); accounts->addAccount(newAccount); if (isDefault) { From ea2d0d06448f4337ff27f70fc96e08d743371eb4 Mon Sep 17 00:00:00 2001 From: cat Date: Sun, 31 May 2026 14:26:32 +0000 Subject: [PATCH 7/9] AccountList: Skip refresh when !shouldRefresh Signed-off-by: cat --- launcher/minecraft/auth/AccountList.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index ac27b6bbc..418ba98d2 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -648,9 +648,17 @@ void AccountList::tryNext() while (m_refreshQueue.length()) { auto accountId = m_refreshQueue.front(); m_refreshQueue.pop_front(); + bool found = false; for (int i = 0; i < count(); i++) { auto account = at(i); if (account->internalId() == accountId) { + found = true; + if (!account->shouldRefresh()) { + // Account no longer needs refreshing, skip it. + qDebug() << "RefreshSchedule: Skipping account" << account->profileName() << "with internal ID" + << accountId << "(no longer needs refresh)"; + break; + } m_currentTask = account->refresh(); if (m_currentTask) { connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded); @@ -660,9 +668,12 @@ void AccountList::tryNext() << accountId; return; } + break; } } - qDebug() << "RefreshSchedule: Account with internal ID" << accountId << "not found."; + if (!found) { + qDebug() << "RefreshSchedule: Account with internal ID" << accountId << "not found."; + } } // if we get here, no account needed refreshing. Schedule refresh in an hour. m_refreshTimer->start(1000 * 3600); From 5c1d7832936943410903ab1a005cf4d5d99e2b23 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 3 Jun 2026 21:04:01 +0200 Subject: [PATCH 8/9] fix(nix): add jdk25 to wrapper Signed-off-by: Sefa Eyeoglu --- nix/wrapper.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nix/wrapper.nix b/nix/wrapper.nix index 00752a8c4..23fc04d9f 100644 --- a/nix/wrapper.nix +++ b/nix/wrapper.nix @@ -6,6 +6,7 @@ glfw3-minecraft, jdk17, jdk21, + jdk25, jdk8, kdePackages, lib, @@ -34,6 +35,7 @@ controllerSupport ? stdenv.hostPlatform.isLinux, gamemodeSupport ? stdenv.hostPlatform.isLinux, jdks ? [ + jdk25 jdk21 jdk17 jdk8 From 502a5175e2fc6ca386e4f8e855a2413f6e10dbfe Mon Sep 17 00:00:00 2001 From: Tayou Date: Thu, 4 Jun 2026 19:07:24 +0200 Subject: [PATCH 9/9] fix category filtering I have no idea how this worked at all when I made the commit originally, but it works now, just as well as it did on the prior commit. Further improvements, using subfolders and other metadata will be in another PR. Signed-off-by: Tayou --- launcher/ui/dialogs/IconPickerDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index 051d8495f..b2269cc70 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -122,7 +122,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui } } - proxyModel = new QSortFilterProxyModel(this); + proxyModel = new IconProxyModel(this); proxyModel->setSourceModel(APPLICATION->icons()); proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); ui->iconView->setModel(proxyModel);