refactor: Create a more clear hierarchy for some instance pages

Previously, the Shaders, Texture packs and Resource packs tabs had as
parent the ModFolderPage, making it so that making changes only to the
Mods page would require checking the id of the page for the correct one.
This was hackish and error-prone.

Now, those pages all inherit from a single class, ExternalResourcesPage,
that handles the basic behaviour of all of them, while allowing for
individual modification in code.

This is still not a clear separation, since internally, all those
resources are derived from Mods, so for now there's still some awkward
common code :/
This commit is contained in:
flow
2022-03-11 18:03:21 -03:00
committed by flow
parent 349fc4143d
commit d394235ee0
10 changed files with 486 additions and 473 deletions

View File

@ -35,368 +35,95 @@
*/
#include "ModFolderPage.h"
#include "ui_ModFolderPage.h"
#include "ui_ExternalResourcesPage.h"
#include <QMessageBox>
#include <QAbstractItemModel>
#include <QEvent>
#include <QKeyEvent>
#include <QAbstractItemModel>
#include <QMenu>
#include <QMessageBox>
#include <QSortFilterProxyModel>
#include "Application.h"
#include "ui/GuiUtil.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/GuiUtil.h"
#include "DesktopServices.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/Mod.h"
#include "minecraft/VersionFilterData.h"
#include "minecraft/PackProfile.h"
#include "minecraft/VersionFilterData.h"
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModAPI.h"
#include "Version.h"
#include "ui/dialogs/ProgressDialog.h"
#include "tasks/SequentialTask.h"
#include "ui/dialogs/ProgressDialog.h"
namespace {
// FIXME: wasteful
void RemoveThePrefix(QString & string) {
QRegularExpression regex(QStringLiteral("^(([Tt][Hh][eE])|([Tt][eE][Hh])) +"));
string.remove(regex);
string = string.trimmed();
}
}
class ModSortProxy : public QSortFilterProxyModel
ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
: ExternalResourcesPage(inst, mods, parent)
{
public:
explicit ModSortProxy(QObject *parent = 0) : QSortFilterProxyModel(parent)
{
}
protected:
bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override {
ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel());
if(!model) {
return false;
}
const auto &mod = model->at(source_row);
if(mod.name().contains(filterRegExp())) {
return true;
}
if(mod.description().contains(filterRegExp())) {
return true;
}
for(auto & author: mod.authors()) {
if (author.contains(filterRegExp())) {
return true;
}
}
return false;
}
bool lessThan(const QModelIndex & source_left, const QModelIndex & source_right) const override
{
ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel());
if(
!model ||
!source_left.isValid() ||
!source_right.isValid() ||
source_left.column() != source_right.column()
) {
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
// we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and proceed.
auto column = (ModFolderModel::Columns) source_left.column();
bool invert = false;
switch(column) {
// GH-2550 - sort by enabled/disabled
case ModFolderModel::ActiveColumn: {
auto dataL = source_left.data(Qt::CheckStateRole).toBool();
auto dataR = source_right.data(Qt::CheckStateRole).toBool();
if(dataL != dataR) {
return dataL > dataR;
}
// fallthrough
invert = sortOrder() == Qt::DescendingOrder;
}
// GH-2722 - sort mod names in a way that discards "The" prefixes
case ModFolderModel::NameColumn: {
auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString();
RemoveThePrefix(dataL);
auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString();
RemoveThePrefix(dataR);
auto less = dataL.compare(dataR, sortCaseSensitivity());
if(less != 0) {
return invert ? (less > 0) : (less < 0);
}
// fallthrough
invert = sortOrder() == Qt::DescendingOrder;
}
// GH-2762 - sort versions by parsing them as versions
case ModFolderModel::VersionColumn: {
auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString());
auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString());
return invert ? (dataL > dataR) : (dataL < dataR);
}
default: {
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
}
}
};
ModFolderPage::ModFolderPage(
BaseInstance *inst,
std::shared_ptr<ModFolderModel> mods,
QString id,
QString iconName,
QString displayName,
QString helpPage,
QWidget *parent
) :
QMainWindow(parent),
ui(new Ui::ModFolderPage)
{
ui->setupUi(this);
// This is structured like that so that these changes
// do not affect the Resouce pack and Shader pack tabs
if(id == "mods") {
// do not affect the Resource pack and Shader pack tabs
{
auto act = new QAction(tr("Download mods"), this);
act->setToolTip(tr("Download mods from online mod platforms"));
ui->actionsToolbar->insertActionBefore(ui->actionAdd, act);
connect(act, &QAction::triggered, this, &ModFolderPage::on_actionInstall_mods_triggered);
ui->actionsToolbar->insertActionBefore(ui->actionAddItem, act);
connect(act, &QAction::triggered, this, &ModFolderPage::installMods);
ui->actionAdd->setText(tr("Add .jar"));
ui->actionAdd->setToolTip(tr("Add mods via local file"));
ui->actionAddItem->setText(tr("Add .jar"));
ui->actionAddItem->setToolTip(tr("Add mods via local file"));
}
ui->actionsToolbar->insertSpacer(ui->actionView_configs);
m_inst = inst;
on_RunningState_changed(m_inst && m_inst->isRunning());
m_mods = mods;
m_id = id;
m_displayName = displayName;
m_iconName = iconName;
m_helpName = helpPage;
m_fileSelectionFilter = "%1 (*.zip *.jar)";
m_filterModel = new ModSortProxy(this);
m_filterModel->setDynamicSortFilter(true);
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
m_filterModel->setSourceModel(m_mods.get());
m_filterModel->setFilterKeyColumn(-1);
ui->modTreeView->setModel(m_filterModel);
ui->modTreeView->installEventFilter(this);
ui->modTreeView->sortByColumn(1, Qt::AscendingOrder);
ui->modTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->modTreeView, &ModListView::customContextMenuRequested, this, &ModFolderPage::ShowContextMenu);
connect(ui->modTreeView, &ModListView::activated, this, &ModFolderPage::modItemActivated);
auto smodel = ui->modTreeView->selectionModel();
connect(smodel, &QItemSelectionModel::currentChanged, this, &ModFolderPage::modCurrent);
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ModFolderPage::on_filterTextChanged);
connect(m_inst, &BaseInstance::runningStatusChanged, this, &ModFolderPage::on_RunningState_changed);
}
void ModFolderPage::modItemActivated(const QModelIndex&)
{
if(!m_controlsEnabled) {
return;
}
auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
m_mods->setModStatus(selection.indexes(), ModFolderModel::Toggle);
}
QMenu * ModFolderPage::createPopupMenu()
{
QMenu* filteredMenu = QMainWindow::createPopupMenu();
filteredMenu->removeAction(ui->actionsToolbar->toggleViewAction() );
return filteredMenu;
}
void ModFolderPage::ShowContextMenu(const QPoint& pos)
{
auto menu = ui->actionsToolbar->createContextMenu(this, tr("Context menu"));
menu->exec(ui->modTreeView->mapToGlobal(pos));
delete menu;
}
void ModFolderPage::openedImpl()
{
m_mods->startWatching();
}
void ModFolderPage::closedImpl()
{
m_mods->stopWatching();
}
void ModFolderPage::on_filterTextChanged(const QString& newContents)
{
m_viewFilter = newContents;
m_filterModel->setFilterFixedString(m_viewFilter);
}
CoreModFolderPage::CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods,
QString id, QString iconName, QString displayName,
QString helpPage, QWidget *parent)
: ModFolderPage(inst, mods, id, iconName, displayName, helpPage, parent)
{
}
ModFolderPage::~ModFolderPage()
{
m_mods->stopWatching();
delete ui;
}
void ModFolderPage::on_RunningState_changed(bool running)
{
if(m_controlsEnabled == !running) {
return;
}
m_controlsEnabled = !running;
ui->actionsToolbar->setEnabled(m_controlsEnabled);
}
CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
: ModFolderPage(inst, mods, parent)
{}
bool ModFolderPage::shouldDisplay() const
{
return true;
}
void ModFolderPage::retranslate()
{
ui->retranslateUi(this);
}
bool CoreModFolderPage::shouldDisplay() const
{
if (ModFolderPage::shouldDisplay())
{
auto inst = dynamic_cast<MinecraftInstance *>(m_inst);
if (ModFolderPage::shouldDisplay()) {
auto inst = dynamic_cast<MinecraftInstance*>(m_instance);
if (!inst)
return true;
auto version = inst->getPackProfile();
if (!version)
return true;
if(!version->getComponent("net.minecraftforge"))
{
if (!version->getComponent("net.minecraftforge"))
return false;
}
if(!version->getComponent("net.minecraft"))
{
if (!version->getComponent("net.minecraft"))
return false;
}
if(version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
{
if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
return true;
}
}
return false;
}
bool ModFolderPage::modListFilter(QKeyEvent *keyEvent)
void ModFolderPage::installMods()
{
switch (keyEvent->key())
{
case Qt::Key_Delete:
on_actionRemove_triggered();
return true;
case Qt::Key_Plus:
on_actionAdd_triggered();
return true;
default:
break;
}
return QWidget::eventFilter(ui->modTreeView, keyEvent);
}
bool ModFolderPage::eventFilter(QObject *obj, QEvent *ev)
{
if (ev->type() != QEvent::KeyPress)
{
return QWidget::eventFilter(obj, ev);
}
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev);
if (obj == ui->modTreeView)
return modListFilter(keyEvent);
return QWidget::eventFilter(obj, ev);
}
void ModFolderPage::on_actionAdd_triggered()
{
if(!m_controlsEnabled) {
if (!m_controlsEnabled)
return;
}
auto list = GuiUtil::BrowseForFiles(
m_helpName,
tr("Select %1",
"Select whatever type of files the page contains. Example: 'Loader Mods'")
.arg(m_displayName),
m_fileSelectionFilter.arg(m_displayName), APPLICATION->settings()->get("CentralModsDir").toString(),
this->parentWidget());
if (!list.empty())
{
for (auto filename : list)
{
m_mods->installMod(filename);
}
}
}
void ModFolderPage::on_actionEnable_triggered()
{
if(!m_controlsEnabled) {
return;
}
auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
m_mods->setModStatus(selection.indexes(), ModFolderModel::Enable);
}
void ModFolderPage::on_actionDisable_triggered()
{
if(!m_controlsEnabled) {
return;
}
auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
m_mods->setModStatus(selection.indexes(), ModFolderModel::Disable);
}
void ModFolderPage::on_actionRemove_triggered()
{
if(!m_controlsEnabled) {
return;
}
auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
m_mods->deleteMods(selection.indexes());
}
void ModFolderPage::on_actionInstall_mods_triggered()
{
if(!m_controlsEnabled) {
return;
}
if(m_inst->typeName() != "Minecraft"){
return; //this is a null instance or a legacy instance
}
auto profile = ((MinecraftInstance *)m_inst)->getPackProfile();
if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance
auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile();
if (profile->getModLoaders() == ModAPI::Unspecified) {
QMessageBox::critical(this,tr("Error"),tr("Please install a mod loader first!"));
QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
return;
}
ModDownloadDialog mdownload(m_mods, this, m_inst);
ModDownloadDialog mdownload(m_model, this, m_instance);
if (mdownload.exec()) {
SequentialTask* tasks = new SequentialTask(this);
connect(tasks, &Task::failed, [this, tasks](QString reason) {
@ -409,40 +136,20 @@ void ModFolderPage::on_actionInstall_mods_triggered()
});
connect(tasks, &Task::succeeded, [this, tasks]() {
QStringList warnings = tasks->warnings();
if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); }
if (warnings.count())
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
tasks->deleteLater();
});
for (auto task : mdownload.getTasks()) {
for (auto& task : mdownload.getTasks()) {
tasks->addTask(task);
}
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(tasks);
m_mods->update();
m_model->update();
}
}
void ModFolderPage::on_actionView_configs_triggered()
{
DesktopServices::openDirectory(m_inst->instanceConfigFolder(), true);
}
void ModFolderPage::on_actionView_Folder_triggered()
{
DesktopServices::openDirectory(m_mods->dir().absolutePath(), true);
}
void ModFolderPage::modCurrent(const QModelIndex &current, const QModelIndex &previous)
{
if (!current.isValid())
{
ui->frame->clear();
return;
}
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
Mod &m = m_mods->operator[](row);
ui->frame->updateWithMod(m);
}