From abcfc06872e873f0c642cb32ad321f3af34c7a3e Mon Sep 17 00:00:00 2001 From: Vivek Kushwaha Date: Thu, 18 Jun 2026 18:03:40 +0530 Subject: [PATCH] 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 {