From 10b8842cf6ebf0b68a211b2a70684d39f218ecfb Mon Sep 17 00:00:00 2001 From: Vivek Kushwaha Date: Wed, 17 Jun 2026 11:44:14 +0530 Subject: [PATCH 1/5] fix(ui): isolate mod profile tab states and resolve grid layout access violations Signed-off-by: Vivek Kushwaha --- launcher/ui/pages/instance/ModFolderPage.cpp | 176 +++++++++++++++++++ launcher/ui/pages/instance/ModFolderPage.h | 73 ++++---- 2 files changed, 217 insertions(+), 32 deletions(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 99c78647c..774d7535e 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -49,6 +49,9 @@ #include #include #include +#include +#include +#include #include #include @@ -70,6 +73,30 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent) : ExternalResourcesPage(inst, model, parent), m_model(model) { + m_profileTabBar = new QTabBar(this); + if (ui->treeView->parentWidget() && ui->treeView->parentWidget()->layout()) { + QLayout* layout = ui->treeView->parentWidget()->layout(); + int index = layout->indexOf(ui->treeView); + + if (index != -1) { + if (auto box = qobject_cast(layout)) { + box->insertWidget(index, m_profileTabBar); + } + else if (auto grid = qobject_cast(layout)) { + grid->addWidget(m_profileTabBar, index, 0); + } + } + } + connect(m_profileTabBar, &QTabBar::currentChanged, this, &ModFolderPage::onProfileTabChanged); + + auto* addProfileAction = new QAction(tr("Add Profile"), this); + connect(addProfileAction, &QAction::triggered, this, &ModFolderPage::onAddProfileClicked); + ui->actionsToolbar->addAction(addProfileAction); + + auto* removeProfileAction = new QAction(tr("Remove Profile"), this); + connect(removeProfileAction, &QAction::triggered, this, &ModFolderPage::onRemoveProfileClicked); + ui->actionsToolbar->addAction(removeProfileAction); + ui->actionDownloadItem->setText(tr("Download Mods")); ui->actionDownloadItem->setToolTip(tr("Download mods from online mod platforms")); ui->actionDownloadItem->setEnabled(true); @@ -110,6 +137,27 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* ui->actionsToolbar->insertActionAfter(ui->actionViewHomepage, ui->actionExportMetadata); ui->actionsToolbar->insertActionAfter(ui->actionViewFolder, ui->actionViewConfigs); + + m_instance->settings()->getOrRegisterSetting("ModProfileList", QStringList() << "All Mods"); + QStringList profileList = m_instance->settings()->get("ModProfileList").toStringList(); + m_profileTabBar->blockSignals(true); + for (const QString& name : profileList) { + m_profileTabBar->addTab(name); + + QString key = "ModProfile_" + name; + m_instance->settings()->getOrRegisterSetting(key, QStringList()); + QStringList saved = m_instance->settings()->get(key).toStringList(); + m_profileStates[name] = QSet(saved.begin(), saved.end()); + } + m_profileTabBar->blockSignals(false); + + m_instance->settings()->getOrRegisterSetting("ModProfileLastActiveIndex", 0); + int savedIndex = m_instance->settings()->get("ModProfileLastActiveIndex").toInt(); + if (savedIndex < 0 || savedIndex >= m_profileTabBar->count()) { + savedIndex = 0; + } + m_profileTabBar->setCurrentIndex(savedIndex); + onProfileTabChanged(m_profileTabBar->currentIndex()); } bool ModFolderPage::shouldDisplay() const @@ -445,3 +493,131 @@ inline bool ModFolderPage::handleNoModLoader() // returning true so the caller doesn't go and continue with opening it's dialog without a mod loader return true; } + +void ModFolderPage::onProfileTabChanged(int index) { + if (m_currentProfile.isEmpty()) { + QVariant val = m_profileTabBar->property("currentProfileName"); + m_currentProfile = val.isValid() ? val.toString() : QString(); + } + + if (!m_currentProfile.isEmpty()) { + bool stillExists = false; + for (int i = 0; i < m_profileTabBar->count(); ++i) { + if (m_profileTabBar->tabText(i) == m_currentProfile) { + stillExists = true; + break; + } + } + if (stillExists) { + QSet enabledMods; + for (int i = 0; i < m_model->rowCount(); ++i) { + const Resource& res = m_model->at(i); + if (res.enabled()) { + enabledMods.insert(res.getOriginalFileName()); + } + } + m_profileStates[m_currentProfile] = enabledMods; + QString key = "ModProfile_" + m_currentProfile; + m_instance->settings()->getOrRegisterSetting(key, QStringList()); + m_instance->settings()->set(key, QStringList(enabledMods.begin(), enabledMods.end())); + } + } + + if (index >= 0 && index < m_profileTabBar->count()) { + QString tabName = m_profileTabBar->tabText(index); + m_currentProfile = tabName; + m_profileTabBar->setProperty("currentProfileName", tabName); + m_instance->settings()->set("ModProfileLastActiveIndex", index); + + QSet enabledMods; + if (m_profileStates.contains(tabName)) { + enabledMods = m_profileStates[tabName]; + } else { + QString key = "ModProfile_" + tabName; + m_instance->settings()->getOrRegisterSetting(key, QStringList()); + QStringList saved = m_instance->settings()->get(key).toStringList(); + enabledMods = QSet(saved.begin(), saved.end()); + m_profileStates[tabName] = enabledMods; + } + + QModelIndexList toEnable; + QModelIndexList toDisable; + for (int i = 0; i < m_model->rowCount(); ++i) { + const Resource& res = m_model->at(i); + QModelIndex idx = m_model->index(i, 0); + if (enabledMods.contains(res.getOriginalFileName())) { + if (!res.enabled()) { + toEnable.append(idx); + } + } else { + if (res.enabled()) { + toDisable.append(idx); + } + } + } + if (!toEnable.isEmpty()) { + m_model->setResourceEnabled(toEnable, EnableAction::ENABLE); + } + if (!toDisable.isEmpty()) { + m_model->setResourceEnabled(toDisable, EnableAction::DISABLE); + } + + m_model->update(); + emit m_model->dataChanged(m_model->index(0, 0), m_model->index(m_model->rowCount() - 1, m_model->columnCount(QModelIndex()) - 1)); + } else { + m_currentProfile = QString(); + m_profileTabBar->setProperty("currentProfileName", QString()); + } +} + +void ModFolderPage::onAddProfileClicked() { + bool ok; + QString name = QInputDialog::getText(this, tr("Add Profile"), tr("Profile Name:"), QLineEdit::Normal, QString(), &ok); + if (ok && !name.isEmpty()) { + for (int i = 0; i < m_profileTabBar->count(); ++i) { + if (m_profileTabBar->tabText(i) == name) { + QMessageBox::warning(this, tr("Warning"), tr("A profile with this name already exists.")); + return; + } + } + + QSet enabledMods; + if (!m_currentProfile.isEmpty() && m_profileStates.contains(m_currentProfile)) { + enabledMods = m_profileStates[m_currentProfile]; + } else { + for (int i = 0; i < m_model->rowCount(); ++i) { + const Resource& res = m_model->at(i); + if (res.enabled()) { + enabledMods.insert(res.getOriginalFileName()); + } + } + } + + m_profileStates[name] = enabledMods; + + QString newKey = "ModProfile_" + name; + m_instance->settings()->getOrRegisterSetting(newKey, QStringList()); + m_instance->settings()->set(newKey, QStringList(enabledMods.begin(), enabledMods.end())); + + m_profileTabBar->addTab(name); + QStringList profileList = m_instance->settings()->get("ModProfileList").toStringList(); + profileList.append(name); + m_instance->settings()->set("ModProfileList", profileList); + } +} + +void ModFolderPage::onRemoveProfileClicked() { + int index = m_profileTabBar->currentIndex(); + if (index != -1) { + QString name = m_profileTabBar->tabText(index); + + m_profileStates.remove(name); + QString key = "ModProfile_" + name; + m_instance->settings()->reset(key); + + m_profileTabBar->removeTab(index); + QStringList profileList = m_instance->settings()->get("ModProfileList").toStringList(); + profileList.removeAll(name); + m_instance->settings()->set("ModProfileList", profileList); + } +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 62db9fad8..a764967cc 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -39,43 +39,52 @@ #pragma once #include +#include +#include +#include #include "ExternalResourcesPage.h" #include "ui/dialogs/ResourceDownloadDialog.h" class ModFolderPage : public ExternalResourcesPage { Q_OBJECT - - inline bool handleNoModLoader(); - - public: - explicit ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent = nullptr); - virtual ~ModFolderPage() = default; - - void setFilter(const QString& filter) { m_fileSelectionFilter = filter; } - - virtual QString displayName() const override { return tr("Mods"); } - virtual QIcon icon() const override { return QIcon::fromTheme("loadermods"); } - virtual QString id() const override { return "mods"; } - virtual QString helpPage() const override { return "Loader-mods"; } - - virtual bool shouldDisplay() const override; - - public slots: - void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; - - private slots: - void removeItems(const QItemSelection& selection) override; - - void downloadMods(); - void downloadDialogFinished(int result); - void updateMods(bool includeDeps = false); - void deleteModMetadata(); - void exportModMetadata(); - void changeModVersion(); - - protected: - ModFolderModel* m_model; - QPointer m_downloadDialog; + + inline bool handleNoModLoader(); + + public: + explicit ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent = nullptr); + virtual ~ModFolderPage() = default; + + void setFilter(const QString& filter) { m_fileSelectionFilter = filter; } + + virtual QString displayName() const override { return tr("Mods"); } + virtual QIcon icon() const override { return QIcon::fromTheme("loadermods"); } + virtual QString id() const override { return "mods"; } + virtual QString helpPage() const override { return "Loader-mods"; } + + virtual bool shouldDisplay() const override; + + public slots: + void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; + + private slots: + void removeItems(const QItemSelection& selection) override; + + void downloadMods(); + void downloadDialogFinished(int result); + void updateMods(bool includeDeps = false); + void deleteModMetadata(); + void exportModMetadata(); + void changeModVersion(); + void onProfileTabChanged(int index); + void onAddProfileClicked(); + void onRemoveProfileClicked(); + + protected: + ModFolderModel* m_model; + QPointer m_downloadDialog; + QTabBar* m_profileTabBar = nullptr; + QMap> m_profileStates; + QString m_currentProfile; }; class CoreModFolderPage : public ModFolderPage { From abcfc06872e873f0c642cb32ad321f3af34c7a3e Mon Sep 17 00:00:00 2001 From: Vivek Kushwaha Date: Thu, 18 Jun 2026 18:03:40 +0530 Subject: [PATCH 2/5] WIP: investigation checkpoint before profile persistence fix Signed-off-by: Vivek Kushwaha --- launcher/minecraft/launch/ScanModFolders.cpp | 54 +- launcher/minecraft/launch/ScanModFolders.h | 9 +- .../minecraft/mod/ResourceFolderModel.cpp | 108 ++- launcher/ui/pages/instance/ModFolderPage.cpp | 798 +++++++++++++++--- launcher/ui/pages/instance/ModFolderPage.h | 43 +- 5 files changed, 897 insertions(+), 115 deletions(-) diff --git a/launcher/minecraft/launch/ScanModFolders.cpp b/launcher/minecraft/launch/ScanModFolders.cpp index cbe1599cb..a6f01a8e4 100644 --- a/launcher/minecraft/launch/ScanModFolders.cpp +++ b/launcher/minecraft/launch/ScanModFolders.cpp @@ -40,24 +40,48 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/mod/ModFolderModel.h" +#include +#include +#include + +extern QAtomicInt g_logSequence; +extern QString getActiveProfileForModel(void* modelPtr); + +ScanModFolders::ScanModFolders(LaunchTask* parent) : LaunchStep(parent) {} + +ScanModFolders::~ScanModFolders() +{ + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "ScanModFolders::~ScanModFolders()" + << "thread:" << QThread::currentThreadId(); +} + void ScanModFolders::executeTask() { auto m_inst = m_parent->instance(); auto loaders = m_inst->loaderModList(); - connect(loaders, &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); + auto cores = m_inst->coreModList(); + auto nils = m_inst->nilModList(); + + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "ScanModFolders::executeTask() START" + << "thread:" << QThread::currentThreadId() + << "loaderModel:" << loaders << "profile:" << getActiveProfileForModel(loaders) + << "coreModel:" << cores << "profile:" << getActiveProfileForModel(cores) + << "nilModel:" << nils << "profile:" << getActiveProfileForModel(nils); + + m_modsConnection = connect(loaders, &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); if (!loaders->update()) { m_modsDone = true; } - auto cores = m_inst->coreModList(); - connect(cores, &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); + m_coreModsConnection = connect(cores, &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); if (!cores->update()) { m_coreModsDone = true; } - auto nils = m_inst->nilModList(); - connect(nils, &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); + m_nilModsConnection = connect(nils, &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); if (!nils->update()) { m_nilModsDone = true; } @@ -85,6 +109,26 @@ void ScanModFolders::nilModsDone() void ScanModFolders::checkDone() { if (m_modsDone && m_coreModsDone && m_nilModsDone) { + auto m_inst = m_parent->instance(); + auto loaders = m_inst->loaderModList(); + auto cores = m_inst->coreModList(); + auto nils = m_inst->nilModList(); + + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "ScanModFolders::checkDone() END" + << "thread:" << QThread::currentThreadId() + << "loaderModel:" << loaders << "profile:" << getActiveProfileForModel(loaders) + << "coreModel:" << cores << "profile:" << getActiveProfileForModel(cores) + << "nilModel:" << nils << "profile:" << getActiveProfileForModel(nils); + + // Sever the connections to the persistent ModFolderModel singletons. + // Without this, updateFinished() would re-invoke these slots the next + // time the Edit dialog opens a mod-folder page, causing emitSucceeded() + // to be called a second time on an already-Succeeded task (Task.cpp:151). + disconnect(m_modsConnection); + disconnect(m_coreModsConnection); + disconnect(m_nilModsConnection); emitSucceeded(); } } + diff --git a/launcher/minecraft/launch/ScanModFolders.h b/launcher/minecraft/launch/ScanModFolders.h index 5d9350952..401629e83 100644 --- a/launcher/minecraft/launch/ScanModFolders.h +++ b/launcher/minecraft/launch/ScanModFolders.h @@ -15,14 +15,15 @@ #pragma once +#include #include #include class ScanModFolders : public LaunchStep { Q_OBJECT public: - explicit ScanModFolders(LaunchTask* parent) : LaunchStep(parent) {}; - virtual ~ScanModFolders() {}; + explicit ScanModFolders(LaunchTask* parent); + virtual ~ScanModFolders(); virtual void executeTask() override; virtual bool canAbort() const override { return false; } @@ -38,4 +39,8 @@ class ScanModFolders : public LaunchStep { bool m_modsDone = false; bool m_nilModsDone = false; bool m_coreModsDone = false; + + QMetaObject::Connection m_modsConnection; + QMetaObject::Connection m_coreModsConnection; + QMetaObject::Connection m_nilModsConnection; }; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index ddc132089..4449a1b7d 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -1,6 +1,12 @@ #include "ResourceFolderModel.h" #include +#include +#include + +extern QAtomicInt g_logSequence; +extern QString getActiveProfileForModel(void* modelPtr); + #include #include #include @@ -302,9 +308,29 @@ bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, Ena auto& resource = m_resources[row]; + QString originalName = resource->getOriginalFileName(); + QString oldPath = resource->fileinfo().absoluteFilePath(); + bool wasEnabled = resource->enabled(); + + qDebug() << "[INSTRUMENTATION-MUTATION]" << ++g_logSequence + << "setResourceEnabled() START" + << "resource:" << originalName + << "wasEnabled:" << wasEnabled + << "oldPath:" << oldPath + << "action:" << (action == EnableAction::ENABLE ? "ENABLE" : (action == EnableAction::DISABLE ? "DISABLE" : "TOGGLE")); + // Preserve the row, but change its ID auto oldId = resource->internalId(); - if (!resource->enable(action)) { + bool ok = resource->enable(action); + + qDebug() << "[INSTRUMENTATION-MUTATION]" << ++g_logSequence + << "setResourceEnabled() END" + << "resource:" << originalName + << "nowEnabled:" << resource->enabled() + << "newPath:" << resource->fileinfo().absoluteFilePath() + << "succeeded:" << ok; + + if (!ok) { succeeded = false; continue; } @@ -314,6 +340,12 @@ bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, Ena m_resourcesIndex.remove(oldId); m_resourcesIndex[newId] = row; + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "EMIT dataChanged() (setResourceEnabled)" + << "row:" << row + << "thread:" << QThread::currentThreadId() + << "model:" << this + << "profile:" << getActiveProfileForModel(this); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } @@ -348,6 +380,11 @@ bool ResourceFolderModel::update() m_scheduledUpdate = false; update(); } else { + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "EMIT updateFinished()" + << "thread:" << QThread::currentThreadId() + << "model:" << this + << "profile:" << getActiveProfileForModel(this); emit updateFinished(); } }, @@ -432,6 +469,12 @@ void ResourceFolderModel::onParseSucceeded(int ticket, const QString& resourceId } int row = m_resourcesIndex[resourceId]; + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "EMIT dataChanged() (onParseSucceeded)" + << "row:" << row + << "thread:" << QThread::currentThreadId() + << "model:" << this + << "profile:" << getActiveProfileForModel(this); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } @@ -856,6 +899,21 @@ void ResourceFolderModel::onParseFailed(int ticket, const QString& resourceId) void ResourceFolderModel::applyUpdates(QSet& currentSet, QSet& newSet, QMap& newResources) { + { + QSet enabledBefore; + for (const auto& res : m_resources) { + if (res->enabled()) { + enabledBefore.insert(res->getOriginalFileName()); + } + } + qDebug() << "[INSTRUMENTATION-APPLYUPDATES] START" + << "seq:" << ++g_logSequence + << "profile:" << getActiveProfileForModel(this) + << "enabledBefore:" << enabledBefore + << "currentSet:" << currentSet + << "newSet:" << newSet; + } + // see if the kept resources changed in some way { QSet keptSet = currentSet; @@ -875,6 +933,12 @@ void ResourceFolderModel::applyUpdates(QSet& currentSet, QSet& currentResource->updateIssues(m_instance); if (hadIssues != currentResource->hasIssues()) { + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "EMIT dataChanged() (applyUpdates unchanged file)" + << "row:" << row + << "thread:" << QThread::currentThreadId() + << "model:" << this + << "profile:" << getActiveProfileForModel(this); emit dataChanged(index(row, 0), index(row, columnCount({}) - 1)); } continue; @@ -894,6 +958,12 @@ void ResourceFolderModel::applyUpdates(QSet& currentSet, QSet& newResource->updateIssues(m_instance); resolveResource(m_resources.at(row)); + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "EMIT dataChanged() (applyUpdates changed file)" + << "row:" << row + << "thread:" << QThread::currentThreadId() + << "model:" << this + << "profile:" << getActiveProfileForModel(this); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } } @@ -959,6 +1029,42 @@ void ResourceFolderModel::applyUpdates(QSet& currentSet, QSet& idx++; } } + + { + QSet enabledAfter; + for (const auto& res : m_resources) { + if (res->enabled()) { + enabledAfter.insert(res->getOriginalFileName()); + } + } + + QSet keptSet = currentSet; + keptSet.intersect(newSet); + QSet removedSet = currentSet; + removedSet.subtract(newSet); + QSet addedSet = newSet; + addedSet.subtract(currentSet); + + qDebug() << "[INSTRUMENTATION-APPLYUPDATES] END" + << "seq:" << ++g_logSequence + << "profile:" << getActiveProfileForModel(this) + << "enabledAfter:" << enabledAfter + << "added:" << addedSet + << "removed:" << removedSet; + for (const auto& kept : keptSet) { + auto rowIt = m_resourcesIndex.constFind(kept); + if (rowIt != m_resourcesIndex.constEnd()) { + auto row = rowIt.value(); + auto& newResource = newResources[kept]; + const auto& currentResource = m_resources.at(row); + if (newResource->enabled() != currentResource->enabled()) { + qDebug() << " -> kept resource changed enabled state:" << kept + << "before:" << currentResource->enabled() + << "after:" << newResource->enabled(); + } + } + } + } } Resource::Ptr ResourceFolderModel::find(QString id) { diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 774d7535e..9ff5e0cd4 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -46,7 +46,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -70,32 +73,137 @@ #include "tasks/Task.h" #include "ui/dialogs/ProgressDialog.h" +#include +#include +#include +#include + +QMutex g_activeProfilesMutex; +QMap g_activeProfiles; +QAtomicInt g_logSequence(0); + +QString getActiveProfileForModel(void* modelPtr) { + QMutexLocker locker(&g_activeProfilesMutex); + return g_activeProfiles.value(modelPtr, QStringLiteral("Unknown/None")); +} + +void setActiveProfileForModel(void* modelPtr, const QString& profile) { + QMutexLocker locker(&g_activeProfilesMutex); + g_activeProfiles[modelPtr] = profile; +} + +void compareModelToProfileState(const QString& stage, ModFolderModel* model, const QSet& expectedMods, int seq) { + QSet actualMods; + for (int i = 0; i < model->rowCount(); ++i) { + const Resource& res = model->at(i); + if (res.enabled()) { + actualMods.insert(res.getOriginalFileName()); + } + } + + bool match = (expectedMods == actualMods); + qDebug() << "[INSTRUMENTATION-DIVERGE]" << seq << stage + << "expectedCount:" << expectedMods.size() + << "actualCount:" << actualMods.size() + << "match:" << match; + + if (!match) { + QSet missing = expectedMods; + missing.subtract(actualMods); + QSet unexpected = actualMods; + unexpected.subtract(expectedMods); + + qDebug() << " -> Expected Set:" << expectedMods; + qDebug() << " -> Actual Set:" << actualMods; + qDebug() << " -> Missing mods:" << missing; + qDebug() << " -> Unexpected enabled mods:" << unexpected; + } +} + +ModFolderPage::~ModFolderPage() +{ + m_destructorStarted = true; + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "ModFolderPage::~ModFolderPage() entered" + << "thread:" << QThread::currentThreadId() + << "model:" << m_model + << "currentProfile:" << m_currentProfile + << "destructorStarted:" << m_destructorStarted; + + { + QMutexLocker locker(&g_activeProfilesMutex); + g_activeProfiles.remove(m_model); + } + + if (m_filterWindow) { + m_filterWindow->removeEventFilter(this); + } + if (ui && ui->treeView && ui->treeView->viewport()) { + ui->treeView->viewport()->removeEventFilter(this); + } +} + + ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent) : ExternalResourcesPage(inst, model, parent), m_model(model) { m_profileTabBar = new QTabBar(this); + m_profileTabBar->setExpanding(false); + m_profileTabBar->setDrawBase(false); // no underline bar — matches Settings tab style + m_profileTabBar->setDocumentMode(false); + m_profileTabBar->setUsesScrollButtons(true); + m_profileTabBar->setElideMode(Qt::ElideRight); + m_profileTabBar->setContextMenuPolicy(Qt::CustomContextMenu); + // Palette-based stylesheet: selected tab uses palette(base) to match the + // treeView background; unselected tabs use the standard button tone. + m_profileTabBar->setStyleSheet( + "QTabBar::tab {" + " padding: 4px 12px;" + " border: none;" + " background: palette(button);" + " color: palette(button-text);" + " margin-right: 1px;" + "}" + "QTabBar::tab:selected {" + " background: palette(base);" + " color: palette(text);" + "}" + "QTabBar::tab:hover:!selected {" + " background: palette(mid);" + " color: palette(button-text);" + "}"); + + // Chrome-style "+" button that sits immediately after the last tab + m_newTabButton = new QToolButton(this); + m_newTabButton->setText(QStringLiteral("+")); + m_newTabButton->setToolTip(tr("New Tab")); + m_newTabButton->setAutoRaise(true); + m_newTabButton->setFixedSize(24, 24); + if (ui->treeView->parentWidget() && ui->treeView->parentWidget()->layout()) { QLayout* layout = ui->treeView->parentWidget()->layout(); - int index = layout->indexOf(ui->treeView); - - if (index != -1) { - if (auto box = qobject_cast(layout)) { - box->insertWidget(index, m_profileTabBar); - } - else if (auto grid = qobject_cast(layout)) { - grid->addWidget(m_profileTabBar, index, 0); - } + if (auto grid = qobject_cast(layout)) { + // Container keeps the tab bar + "+" button left-aligned while + // remaining flush with treeView (row=1, col=1, colSpan=2). + QWidget* tabRowContainer = new QWidget(this); + auto* tabRowLayout = new QHBoxLayout(tabRowContainer); + tabRowLayout->setContentsMargins(0, 0, 0, 0); + tabRowLayout->setSpacing(0); + tabRowLayout->addWidget(m_profileTabBar, 0); // natural tab-bar width + tabRowLayout->addWidget(m_newTabButton, 0); // fixed right of last tab + tabRowLayout->addStretch(1); // remaining space left empty + grid->addWidget(tabRowContainer, 0, 1, 1, 2); } } + + // Install viewport filter so clicking blank space in the list clears selection. + ui->treeView->viewport()->installEventFilter(this); + connect(m_profileTabBar, &QTabBar::currentChanged, this, &ModFolderPage::onProfileTabChanged); - - auto* addProfileAction = new QAction(tr("Add Profile"), this); - connect(addProfileAction, &QAction::triggered, this, &ModFolderPage::onAddProfileClicked); - ui->actionsToolbar->addAction(addProfileAction); - - auto* removeProfileAction = new QAction(tr("Remove Profile"), this); - connect(removeProfileAction, &QAction::triggered, this, &ModFolderPage::onRemoveProfileClicked); - ui->actionsToolbar->addAction(removeProfileAction); + connect(m_profileTabBar, &QTabBar::customContextMenuRequested, + this, &ModFolderPage::onTabContextMenuRequested); + connect(m_newTabButton, &QToolButton::clicked, this, [this] { + onTabNewToRight(m_profileTabBar->count() - 1); + }); ui->actionDownloadItem->setText(tr("Download Mods")); ui->actionDownloadItem->setToolTip(tr("Download mods from online mod platforms")); @@ -138,26 +246,61 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* ui->actionsToolbar->insertActionAfter(ui->actionViewFolder, ui->actionViewConfigs); - m_instance->settings()->getOrRegisterSetting("ModProfileList", QStringList() << "All Mods"); - QStringList profileList = m_instance->settings()->get("ModProfileList").toStringList(); + // Derive the settings key prefix from the model's directory name so that + // each page type ("mods", "coremods", "nilmods") stores profiles independently. + m_settingsPrefix = m_model->dir().dirName(); + + m_instance->settings()->getOrRegisterSetting(profileListKey(), QStringList() << "All Mods"); + QStringList profileList = m_instance->settings()->get(profileListKey()).toStringList(); m_profileTabBar->blockSignals(true); for (const QString& name : profileList) { m_profileTabBar->addTab(name); - QString key = "ModProfile_" + name; + QString key = profileKey(name); m_instance->settings()->getOrRegisterSetting(key, QStringList()); QStringList saved = m_instance->settings()->get(key).toStringList(); m_profileStates[name] = QSet(saved.begin(), saved.end()); + qDebug().noquote().nospace() + << "[PROFILE_STATE_WRITE]\n" + << "reason = constructor\n" + << "profile = " << name << "\n" + << "count = " << m_profileStates[name].size(); + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "m_profileStates write (constructor)" + << "profile:" << name + << "key:" << key + << "modsCount:" << m_profileStates[name].size(); } m_profileTabBar->blockSignals(false); - m_instance->settings()->getOrRegisterSetting("ModProfileLastActiveIndex", 0); - int savedIndex = m_instance->settings()->get("ModProfileLastActiveIndex").toInt(); + m_instance->settings()->getOrRegisterSetting(lastActiveIndexKey(), 0); + int savedIndex = m_instance->settings()->get(lastActiveIndexKey()).toInt(); if (savedIndex < 0 || savedIndex >= m_profileTabBar->count()) { savedIndex = 0; } m_profileTabBar->setCurrentIndex(savedIndex); onProfileTabChanged(m_profileTabBar->currentIndex()); + + connect(m_model, &ResourceFolderModel::updateFinished, this, [this]() { + qDebug().noquote().nospace() + << "[SAVE_TRIGGER]\n" + << "source = updateFinished\n" + << "currentProfile = " << m_currentProfile; + saveCurrentProfileState(); + }); + connect(m_model, &QAbstractItemModel::dataChanged, this, [this]() { + qDebug().noquote().nospace() + << "[SAVE_TRIGGER]\n" + << "source = dataChanged\n" + << "currentProfile = " << m_currentProfile; + saveCurrentProfileState(); + }); + connect(m_model, &ResourceFolderModel::updateFinished, this, [this] { + if (!m_currentProfile.isEmpty()) { + QSet expectedMods = m_profileStates.value(m_currentProfile); + compareModelToProfileState("updateFinished() FIRED", m_model, expectedMods, ++g_logSequence); + } + }); } bool ModFolderPage::shouldDisplay() const @@ -230,17 +373,38 @@ void ModFolderPage::downloadMods() void ModFolderPage::downloadDialogFinished(int result) { + if (m_downloadFlowActive) { + return; + } + m_downloadFlowActive = true; + + auto dialog = m_downloadDialog; + m_downloadDialog.clear(); + if (result != 0) { auto* tasks = new ConcurrentTask(tr("Download Mods"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](const QString& reason) { + auto terminalHandled = std::make_shared(false); + connect(tasks, &Task::failed, [this, tasks, terminalHandled](const QString& reason) { + if (*terminalHandled) { + return; + } + *terminalHandled = true; CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); - connect(tasks, &Task::aborted, [this, tasks]() { + connect(tasks, &Task::aborted, [this, tasks, terminalHandled]() { + if (*terminalHandled) { + return; + } + *terminalHandled = true; CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); - connect(tasks, &Task::succeeded, [this, tasks]() { + connect(tasks, &Task::succeeded, [this, tasks, terminalHandled]() { + if (*terminalHandled) { + return; + } + *terminalHandled = true; QStringList warnings = tasks->warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); @@ -249,8 +413,8 @@ void ModFolderPage::downloadDialogFinished(int result) tasks->deleteLater(); }); - if (m_downloadDialog) { - for (auto& task : m_downloadDialog->getTasks()) { + if (dialog) { + for (auto& task : dialog->getTasks()) { tasks->addTask(task); } } else { @@ -263,9 +427,10 @@ void ModFolderPage::downloadDialogFinished(int result) m_model->update(); } - if (m_downloadDialog) { - m_downloadDialog->deleteLater(); + if (dialog) { + dialog->deleteLater(); } + m_downloadFlowActive = false; } void ModFolderPage::updateMods(bool includeDeps) @@ -327,15 +492,28 @@ void ModFolderPage::updateMods(bool includeDeps) if (updateDialog.exec() != 0) { auto* tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](const QString& reason) { + auto terminalHandled = std::make_shared(false); + connect(tasks, &Task::failed, [this, tasks, terminalHandled](const QString& reason) { + if (*terminalHandled) { + return; + } + *terminalHandled = true; CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); - connect(tasks, &Task::aborted, [this, tasks]() { + connect(tasks, &Task::aborted, [this, tasks, terminalHandled]() { + if (*terminalHandled) { + return; + } + *terminalHandled = true; CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); - connect(tasks, &Task::succeeded, [this, tasks]() { + connect(tasks, &Task::succeeded, [this, tasks, terminalHandled]() { + if (*terminalHandled) { + return; + } + *terminalHandled = true; QStringList warnings = tasks->warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); @@ -413,7 +591,12 @@ void ModFolderPage::exportModMetadata() auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectedMods = m_model->selectedMods(selection); if (selectedMods.length() == 0) { - selectedMods = m_model->allMods(); + // Scope to the mods currently enabled by the active profile tab only. + for (auto* mod : m_model->allMods()) { + if (mod->enabled()) { + selectedMods.append(mod); + } + } } std::ranges::sort(selectedMods, [](const Mod* a, const Mod* b) { return a->name() < b->name(); }); @@ -421,6 +604,7 @@ void ModFolderPage::exportModMetadata() dlg.exec(); } + CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, ModFolderModel* mods, QWidget* parent) : ModFolderPage(inst, mods, parent) { auto* mcInst = dynamic_cast(m_instance); @@ -430,16 +614,16 @@ CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, ModFolderModel* mods, Q auto minecraftCmp = version->getComponent("net.minecraft"); if (!minecraftCmp->m_loaded) { version->reload(Net::Mode::Offline); + auto update = version->getCurrentTask(); - if (update) { + if (update && update->isRunning()) { connect(update.get(), &Task::finished, this, [this] { if (m_container) { m_container->refreshContainer(); } }); - if (!update->isRunning()) { - update->start(); - } + } else if (m_container) { + m_container->refreshContainer(); } } } @@ -494,50 +678,122 @@ inline bool ModFolderPage::handleNoModLoader() return true; } +void ModFolderPage::saveCurrentProfileState() +{ + if (m_profileLoading) { + return; + } + + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "saveCurrentProfileState() START" + << "thread:" << QThread::currentThreadId() + << "model:" << m_model + << "profile:" << m_currentProfile + << "destructorStarted:" << m_destructorStarted; + + if (m_currentProfile.isEmpty()) { + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "saveCurrentProfileState() END (empty profile)" + << "thread:" << QThread::currentThreadId() + << "model:" << m_model; + return; + } + bool stillExists = false; + for (int i = 0; i < m_profileTabBar->count(); ++i) { + if (m_profileTabBar->tabText(i) == m_currentProfile) { + stillExists = true; + break; + } + } + if (!stillExists) { + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "saveCurrentProfileState() END (profile no longer exists)" + << "thread:" << QThread::currentThreadId() + << "model:" << m_model + << "profile:" << m_currentProfile; + return; + } + QSet enabledMods; + for (int i = 0; i < m_model->rowCount(); ++i) { + const Resource& res = m_model->at(i); + if (res.enabled()) { + enabledMods.insert(res.getOriginalFileName()); + } + } + m_profileStates[m_currentProfile] = enabledMods; + qDebug().noquote().nospace() + << "[PROFILE_STATE_WRITE]\n" + << "reason = saveCurrentProfileState\n" + << "profile = " << m_currentProfile << "\n" + << "count = " << m_profileStates[m_currentProfile].size(); + QString key = profileKey(m_currentProfile); + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "m_profileStates write (saveCurrentProfileState)" + << "profile:" << m_currentProfile + << "key:" << key + << "modsCount:" << enabledMods.size(); + + m_instance->settings()->getOrRegisterSetting(key, QStringList()); + m_instance->settings()->set(key, QStringList(enabledMods.begin(), enabledMods.end())); + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "settings()->set write (saveCurrentProfileState)" + << "profile:" << m_currentProfile + << "key:" << key + << "modsCount:" << enabledMods.size(); + + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "saveCurrentProfileState() END (saved)" + << "thread:" << QThread::currentThreadId() + << "model:" << m_model + << "profile:" << m_currentProfile + << "savedModsCount:" << enabledMods.size(); +} + void ModFolderPage::onProfileTabChanged(int index) { + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "onProfileTabChanged() START" + << "index:" << index + << "thread:" << QThread::currentThreadId() + << "model:" << m_model + << "profileBefore:" << m_currentProfile + << "destructorStarted:" << m_destructorStarted; + if (m_currentProfile.isEmpty()) { QVariant val = m_profileTabBar->property("currentProfileName"); m_currentProfile = val.isValid() ? val.toString() : QString(); } - if (!m_currentProfile.isEmpty()) { - bool stillExists = false; - for (int i = 0; i < m_profileTabBar->count(); ++i) { - if (m_profileTabBar->tabText(i) == m_currentProfile) { - stillExists = true; - break; - } - } - if (stillExists) { - QSet enabledMods; - for (int i = 0; i < m_model->rowCount(); ++i) { - const Resource& res = m_model->at(i); - if (res.enabled()) { - enabledMods.insert(res.getOriginalFileName()); - } - } - m_profileStates[m_currentProfile] = enabledMods; - QString key = "ModProfile_" + m_currentProfile; - m_instance->settings()->getOrRegisterSetting(key, QStringList()); - m_instance->settings()->set(key, QStringList(enabledMods.begin(), enabledMods.end())); - } - } + saveCurrentProfileState(); + + m_profileLoading = true; if (index >= 0 && index < m_profileTabBar->count()) { QString tabName = m_profileTabBar->tabText(index); m_currentProfile = tabName; + setActiveProfileForModel(m_model, m_currentProfile); m_profileTabBar->setProperty("currentProfileName", tabName); - m_instance->settings()->set("ModProfileLastActiveIndex", index); + m_instance->settings()->set(lastActiveIndexKey(), index); QSet enabledMods; if (m_profileStates.contains(tabName)) { enabledMods = m_profileStates[tabName]; + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "profile load (onProfileTabChanged): cached state read" + << "thread:" << QThread::currentThreadId() + << "model:" << m_model + << "profile:" << tabName + << "modsCount:" << enabledMods.size(); } else { - QString key = "ModProfile_" + tabName; + QString key = profileKey(tabName); m_instance->settings()->getOrRegisterSetting(key, QStringList()); QStringList saved = m_instance->settings()->get(key).toStringList(); enabledMods = QSet(saved.begin(), saved.end()); m_profileStates[tabName] = enabledMods; + qDebug().noquote().nospace() + << "[PROFILE_STATE_WRITE]\n" + << "reason = onProfileTabChanged\n" + << "profile = " << tabName << "\n" + << "count = " << m_profileStates[tabName].size(); + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "m_profileStates write (onProfileTabChanged - settings read)" + << "profile:" << tabName + << "key:" << key + << "modsCount:" << enabledMods.size(); } QModelIndexList toEnable; @@ -562,62 +818,398 @@ void ModFolderPage::onProfileTabChanged(int index) { m_model->setResourceEnabled(toDisable, EnableAction::DISABLE); } - m_model->update(); + compareModelToProfileState("BEFORE update()", m_model, enabledMods, ++g_logSequence); + bool started = m_model->update(); + qDebug().noquote().nospace() + << "[ProfileSwitch]\n" + << "Profile: " << tabName << "\n" + << "Enable: " << toEnable.size() << "\n" + << "Disable: " << toDisable.size() << "\n" + << "updateStarted: " << (started ? "true" : "false"); + compareModelToProfileState("AFTER update()", m_model, enabledMods, ++g_logSequence); + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "EMIT m_model->dataChanged() (onProfileTabChanged layout update)" + << "thread:" << QThread::currentThreadId() + << "model:" << m_model + << "profile:" << m_currentProfile; emit m_model->dataChanged(m_model->index(0, 0), m_model->index(m_model->rowCount() - 1, m_model->columnCount(QModelIndex()) - 1)); } else { m_currentProfile = QString(); + setActiveProfileForModel(m_model, QStringLiteral("None")); m_profileTabBar->setProperty("currentProfileName", QString()); } + + m_profileLoading = false; + + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "onProfileTabChanged() END" + << "index:" << index + << "thread:" << QThread::currentThreadId() + << "model:" << m_model + << "profileAfter:" << m_currentProfile; } + void ModFolderPage::onAddProfileClicked() { bool ok; - QString name = QInputDialog::getText(this, tr("Add Profile"), tr("Profile Name:"), QLineEdit::Normal, QString(), &ok); + QString name = QInputDialog::getText(this, tr("Add Profile"), tr("Profile Name:"), + QLineEdit::Normal, QString(), &ok); if (ok && !name.isEmpty()) { - for (int i = 0; i < m_profileTabBar->count(); ++i) { - if (m_profileTabBar->tabText(i) == name) { - QMessageBox::warning(this, tr("Warning"), tr("A profile with this name already exists.")); - return; - } - } - - QSet enabledMods; - if (!m_currentProfile.isEmpty() && m_profileStates.contains(m_currentProfile)) { - enabledMods = m_profileStates[m_currentProfile]; - } else { - for (int i = 0; i < m_model->rowCount(); ++i) { - const Resource& res = m_model->at(i); - if (res.enabled()) { - enabledMods.insert(res.getOriginalFileName()); - } - } - } - - m_profileStates[name] = enabledMods; - - QString newKey = "ModProfile_" + name; - m_instance->settings()->getOrRegisterSetting(newKey, QStringList()); - m_instance->settings()->set(newKey, QStringList(enabledMods.begin(), enabledMods.end())); - - m_profileTabBar->addTab(name); - QStringList profileList = m_instance->settings()->get("ModProfileList").toStringList(); - profileList.append(name); - m_instance->settings()->set("ModProfileList", profileList); + // Always create a blank-slate profile — no state bleeding from the current profile. + createProfile(name, QSet{}); } } void ModFolderPage::onRemoveProfileClicked() { int index = m_profileTabBar->currentIndex(); - if (index != -1) { - QString name = m_profileTabBar->tabText(index); - - m_profileStates.remove(name); - QString key = "ModProfile_" + name; - m_instance->settings()->reset(key); - - m_profileTabBar->removeTab(index); - QStringList profileList = m_instance->settings()->get("ModProfileList").toStringList(); - profileList.removeAll(name); - m_instance->settings()->set("ModProfileList", profileList); + // Guard: index 0 is the base profile and must never be removed. + if (index > 0) { + onTabRemove(index); } } + +// --------------------------------------------------------------------------- +// Profile creation helpers +// --------------------------------------------------------------------------- + +void ModFolderPage::createProfile(const QString& name, const QSet& initialState, + int insertAfterIndex) +{ + // Guard: duplicate name + for (int i = 0; i < m_profileTabBar->count(); ++i) { + if (m_profileTabBar->tabText(i) == name) { + QMessageBox::warning(this, tr("Warning"), + tr("A profile with this name already exists.")); + return; + } + } + + // Persist state to memory and settings + m_profileStates[name] = initialState; + qDebug().noquote().nospace() + << "[PROFILE_STATE_WRITE]\n" + << "reason = createProfile\n" + << "profile = " << name << "\n" + << "count = " << m_profileStates[name].size(); + QString key = profileKey(name); + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "m_profileStates write (createProfile)" + << "profile:" << name + << "key:" << key + << "modsCount:" << initialState.size(); + + m_instance->settings()->getOrRegisterSetting(key, QStringList()); + m_instance->settings()->set(key, QStringList(initialState.begin(), initialState.end())); + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "settings()->set write (createProfile)" + << "profile:" << name + << "key:" << key + << "modsCount:" << initialState.size(); + + // Insert tab at the correct position + if (insertAfterIndex >= 0 && insertAfterIndex < m_profileTabBar->count()) { + m_profileTabBar->insertTab(insertAfterIndex + 1, name); + } else { + m_profileTabBar->addTab(name); + } + + saveProfileList(); +} + +void ModFolderPage::saveProfileList() +{ + QStringList profileList; + for (int i = 0; i < m_profileTabBar->count(); ++i) { + profileList.append(m_profileTabBar->tabText(i)); + } + m_instance->settings()->set(profileListKey(), profileList); +} + +// --------------------------------------------------------------------------- +// Tab context-menu +// --------------------------------------------------------------------------- + +void ModFolderPage::onTabContextMenuRequested(const QPoint& pos) +{ + int tabIndex = m_profileTabBar->tabAt(pos); + + QMenu menu(this); + + // ── Management group ──────────────────────────────────────────────────── + auto* newToRight = menu.addAction(tr("New Tab to Right")); + int insertAfter = tabIndex >= 0 ? tabIndex : m_profileTabBar->count() - 1; + connect(newToRight, &QAction::triggered, this, [this, insertAfter] { + onTabNewToRight(insertAfter); + }); + + if (tabIndex >= 0) { + auto* duplicate = menu.addAction(tr("Duplicate")); + connect(duplicate, &QAction::triggered, this, [this, tabIndex] { + onTabDuplicate(tabIndex); + }); + + // Rename and Remove are permanently blocked for the base profile (index 0). + if (tabIndex != 0) { + auto* rename = menu.addAction(tr("Rename")); + connect(rename, &QAction::triggered, this, [this, tabIndex] { + onTabRename(tabIndex); + }); + + auto* remove = menu.addAction(tr("Remove")); + connect(remove, &QAction::triggered, this, [this, tabIndex] { + onTabRemove(tabIndex); + }); + } + + // ── Selection / bulk-action group ──────────────────────────────────── + menu.addSeparator(); + + auto* enableAll = menu.addAction(tr("Enable All")); + connect(enableAll, &QAction::triggered, this, [this, tabIndex] { + onTabEnableAll(tabIndex); + }); + + auto* disableAll = menu.addAction(tr("Disable All")); + connect(disableAll, &QAction::triggered, this, [this, tabIndex] { + onTabDisableAll(tabIndex); + }); + } + + menu.exec(m_profileTabBar->mapToGlobal(pos)); +} + +void ModFolderPage::onTabNewToRight(int sourceIndex) +{ + bool ok; + QString name = QInputDialog::getText(this, tr("New Profile"), tr("Profile Name:"), + QLineEdit::Normal, QString(), &ok); + if (ok && !name.isEmpty()) { + createProfile(name, QSet{}, sourceIndex); + } +} + +void ModFolderPage::onTabDuplicate(int sourceIndex) +{ + // Flush the live state of the currently active profile before reading source. + if (!m_currentProfile.isEmpty()) { + QSet enabledMods; + for (int i = 0; i < m_model->rowCount(); ++i) { + const Resource& res = m_model->at(i); + if (res.enabled()) { + enabledMods.insert(res.getOriginalFileName()); + } + } + m_profileStates[m_currentProfile] = enabledMods; + qDebug().noquote().nospace() + << "[PROFILE_STATE_WRITE]\n" + << "reason = onTabDuplicate\n" + << "profile = " << m_currentProfile << "\n" + << "count = " << m_profileStates[m_currentProfile].size(); + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "m_profileStates write (onTabDuplicate)" + << "profile:" << m_currentProfile + << "key:" << profileKey(m_currentProfile) + << "modsCount:" << enabledMods.size(); + } + + QString sourceName = m_profileTabBar->tabText(sourceIndex); + QSet sourceState = m_profileStates.value(sourceName); + + bool ok; + QString name = QInputDialog::getText(this, tr("Duplicate Profile"), + tr("New Profile Name:"), QLineEdit::Normal, + tr("Copy of %1").arg(sourceName), &ok); + if (ok && !name.isEmpty()) { + createProfile(name, sourceState, sourceIndex); + } +} + +void ModFolderPage::onTabRename(int tabIndex) +{ + if (tabIndex == 0) return; // Guard: base profile cannot be renamed + + QString oldName = m_profileTabBar->tabText(tabIndex); + + bool ok; + QString newName = QInputDialog::getText(this, tr("Rename Profile"), + tr("New Profile Name:"), QLineEdit::Normal, + oldName, &ok); + + if (!ok || newName.isEmpty() || newName == oldName) return; + + // Validate: no duplicate names + for (int i = 0; i < m_profileTabBar->count(); ++i) { + if (m_profileTabBar->tabText(i) == newName) { + QMessageBox::warning(this, tr("Warning"), + tr("A profile named \"%1\" already exists.").arg(newName)); + return; + } + } + + // Migrate in-memory state to the new key + if (m_profileStates.contains(oldName)) { + m_profileStates[newName] = m_profileStates.take(oldName); + qDebug().noquote().nospace() + << "[PROFILE_STATE_WRITE]\n" + << "reason = onTabRename\n" + << "profile = " << newName << "\n" + << "count = " << m_profileStates[newName].size(); + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "m_profileStates write (onTabRename)" + << "profile:" << newName + << "key:" << profileKey(newName) + << "modsCount:" << m_profileStates[newName].size(); + } + + // Migrate settings: read → reset old key → write under new key + QString oldKey = profileKey(oldName); + QString newKey = profileKey(newName); + QStringList stateData = m_instance->settings()->get(oldKey).toStringList(); + m_instance->settings()->reset(oldKey); + m_instance->settings()->getOrRegisterSetting(newKey, QStringList()); + m_instance->settings()->set(newKey, stateData); + qDebug() << "[INSTRUMENTATION]" << ++g_logSequence + << "settings()->set write (onTabRename)" + << "profile:" << newName + << "key:" << newKey + << "modsCount:" << stateData.size(); + + // Update the visible tab label + m_profileTabBar->setTabText(tabIndex, newName); + + // Keep current-profile tracking in sync + if (m_currentProfile == oldName) { + m_currentProfile = newName; + m_profileTabBar->setProperty("currentProfileName", newName); + } + + saveProfileList(); +} + +void ModFolderPage::onTabRemove(int tabIndex) +{ + if (tabIndex == 0) return; // Guard: base profile cannot be removed + + QString name = m_profileTabBar->tabText(tabIndex); + + auto response = CustomMessageBox::selectable( + this, tr("Remove Profile"), + tr("Are you sure you want to remove the profile \"%1\"?\n" + "This cannot be undone.").arg(name), + QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, QMessageBox::No)->exec(); + + if (response != QMessageBox::Yes) return; + + m_profileStates.remove(name); + qDebug().noquote().nospace() + << "[PROFILE_STATE_WRITE]\n" + << "reason = onTabRemove\n" + << "profile = " << name << "\n" + << "count = 0"; + m_instance->settings()->reset(profileKey(name)); + + m_profileTabBar->removeTab(tabIndex); + saveProfileList(); +} + +void ModFolderPage::onTabEnableAll(int tabIndex) +{ + if (m_profileTabBar->currentIndex() != tabIndex) { + m_profileTabBar->setCurrentIndex(tabIndex); + } + // Build the full index list and enable every mod in the active profile. + QModelIndexList allIndices; + allIndices.reserve(m_model->rowCount()); + for (int i = 0; i < m_model->rowCount(); ++i) { + allIndices.append(m_model->index(i, 0)); + } + if (!allIndices.isEmpty()) { + m_model->setResourceEnabled(allIndices, EnableAction::ENABLE); + m_model->update(); + } +} + +void ModFolderPage::onTabDisableAll(int tabIndex) +{ + if (m_profileTabBar->currentIndex() != tabIndex) { + m_profileTabBar->setCurrentIndex(tabIndex); + } + // Build the full index list and disable every mod in the active profile. + QModelIndexList allIndices; + allIndices.reserve(m_model->rowCount()); + for (int i = 0; i < m_model->rowCount(); ++i) { + allIndices.append(m_model->index(i, 0)); + } + if (!allIndices.isEmpty()) { + m_model->setResourceEnabled(allIndices, EnableAction::DISABLE); + m_model->update(); + } +} + +bool ModFolderPage::eventFilter(QObject* obj, QEvent* ev) +{ + // Safety guard: ensure the page, its UI, and tree view are fully valid + if (!ui || !ui->treeView || !m_model) { + return ExternalResourcesPage::eventFilter(obj, ev); + } + + // When focus or clicks land outside this page (e.g. the instance sidebar), clear selection. + if (ev->type() == QEvent::MouseButtonPress || ev->type() == QEvent::FocusIn) { + if (this->isVisible()) { + QWidget* widget = qobject_cast(obj); + if (widget) { + if (!this->isAncestorOf(widget) && widget != this) { + ui->treeView->clearSelection(); + ui->treeView->setCurrentIndex(QModelIndex()); + } + } + } + } + + // Clear treeView selection when the user clicks on empty space below the + // last row, so that Export List reverts to the full active-profile scope. + if (obj == ui->treeView->viewport() && ev->type() == QEvent::MouseButtonPress) { + auto* me = static_cast(ev); + const QModelIndex idx = ui->treeView->indexAt(me->pos()); + if (!idx.isValid()) { + ui->treeView->clearSelection(); + ui->treeView->setCurrentIndex(QModelIndex()); + return true; + } + } + return ExternalResourcesPage::eventFilter(obj, ev); +} + +void ModFolderPage::mousePressEvent(QMouseEvent* event) +{ + if (ui->treeView) { + QPoint posInTreeView = ui->treeView->mapFrom(this, event->pos()); + if (!ui->treeView->rect().contains(posInTreeView)) { + ui->treeView->clearSelection(); + ui->treeView->setCurrentIndex(QModelIndex()); + } + } + ExternalResourcesPage::mousePressEvent(event); +} + +void ModFolderPage::showEvent(QShowEvent* event) +{ + ExternalResourcesPage::showEvent(event); + QWidget* w = window(); + if (w && m_filterWindow != w) { + if (m_filterWindow) { + m_filterWindow->removeEventFilter(this); + } + m_filterWindow = w; + w->installEventFilter(this); + } +} + +void ModFolderPage::hideEvent(QHideEvent* event) +{ + if (m_filterWindow) { + m_filterWindow->removeEventFilter(this); + m_filterWindow = nullptr; + } + ExternalResourcesPage::hideEvent(event); +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index a764967cc..d835560b2 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -42,6 +42,7 @@ #include #include #include +#include #include "ExternalResourcesPage.h" #include "ui/dialogs/ResourceDownloadDialog.h" @@ -52,7 +53,7 @@ class ModFolderPage : public ExternalResourcesPage { public: explicit ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent = nullptr); - virtual ~ModFolderPage() = default; + virtual ~ModFolderPage(); void setFilter(const QString& filter) { m_fileSelectionFilter = filter; } @@ -68,7 +69,7 @@ class ModFolderPage : public ExternalResourcesPage { private slots: void removeItems(const QItemSelection& selection) override; - + void downloadMods(); void downloadDialogFinished(int result); void updateMods(bool includeDeps = false); @@ -78,13 +79,47 @@ class ModFolderPage : public ExternalResourcesPage { void onProfileTabChanged(int index); void onAddProfileClicked(); void onRemoveProfileClicked(); - + void saveCurrentProfileState(); + + // Tab context-menu slots + void onTabContextMenuRequested(const QPoint& pos); + void onTabNewToRight(int sourceIndex); + void onTabDuplicate(int sourceIndex); + void onTabRename(int tabIndex); + void onTabRemove(int tabIndex); + void onTabEnableAll(int tabIndex); + void onTabDisableAll(int tabIndex); + + private: + // Creates a new profile tab with the given initial mod-enable state. + // Pass an empty QSet for a blank slate; pass a copied QSet for duplication. + void createProfile(const QString& name, const QSet& initialState, int insertAfterIndex = -1); + // Re-serialises the current tab order into the "ModProfileList" setting. + void saveProfileList(); + + // Settings-key helpers — namespaced by model directory to prevent + // Mods / CoreMods / NilMods pages from overwriting each other's state. + QString profileListKey() const { return QStringLiteral("ModProfileList_") + m_settingsPrefix; } + QString profileKey(const QString& name) const { return m_settingsPrefix + QStringLiteral("/ModProfile_") + name; } + QString lastActiveIndexKey() const { return QStringLiteral("ModProfileLastActiveIndex_") + m_settingsPrefix; } + protected: + bool eventFilter(QObject* obj, QEvent* ev) override; + void mousePressEvent(QMouseEvent* event) override; + void showEvent(QShowEvent* event) override; + void hideEvent(QHideEvent* event) override; + ModFolderModel* m_model; QPointer m_downloadDialog; - QTabBar* m_profileTabBar = nullptr; + QPointer m_filterWindow; + bool m_downloadFlowActive = false; + QTabBar* m_profileTabBar = nullptr; + QToolButton* m_newTabButton = nullptr; ///< Chrome-style "+" button appended after last tab QMap> m_profileStates; QString m_currentProfile; + QString m_settingsPrefix; + bool m_destructorStarted = false; + bool m_profileLoading = false; }; class CoreModFolderPage : public ModFolderPage { From f9e8dbfcc0cc94ca70989b275e67ce39739cfc4c Mon Sep 17 00:00:00 2001 From: Vivek Kushwaha Date: Thu, 18 Jun 2026 18:35:28 +0530 Subject: [PATCH 3/5] Fix profile persistence race during asynchronous mod profile switching Signed-off-by: Vivek Kushwaha --- launcher/ui/pages/instance/ModFolderPage.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 9ff5e0cd4..a7e8f659e 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -286,6 +286,10 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* << "[SAVE_TRIGGER]\n" << "source = updateFinished\n" << "currentProfile = " << m_currentProfile; + if (m_profileLoading) { + m_profileLoading = false; + return; + } saveCurrentProfileState(); }); connect(m_model, &QAbstractItemModel::dataChanged, this, [this]() { @@ -837,10 +841,9 @@ void ModFolderPage::onProfileTabChanged(int index) { m_currentProfile = QString(); setActiveProfileForModel(m_model, QStringLiteral("None")); m_profileTabBar->setProperty("currentProfileName", QString()); + m_profileLoading = false; } - m_profileLoading = false; - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "onProfileTabChanged() END" << "index:" << index << "thread:" << QThread::currentThreadId() From 352b45bf5e89c0eae0dba047eaefda9098bf971a Mon Sep 17 00:00:00 2001 From: Vivek Kushwaha Date: Tue, 23 Jun 2026 14:58:15 +0530 Subject: [PATCH 4/5] fix(ModFolderPage): prevent stale profile saves during tab switching Signed-off-by: Vivek Kushwaha --- launcher/ui/pages/instance/ModFolderPage.cpp | 59 +++++++++++--------- launcher/ui/pages/instance/ModFolderPage.h | 19 ++++--- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index a7e8f659e..348ac4a4b 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -198,7 +198,10 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* // Install viewport filter so clicking blank space in the list clears selection. ui->treeView->viewport()->installEventFilter(this); - connect(m_profileTabBar, &QTabBar::currentChanged, this, &ModFolderPage::onProfileTabChanged); + connect(m_profileTabBar, &QTabBar::currentChanged, this, [this](int index) { + ++m_profileSwitchGeneration; + applyProfileSwitch(index, m_profileSwitchGeneration); + }); connect(m_profileTabBar, &QTabBar::customContextMenuRequested, this, &ModFolderPage::onTabContextMenuRequested); connect(m_newTabButton, &QToolButton::clicked, this, [this] { @@ -279,30 +282,15 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* savedIndex = 0; } m_profileTabBar->setCurrentIndex(savedIndex); - onProfileTabChanged(m_profileTabBar->currentIndex()); + applyProfileSwitch(m_profileTabBar->currentIndex(), m_profileSwitchGeneration); - connect(m_model, &ResourceFolderModel::updateFinished, this, [this]() { - qDebug().noquote().nospace() - << "[SAVE_TRIGGER]\n" - << "source = updateFinished\n" - << "currentProfile = " << m_currentProfile; - if (m_profileLoading) { - m_profileLoading = false; - return; - } - saveCurrentProfileState(); - }); connect(m_model, &QAbstractItemModel::dataChanged, this, [this]() { - qDebug().noquote().nospace() - << "[SAVE_TRIGGER]\n" - << "source = dataChanged\n" - << "currentProfile = " << m_currentProfile; - saveCurrentProfileState(); - }); - connect(m_model, &ResourceFolderModel::updateFinished, this, [this] { - if (!m_currentProfile.isEmpty()) { - QSet expectedMods = m_profileStates.value(m_currentProfile); - compareModelToProfileState("updateFinished() FIRED", m_model, expectedMods, ++g_logSequence); + if (!m_applyingProfile) { + qDebug().noquote().nospace() + << "[SAVE_TRIGGER]\n" + << "source = dataChanged\n" + << "currentProfile = " << m_currentProfile; + saveCurrentProfileState(); } }); } @@ -749,7 +737,7 @@ void ModFolderPage::saveCurrentProfileState() << "savedModsCount:" << enabledMods.size(); } -void ModFolderPage::onProfileTabChanged(int index) { +void ModFolderPage::applyProfileSwitch(int index, int generation) { qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "onProfileTabChanged() START" << "index:" << index << "thread:" << QThread::currentThreadId() @@ -823,7 +811,23 @@ void ModFolderPage::onProfileTabChanged(int index) { } compareModelToProfileState("BEFORE update()", m_model, enabledMods, ++g_logSequence); - bool started = m_model->update(); + + // Capture generation token. If tab changes again before this update + // completes, the token will be stale and we discard the result. + int capturedGeneration = generation; + m_applyingProfile = true; + connect(m_model, &ResourceFolderModel::updateFinished, this, + [this, capturedGeneration] { + if (capturedGeneration == m_profileSwitchGeneration) { + saveCurrentProfileState(); + } + m_applyingProfile = false; + }, + Qt::SingleShotConnection); + + m_model->update(); + + qDebug().noquote().nospace() << "[ProfileSwitch]\n" << "Profile: " << tabName << "\n" @@ -842,6 +846,7 @@ void ModFolderPage::onProfileTabChanged(int index) { setActiveProfileForModel(m_model, QStringLiteral("None")); m_profileTabBar->setProperty("currentProfileName", QString()); m_profileLoading = false; + m_applyingProfile = false; } qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "onProfileTabChanged() END" @@ -1119,6 +1124,8 @@ void ModFolderPage::onTabEnableAll(int tabIndex) { if (m_profileTabBar->currentIndex() != tabIndex) { m_profileTabBar->setCurrentIndex(tabIndex); + } else { + applyProfileSwitch(tabIndex, m_profileSwitchGeneration); } // Build the full index list and enable every mod in the active profile. QModelIndexList allIndices; @@ -1136,6 +1143,8 @@ void ModFolderPage::onTabDisableAll(int tabIndex) { if (m_profileTabBar->currentIndex() != tabIndex) { m_profileTabBar->setCurrentIndex(tabIndex); + } else { + applyProfileSwitch(tabIndex, m_profileSwitchGeneration); } // Build the full index list and disable every mod in the active profile. QModelIndexList allIndices; diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index d835560b2..a32bfc192 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -48,25 +48,25 @@ class ModFolderPage : public ExternalResourcesPage { Q_OBJECT - + inline bool handleNoModLoader(); - + public: explicit ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent = nullptr); virtual ~ModFolderPage(); - + void setFilter(const QString& filter) { m_fileSelectionFilter = filter; } - + virtual QString displayName() const override { return tr("Mods"); } virtual QIcon icon() const override { return QIcon::fromTheme("loadermods"); } virtual QString id() const override { return "mods"; } virtual QString helpPage() const override { return "Loader-mods"; } - + virtual bool shouldDisplay() const override; - + public slots: void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; - + private slots: void removeItems(const QItemSelection& selection) override; @@ -76,7 +76,6 @@ class ModFolderPage : public ExternalResourcesPage { void deleteModMetadata(); void exportModMetadata(); void changeModVersion(); - void onProfileTabChanged(int index); void onAddProfileClicked(); void onRemoveProfileClicked(); void saveCurrentProfileState(); @@ -91,6 +90,8 @@ class ModFolderPage : public ExternalResourcesPage { void onTabDisableAll(int tabIndex); private: + void applyProfileSwitch(int index, int generation); + // Creates a new profile tab with the given initial mod-enable state. // Pass an empty QSet for a blank slate; pass a copied QSet for duplication. void createProfile(const QString& name, const QSet& initialState, int insertAfterIndex = -1); @@ -120,6 +121,8 @@ class ModFolderPage : public ExternalResourcesPage { QString m_settingsPrefix; bool m_destructorStarted = false; bool m_profileLoading = false; + int m_profileSwitchGeneration = 0; + bool m_applyingProfile = false; }; class CoreModFolderPage : public ModFolderPage { From 8e85ecfd439bb73fdec8f9dbf71eb094efdfc383 Mon Sep 17 00:00:00 2001 From: Vivek Kushwaha Date: Tue, 23 Jun 2026 17:53:36 +0530 Subject: [PATCH 5/5] fix(ModFolderPage): preserve profile state synchronization during cleanup Signed-off-by: Vivek Kushwaha --- launcher/minecraft/launch/ScanModFolders.cpp | 28 --- .../minecraft/mod/ResourceFolderModel.cpp | 108 +-------- launcher/ui/pages/instance/ModFolderPage.cpp | 223 +----------------- launcher/ui/pages/instance/ModFolderPage.h | 1 - 4 files changed, 12 insertions(+), 348 deletions(-) diff --git a/launcher/minecraft/launch/ScanModFolders.cpp b/launcher/minecraft/launch/ScanModFolders.cpp index a6f01a8e4..45a326e82 100644 --- a/launcher/minecraft/launch/ScanModFolders.cpp +++ b/launcher/minecraft/launch/ScanModFolders.cpp @@ -40,20 +40,10 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/mod/ModFolderModel.h" -#include -#include -#include - -extern QAtomicInt g_logSequence; -extern QString getActiveProfileForModel(void* modelPtr); - ScanModFolders::ScanModFolders(LaunchTask* parent) : LaunchStep(parent) {} ScanModFolders::~ScanModFolders() { - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "ScanModFolders::~ScanModFolders()" - << "thread:" << QThread::currentThreadId(); } void ScanModFolders::executeTask() @@ -64,13 +54,6 @@ void ScanModFolders::executeTask() auto cores = m_inst->coreModList(); auto nils = m_inst->nilModList(); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "ScanModFolders::executeTask() START" - << "thread:" << QThread::currentThreadId() - << "loaderModel:" << loaders << "profile:" << getActiveProfileForModel(loaders) - << "coreModel:" << cores << "profile:" << getActiveProfileForModel(cores) - << "nilModel:" << nils << "profile:" << getActiveProfileForModel(nils); - m_modsConnection = connect(loaders, &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); if (!loaders->update()) { m_modsDone = true; @@ -109,17 +92,6 @@ void ScanModFolders::nilModsDone() void ScanModFolders::checkDone() { if (m_modsDone && m_coreModsDone && m_nilModsDone) { - auto m_inst = m_parent->instance(); - auto loaders = m_inst->loaderModList(); - auto cores = m_inst->coreModList(); - auto nils = m_inst->nilModList(); - - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "ScanModFolders::checkDone() END" - << "thread:" << QThread::currentThreadId() - << "loaderModel:" << loaders << "profile:" << getActiveProfileForModel(loaders) - << "coreModel:" << cores << "profile:" << getActiveProfileForModel(cores) - << "nilModel:" << nils << "profile:" << getActiveProfileForModel(nils); // Sever the connections to the persistent ModFolderModel singletons. // Without this, updateFinished() would re-invoke these slots the next diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 4449a1b7d..ddc132089 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -1,12 +1,6 @@ #include "ResourceFolderModel.h" #include -#include -#include - -extern QAtomicInt g_logSequence; -extern QString getActiveProfileForModel(void* modelPtr); - #include #include #include @@ -308,29 +302,9 @@ bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, Ena auto& resource = m_resources[row]; - QString originalName = resource->getOriginalFileName(); - QString oldPath = resource->fileinfo().absoluteFilePath(); - bool wasEnabled = resource->enabled(); - - qDebug() << "[INSTRUMENTATION-MUTATION]" << ++g_logSequence - << "setResourceEnabled() START" - << "resource:" << originalName - << "wasEnabled:" << wasEnabled - << "oldPath:" << oldPath - << "action:" << (action == EnableAction::ENABLE ? "ENABLE" : (action == EnableAction::DISABLE ? "DISABLE" : "TOGGLE")); - // Preserve the row, but change its ID auto oldId = resource->internalId(); - bool ok = resource->enable(action); - - qDebug() << "[INSTRUMENTATION-MUTATION]" << ++g_logSequence - << "setResourceEnabled() END" - << "resource:" << originalName - << "nowEnabled:" << resource->enabled() - << "newPath:" << resource->fileinfo().absoluteFilePath() - << "succeeded:" << ok; - - if (!ok) { + if (!resource->enable(action)) { succeeded = false; continue; } @@ -340,12 +314,6 @@ bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, Ena m_resourcesIndex.remove(oldId); m_resourcesIndex[newId] = row; - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "EMIT dataChanged() (setResourceEnabled)" - << "row:" << row - << "thread:" << QThread::currentThreadId() - << "model:" << this - << "profile:" << getActiveProfileForModel(this); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } @@ -380,11 +348,6 @@ bool ResourceFolderModel::update() m_scheduledUpdate = false; update(); } else { - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "EMIT updateFinished()" - << "thread:" << QThread::currentThreadId() - << "model:" << this - << "profile:" << getActiveProfileForModel(this); emit updateFinished(); } }, @@ -469,12 +432,6 @@ void ResourceFolderModel::onParseSucceeded(int ticket, const QString& resourceId } int row = m_resourcesIndex[resourceId]; - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "EMIT dataChanged() (onParseSucceeded)" - << "row:" << row - << "thread:" << QThread::currentThreadId() - << "model:" << this - << "profile:" << getActiveProfileForModel(this); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } @@ -899,21 +856,6 @@ void ResourceFolderModel::onParseFailed(int ticket, const QString& resourceId) void ResourceFolderModel::applyUpdates(QSet& currentSet, QSet& newSet, QMap& newResources) { - { - QSet enabledBefore; - for (const auto& res : m_resources) { - if (res->enabled()) { - enabledBefore.insert(res->getOriginalFileName()); - } - } - qDebug() << "[INSTRUMENTATION-APPLYUPDATES] START" - << "seq:" << ++g_logSequence - << "profile:" << getActiveProfileForModel(this) - << "enabledBefore:" << enabledBefore - << "currentSet:" << currentSet - << "newSet:" << newSet; - } - // see if the kept resources changed in some way { QSet keptSet = currentSet; @@ -933,12 +875,6 @@ void ResourceFolderModel::applyUpdates(QSet& currentSet, QSet& currentResource->updateIssues(m_instance); if (hadIssues != currentResource->hasIssues()) { - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "EMIT dataChanged() (applyUpdates unchanged file)" - << "row:" << row - << "thread:" << QThread::currentThreadId() - << "model:" << this - << "profile:" << getActiveProfileForModel(this); emit dataChanged(index(row, 0), index(row, columnCount({}) - 1)); } continue; @@ -958,12 +894,6 @@ void ResourceFolderModel::applyUpdates(QSet& currentSet, QSet& newResource->updateIssues(m_instance); resolveResource(m_resources.at(row)); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "EMIT dataChanged() (applyUpdates changed file)" - << "row:" << row - << "thread:" << QThread::currentThreadId() - << "model:" << this - << "profile:" << getActiveProfileForModel(this); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } } @@ -1029,42 +959,6 @@ void ResourceFolderModel::applyUpdates(QSet& currentSet, QSet& idx++; } } - - { - QSet enabledAfter; - for (const auto& res : m_resources) { - if (res->enabled()) { - enabledAfter.insert(res->getOriginalFileName()); - } - } - - QSet keptSet = currentSet; - keptSet.intersect(newSet); - QSet removedSet = currentSet; - removedSet.subtract(newSet); - QSet addedSet = newSet; - addedSet.subtract(currentSet); - - qDebug() << "[INSTRUMENTATION-APPLYUPDATES] END" - << "seq:" << ++g_logSequence - << "profile:" << getActiveProfileForModel(this) - << "enabledAfter:" << enabledAfter - << "added:" << addedSet - << "removed:" << removedSet; - for (const auto& kept : keptSet) { - auto rowIt = m_resourcesIndex.constFind(kept); - if (rowIt != m_resourcesIndex.constEnd()) { - auto row = rowIt.value(); - auto& newResource = newResources[kept]; - const auto& currentResource = m_resources.at(row); - if (newResource->enabled() != currentResource->enabled()) { - qDebug() << " -> kept resource changed enabled state:" << kept - << "before:" << currentResource->enabled() - << "after:" << newResource->enabled(); - } - } - } - } } Resource::Ptr ResourceFolderModel::find(QString id) { diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 348ac4a4b..072f16adc 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -73,66 +73,9 @@ #include "tasks/Task.h" #include "ui/dialogs/ProgressDialog.h" -#include -#include -#include -#include - -QMutex g_activeProfilesMutex; -QMap g_activeProfiles; -QAtomicInt g_logSequence(0); - -QString getActiveProfileForModel(void* modelPtr) { - QMutexLocker locker(&g_activeProfilesMutex); - return g_activeProfiles.value(modelPtr, QStringLiteral("Unknown/None")); -} - -void setActiveProfileForModel(void* modelPtr, const QString& profile) { - QMutexLocker locker(&g_activeProfilesMutex); - g_activeProfiles[modelPtr] = profile; -} - -void compareModelToProfileState(const QString& stage, ModFolderModel* model, const QSet& expectedMods, int seq) { - QSet actualMods; - for (int i = 0; i < model->rowCount(); ++i) { - const Resource& res = model->at(i); - if (res.enabled()) { - actualMods.insert(res.getOriginalFileName()); - } - } - - bool match = (expectedMods == actualMods); - qDebug() << "[INSTRUMENTATION-DIVERGE]" << seq << stage - << "expectedCount:" << expectedMods.size() - << "actualCount:" << actualMods.size() - << "match:" << match; - - if (!match) { - QSet missing = expectedMods; - missing.subtract(actualMods); - QSet unexpected = actualMods; - unexpected.subtract(expectedMods); - - qDebug() << " -> Expected Set:" << expectedMods; - qDebug() << " -> Actual Set:" << actualMods; - qDebug() << " -> Missing mods:" << missing; - qDebug() << " -> Unexpected enabled mods:" << unexpected; - } -} - ModFolderPage::~ModFolderPage() { m_destructorStarted = true; - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "ModFolderPage::~ModFolderPage() entered" - << "thread:" << QThread::currentThreadId() - << "model:" << m_model - << "currentProfile:" << m_currentProfile - << "destructorStarted:" << m_destructorStarted; - - { - QMutexLocker locker(&g_activeProfilesMutex); - g_activeProfiles.remove(m_model); - } if (m_filterWindow) { m_filterWindow->removeEventFilter(this); @@ -142,7 +85,6 @@ ModFolderPage::~ModFolderPage() } } - ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent) : ExternalResourcesPage(inst, model, parent), m_model(model) { @@ -150,7 +92,7 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* m_profileTabBar->setExpanding(false); m_profileTabBar->setDrawBase(false); // no underline bar — matches Settings tab style m_profileTabBar->setDocumentMode(false); - m_profileTabBar->setUsesScrollButtons(true); + m_profileTabBar->setUsesScrollButtons(m_profileTabBar->count() > 1); m_profileTabBar->setElideMode(Qt::ElideRight); m_profileTabBar->setContextMenuPolicy(Qt::CustomContextMenu); // Palette-based stylesheet: selected tab uses palette(base) to match the @@ -258,23 +200,14 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* m_profileTabBar->blockSignals(true); for (const QString& name : profileList) { m_profileTabBar->addTab(name); - + QString key = profileKey(name); m_instance->settings()->getOrRegisterSetting(key, QStringList()); QStringList saved = m_instance->settings()->get(key).toStringList(); m_profileStates[name] = QSet(saved.begin(), saved.end()); - qDebug().noquote().nospace() - << "[PROFILE_STATE_WRITE]\n" - << "reason = constructor\n" - << "profile = " << name << "\n" - << "count = " << m_profileStates[name].size(); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "m_profileStates write (constructor)" - << "profile:" << name - << "key:" << key - << "modsCount:" << m_profileStates[name].size(); } m_profileTabBar->blockSignals(false); + m_profileTabBar->setUsesScrollButtons(m_profileTabBar->count() > 1); m_instance->settings()->getOrRegisterSetting(lastActiveIndexKey(), 0); int savedIndex = m_instance->settings()->get(lastActiveIndexKey()).toInt(); @@ -286,10 +219,6 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* connect(m_model, &QAbstractItemModel::dataChanged, this, [this]() { if (!m_applyingProfile) { - qDebug().noquote().nospace() - << "[SAVE_TRIGGER]\n" - << "source = dataChanged\n" - << "currentProfile = " << m_currentProfile; saveCurrentProfileState(); } }); @@ -672,20 +601,7 @@ inline bool ModFolderPage::handleNoModLoader() void ModFolderPage::saveCurrentProfileState() { - if (m_profileLoading) { - return; - } - - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "saveCurrentProfileState() START" - << "thread:" << QThread::currentThreadId() - << "model:" << m_model - << "profile:" << m_currentProfile - << "destructorStarted:" << m_destructorStarted; - if (m_currentProfile.isEmpty()) { - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "saveCurrentProfileState() END (empty profile)" - << "thread:" << QThread::currentThreadId() - << "model:" << m_model; return; } bool stillExists = false; @@ -696,10 +612,6 @@ void ModFolderPage::saveCurrentProfileState() } } if (!stillExists) { - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "saveCurrentProfileState() END (profile no longer exists)" - << "thread:" << QThread::currentThreadId() - << "model:" << m_model - << "profile:" << m_currentProfile; return; } QSet enabledMods; @@ -710,41 +622,13 @@ void ModFolderPage::saveCurrentProfileState() } } m_profileStates[m_currentProfile] = enabledMods; - qDebug().noquote().nospace() - << "[PROFILE_STATE_WRITE]\n" - << "reason = saveCurrentProfileState\n" - << "profile = " << m_currentProfile << "\n" - << "count = " << m_profileStates[m_currentProfile].size(); QString key = profileKey(m_currentProfile); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "m_profileStates write (saveCurrentProfileState)" - << "profile:" << m_currentProfile - << "key:" << key - << "modsCount:" << enabledMods.size(); m_instance->settings()->getOrRegisterSetting(key, QStringList()); m_instance->settings()->set(key, QStringList(enabledMods.begin(), enabledMods.end())); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "settings()->set write (saveCurrentProfileState)" - << "profile:" << m_currentProfile - << "key:" << key - << "modsCount:" << enabledMods.size(); - - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "saveCurrentProfileState() END (saved)" - << "thread:" << QThread::currentThreadId() - << "model:" << m_model - << "profile:" << m_currentProfile - << "savedModsCount:" << enabledMods.size(); } void ModFolderPage::applyProfileSwitch(int index, int generation) { - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "onProfileTabChanged() START" - << "index:" << index - << "thread:" << QThread::currentThreadId() - << "model:" << m_model - << "profileBefore:" << m_currentProfile - << "destructorStarted:" << m_destructorStarted; - if (m_currentProfile.isEmpty()) { QVariant val = m_profileTabBar->property("currentProfileName"); m_currentProfile = val.isValid() ? val.toString() : QString(); @@ -752,40 +636,21 @@ void ModFolderPage::applyProfileSwitch(int index, int generation) { saveCurrentProfileState(); - m_profileLoading = true; - if (index >= 0 && index < m_profileTabBar->count()) { QString tabName = m_profileTabBar->tabText(index); m_currentProfile = tabName; - setActiveProfileForModel(m_model, m_currentProfile); m_profileTabBar->setProperty("currentProfileName", tabName); m_instance->settings()->set(lastActiveIndexKey(), index); QSet enabledMods; if (m_profileStates.contains(tabName)) { enabledMods = m_profileStates[tabName]; - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "profile load (onProfileTabChanged): cached state read" - << "thread:" << QThread::currentThreadId() - << "model:" << m_model - << "profile:" << tabName - << "modsCount:" << enabledMods.size(); } else { QString key = profileKey(tabName); m_instance->settings()->getOrRegisterSetting(key, QStringList()); QStringList saved = m_instance->settings()->get(key).toStringList(); enabledMods = QSet(saved.begin(), saved.end()); m_profileStates[tabName] = enabledMods; - qDebug().noquote().nospace() - << "[PROFILE_STATE_WRITE]\n" - << "reason = onProfileTabChanged\n" - << "profile = " << tabName << "\n" - << "count = " << m_profileStates[tabName].size(); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "m_profileStates write (onProfileTabChanged - settings read)" - << "profile:" << tabName - << "key:" << key - << "modsCount:" << enabledMods.size(); } QModelIndexList toEnable; @@ -803,6 +668,7 @@ void ModFolderPage::applyProfileSwitch(int index, int generation) { } } } + m_applyingProfile = true; if (!toEnable.isEmpty()) { m_model->setResourceEnabled(toEnable, EnableAction::ENABLE); } @@ -810,12 +676,9 @@ void ModFolderPage::applyProfileSwitch(int index, int generation) { m_model->setResourceEnabled(toDisable, EnableAction::DISABLE); } - compareModelToProfileState("BEFORE update()", m_model, enabledMods, ++g_logSequence); - // Capture generation token. If tab changes again before this update // completes, the token will be stale and we discard the result. int capturedGeneration = generation; - m_applyingProfile = true; connect(m_model, &ResourceFolderModel::updateFinished, this, [this, capturedGeneration] { if (capturedGeneration == m_profileSwitchGeneration) { @@ -827,33 +690,11 @@ void ModFolderPage::applyProfileSwitch(int index, int generation) { m_model->update(); - - qDebug().noquote().nospace() - << "[ProfileSwitch]\n" - << "Profile: " << tabName << "\n" - << "Enable: " << toEnable.size() << "\n" - << "Disable: " << toDisable.size() << "\n" - << "updateStarted: " << (started ? "true" : "false"); - compareModelToProfileState("AFTER update()", m_model, enabledMods, ++g_logSequence); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "EMIT m_model->dataChanged() (onProfileTabChanged layout update)" - << "thread:" << QThread::currentThreadId() - << "model:" << m_model - << "profile:" << m_currentProfile; - emit m_model->dataChanged(m_model->index(0, 0), m_model->index(m_model->rowCount() - 1, m_model->columnCount(QModelIndex()) - 1)); } else { m_currentProfile = QString(); - setActiveProfileForModel(m_model, QStringLiteral("None")); m_profileTabBar->setProperty("currentProfileName", QString()); - m_profileLoading = false; m_applyingProfile = false; } - - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence << "onProfileTabChanged() END" - << "index:" << index - << "thread:" << QThread::currentThreadId() - << "model:" << m_model - << "profileAfter:" << m_currentProfile; } @@ -893,25 +734,10 @@ void ModFolderPage::createProfile(const QString& name, const QSet& init // Persist state to memory and settings m_profileStates[name] = initialState; - qDebug().noquote().nospace() - << "[PROFILE_STATE_WRITE]\n" - << "reason = createProfile\n" - << "profile = " << name << "\n" - << "count = " << m_profileStates[name].size(); QString key = profileKey(name); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "m_profileStates write (createProfile)" - << "profile:" << name - << "key:" << key - << "modsCount:" << initialState.size(); m_instance->settings()->getOrRegisterSetting(key, QStringList()); m_instance->settings()->set(key, QStringList(initialState.begin(), initialState.end())); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "settings()->set write (createProfile)" - << "profile:" << name - << "key:" << key - << "modsCount:" << initialState.size(); // Insert tab at the correct position if (insertAfterIndex >= 0 && insertAfterIndex < m_profileTabBar->count()) { @@ -919,6 +745,7 @@ void ModFolderPage::createProfile(const QString& name, const QSet& init } else { m_profileTabBar->addTab(name); } + m_profileTabBar->setUsesScrollButtons(m_profileTabBar->count() > 1); saveProfileList(); } @@ -1007,16 +834,6 @@ void ModFolderPage::onTabDuplicate(int sourceIndex) } } m_profileStates[m_currentProfile] = enabledMods; - qDebug().noquote().nospace() - << "[PROFILE_STATE_WRITE]\n" - << "reason = onTabDuplicate\n" - << "profile = " << m_currentProfile << "\n" - << "count = " << m_profileStates[m_currentProfile].size(); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "m_profileStates write (onTabDuplicate)" - << "profile:" << m_currentProfile - << "key:" << profileKey(m_currentProfile) - << "modsCount:" << enabledMods.size(); } QString sourceName = m_profileTabBar->tabText(sourceIndex); @@ -1056,16 +873,6 @@ void ModFolderPage::onTabRename(int tabIndex) // Migrate in-memory state to the new key if (m_profileStates.contains(oldName)) { m_profileStates[newName] = m_profileStates.take(oldName); - qDebug().noquote().nospace() - << "[PROFILE_STATE_WRITE]\n" - << "reason = onTabRename\n" - << "profile = " << newName << "\n" - << "count = " << m_profileStates[newName].size(); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "m_profileStates write (onTabRename)" - << "profile:" << newName - << "key:" << profileKey(newName) - << "modsCount:" << m_profileStates[newName].size(); } // Migrate settings: read → reset old key → write under new key @@ -1075,11 +882,6 @@ void ModFolderPage::onTabRename(int tabIndex) m_instance->settings()->reset(oldKey); m_instance->settings()->getOrRegisterSetting(newKey, QStringList()); m_instance->settings()->set(newKey, stateData); - qDebug() << "[INSTRUMENTATION]" << ++g_logSequence - << "settings()->set write (onTabRename)" - << "profile:" << newName - << "key:" << newKey - << "modsCount:" << stateData.size(); // Update the visible tab label m_profileTabBar->setTabText(tabIndex, newName); @@ -1109,14 +911,11 @@ void ModFolderPage::onTabRemove(int tabIndex) if (response != QMessageBox::Yes) return; m_profileStates.remove(name); - qDebug().noquote().nospace() - << "[PROFILE_STATE_WRITE]\n" - << "reason = onTabRemove\n" - << "profile = " << name << "\n" - << "count = 0"; + m_instance->settings()->reset(profileKey(name)); m_profileTabBar->removeTab(tabIndex); + m_profileTabBar->setUsesScrollButtons(m_profileTabBar->count() > 1); saveProfileList(); } @@ -1124,9 +923,9 @@ void ModFolderPage::onTabEnableAll(int tabIndex) { if (m_profileTabBar->currentIndex() != tabIndex) { m_profileTabBar->setCurrentIndex(tabIndex); - } else { - applyProfileSwitch(tabIndex, m_profileSwitchGeneration); } + applyProfileSwitch(tabIndex, m_profileSwitchGeneration); + // Build the full index list and enable every mod in the active profile. QModelIndexList allIndices; allIndices.reserve(m_model->rowCount()); @@ -1143,9 +942,9 @@ void ModFolderPage::onTabDisableAll(int tabIndex) { if (m_profileTabBar->currentIndex() != tabIndex) { m_profileTabBar->setCurrentIndex(tabIndex); - } else { - applyProfileSwitch(tabIndex, m_profileSwitchGeneration); } + applyProfileSwitch(tabIndex, m_profileSwitchGeneration); + // Build the full index list and disable every mod in the active profile. QModelIndexList allIndices; allIndices.reserve(m_model->rowCount()); diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index a32bfc192..f653e5ef8 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -120,7 +120,6 @@ class ModFolderPage : public ExternalResourcesPage { QString m_currentProfile; QString m_settingsPrefix; bool m_destructorStarted = false; - bool m_profileLoading = false; int m_profileSwitchGeneration = 0; bool m_applyingProfile = false; };