This commit is contained in:
Vivek Kushwaha 2026-06-26 18:52:18 +05:00 committed by GitHub
commit 6d6a015ca1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 691 additions and 45 deletions

View file

@ -40,24 +40,31 @@
#include "minecraft/MinecraftInstance.h"
#include "minecraft/mod/ModFolderModel.h"
ScanModFolders::ScanModFolders(LaunchTask* parent) : LaunchStep(parent) {}
ScanModFolders::~ScanModFolders()
{
}
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();
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 +92,15 @@ void ScanModFolders::nilModsDone()
void ScanModFolders::checkDone()
{
if (m_modsDone && m_coreModsDone && m_nilModsDone) {
// 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();
}
}

View file

@ -15,14 +15,15 @@
#pragma once
#include <QMetaObject>
#include <launch/LaunchStep.h>
#include <memory>
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;
};

View file

@ -46,9 +46,15 @@
#include <QAction>
#include <QEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QHideEvent>
#include <QMenu>
#include <QShowEvent>
#include <QMessageBox>
#include <QSortFilterProxyModel>
#include <QInputDialog>
#include <QBoxLayout>
#include <QGridLayout>
#include <algorithm>
#include <memory>
@ -67,9 +73,83 @@
#include "tasks/Task.h"
#include "ui/dialogs/ProgressDialog.h"
ModFolderPage::~ModFolderPage()
{
m_destructorStarted = true;
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(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
// 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();
if (auto grid = qobject_cast<QGridLayout*>(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, [this](int index) {
++m_profileSwitchGeneration;
applyProfileSwitch(index, m_profileSwitchGeneration);
});
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"));
ui->actionDownloadItem->setEnabled(true);
@ -110,6 +190,38 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget*
ui->actionsToolbar->insertActionAfter(ui->actionViewHomepage, ui->actionExportMetadata);
ui->actionsToolbar->insertActionAfter(ui->actionViewFolder, ui->actionViewConfigs);
// 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 = profileKey(name);
m_instance->settings()->getOrRegisterSetting(key, QStringList());
QStringList saved = m_instance->settings()->get(key).toStringList();
m_profileStates[name] = QSet<QString>(saved.begin(), saved.end());
}
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();
if (savedIndex < 0 || savedIndex >= m_profileTabBar->count()) {
savedIndex = 0;
}
m_profileTabBar->setCurrentIndex(savedIndex);
applyProfileSwitch(m_profileTabBar->currentIndex(), m_profileSwitchGeneration);
connect(m_model, &QAbstractItemModel::dataChanged, this, [this]() {
if (!m_applyingProfile) {
saveCurrentProfileState();
}
});
}
bool ModFolderPage::shouldDisplay() const
@ -182,17 +294,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<bool>(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();
@ -201,8 +334,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 {
@ -215,9 +348,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)
@ -279,15 +413,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<bool>(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();
@ -365,7 +512,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(); });
@ -373,6 +525,7 @@ void ModFolderPage::exportModMetadata()
dlg.exec();
}
CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, ModFolderModel* mods, QWidget* parent) : ModFolderPage(inst, mods, parent)
{
auto* mcInst = dynamic_cast<MinecraftInstance*>(m_instance);
@ -382,16 +535,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();
}
}
}
@ -445,3 +598,429 @@ 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::saveCurrentProfileState()
{
if (m_currentProfile.isEmpty()) {
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) {
return;
}
QSet<QString> 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 = profileKey(m_currentProfile);
m_instance->settings()->getOrRegisterSetting(key, QStringList());
m_instance->settings()->set(key, QStringList(enabledMods.begin(), enabledMods.end()));
}
void ModFolderPage::applyProfileSwitch(int index, int generation) {
if (m_currentProfile.isEmpty()) {
QVariant val = m_profileTabBar->property("currentProfileName");
m_currentProfile = val.isValid() ? val.toString() : QString();
}
saveCurrentProfileState();
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(lastActiveIndexKey(), index);
QSet<QString> enabledMods;
if (m_profileStates.contains(tabName)) {
enabledMods = m_profileStates[tabName];
} else {
QString key = profileKey(tabName);
m_instance->settings()->getOrRegisterSetting(key, QStringList());
QStringList saved = m_instance->settings()->get(key).toStringList();
enabledMods = QSet<QString>(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);
}
}
}
m_applyingProfile = true;
if (!toEnable.isEmpty()) {
m_model->setResourceEnabled(toEnable, EnableAction::ENABLE);
}
if (!toDisable.isEmpty()) {
m_model->setResourceEnabled(toDisable, EnableAction::DISABLE);
}
// Capture generation token. If tab changes again before this update
// completes, the token will be stale and we discard the result.
int capturedGeneration = generation;
connect(m_model, &ResourceFolderModel::updateFinished, this,
[this, capturedGeneration] {
if (capturedGeneration == m_profileSwitchGeneration) {
saveCurrentProfileState();
}
m_applyingProfile = false;
},
Qt::SingleShotConnection);
m_model->update();
} else {
m_currentProfile = QString();
m_profileTabBar->setProperty("currentProfileName", QString());
m_applyingProfile = false;
}
}
void ModFolderPage::onAddProfileClicked() {
bool ok;
QString name = QInputDialog::getText(this, tr("Add Profile"), tr("Profile Name:"),
QLineEdit::Normal, QString(), &ok);
if (ok && !name.isEmpty()) {
// Always create a blank-slate profile — no state bleeding from the current profile.
createProfile(name, QSet<QString>{});
}
}
void ModFolderPage::onRemoveProfileClicked() {
int index = m_profileTabBar->currentIndex();
// 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<QString>& 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;
QString key = profileKey(name);
m_instance->settings()->getOrRegisterSetting(key, QStringList());
m_instance->settings()->set(key, QStringList(initialState.begin(), initialState.end()));
// Insert tab at the correct position
if (insertAfterIndex >= 0 && insertAfterIndex < m_profileTabBar->count()) {
m_profileTabBar->insertTab(insertAfterIndex + 1, name);
} else {
m_profileTabBar->addTab(name);
}
m_profileTabBar->setUsesScrollButtons(m_profileTabBar->count() > 1);
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<QString>{}, sourceIndex);
}
}
void ModFolderPage::onTabDuplicate(int sourceIndex)
{
// Flush the live state of the currently active profile before reading source.
if (!m_currentProfile.isEmpty()) {
QSet<QString> 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 sourceName = m_profileTabBar->tabText(sourceIndex);
QSet<QString> 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);
}
// 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);
// 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);
m_instance->settings()->reset(profileKey(name));
m_profileTabBar->removeTab(tabIndex);
m_profileTabBar->setUsesScrollButtons(m_profileTabBar->count() > 1);
saveProfileList();
}
void ModFolderPage::onTabEnableAll(int tabIndex)
{
if (m_profileTabBar->currentIndex() != tabIndex) {
m_profileTabBar->setCurrentIndex(tabIndex);
}
applyProfileSwitch(tabIndex, m_profileSwitchGeneration);
// 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);
}
applyProfileSwitch(tabIndex, m_profileSwitchGeneration);
// 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<QWidget*>(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<QMouseEvent*>(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);
}

View file

@ -39,43 +39,89 @@
#pragma once
#include <QPointer>
#include <QMap>
#include <QSet>
#include <QTabBar>
#include <QToolButton>
#include "ExternalResourcesPage.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
class ModFolderPage : public ExternalResourcesPage {
Q_OBJECT
inline bool handleNoModLoader();
inline bool handleNoModLoader();
public:
explicit ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent = nullptr);
virtual ~ModFolderPage() = default;
public:
explicit ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent = nullptr);
virtual ~ModFolderPage();
void setFilter(const QString& filter) { m_fileSelectionFilter = filter; }
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 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;
virtual bool shouldDisplay() const override;
public slots:
void updateFrame(const QModelIndex& current, const QModelIndex& previous) override;
public slots:
void updateFrame(const QModelIndex& current, const QModelIndex& previous) override;
private slots:
void removeItems(const QItemSelection& selection) 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 downloadMods();
void downloadDialogFinished(int result);
void updateMods(bool includeDeps = false);
void deleteModMetadata();
void exportModMetadata();
void changeModVersion();
void onAddProfileClicked();
void onRemoveProfileClicked();
void saveCurrentProfileState();
protected:
ModFolderModel* m_model;
QPointer<ResourceDownload::ModDownloadDialog> m_downloadDialog;
// 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:
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<QString>& 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<ResourceDownload::ModDownloadDialog> m_downloadDialog;
QPointer<QWidget> m_filterWindow;
bool m_downloadFlowActive = false;
QTabBar* m_profileTabBar = nullptr;
QToolButton* m_newTabButton = nullptr; ///< Chrome-style "+" button appended after last tab
QMap<QString, QSet<QString>> m_profileStates;
QString m_currentProfile;
QString m_settingsPrefix;
bool m_destructorStarted = false;
int m_profileSwitchGeneration = 0;
bool m_applyingProfile = false;
};
class CoreModFolderPage : public ModFolderPage {