diff options
Diffstat (limited to 'launcher/ui')
47 files changed, 1915 insertions, 228 deletions
diff --git a/launcher/ui/InstanceWindow.cpp b/launcher/ui/InstanceWindow.cpp index 09ce0d67..c62b370f 100644 --- a/launcher/ui/InstanceWindow.cpp +++ b/launcher/ui/InstanceWindow.cpp @@ -132,6 +132,12 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget *parent) { connect(m_instance.get(), &BaseInstance::statusChanged, this, &InstanceWindow::on_instanceStatusChanged); } + + // add ourself as the modpack page's instance window + { + static_cast<ManagedPackPage*>(m_container->getPage("managed_pack"))->setInstanceWindow(this); + } + show(); } diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 85b00b67..cc81d8e3 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -39,6 +39,7 @@ #include "Application.h" #include "BuildConfig.h" +#include "FileSystem.h" #include "MainWindow.h" @@ -49,7 +50,7 @@ #include <QKeyEvent> #include <QAction> - +#include <QActionGroup> #include <QApplication> #include <QButtonGroup> #include <QHBoxLayout> @@ -61,6 +62,7 @@ #include <QMenu> #include <QMenuBar> #include <QMessageBox> +#include <QFileDialog> #include <QInputDialog> #include <QLabel> #include <QToolButton> @@ -70,6 +72,7 @@ #include <BaseInstance.h> #include <InstanceList.h> +#include <minecraft/MinecraftInstance.h> #include <MMCZip.h> #include <icons/IconList.h> #include <java/JavaUtils.h> @@ -105,6 +108,13 @@ #include "ui/dialogs/UpdateDialog.h" #include "ui/dialogs/EditAccountDialog.h" #include "ui/dialogs/ExportInstanceDialog.h" +#include "ui/dialogs/ImportResourcePackDialog.h" +#include "ui/themes/ITheme.h" + +#include <minecraft/mod/ResourcePackFolderModel.h> +#include <minecraft/mod/tasks/LocalResourcePackParseTask.h> +#include <minecraft/mod/TexturePackFolderModel.h> +#include <minecraft/mod/tasks/LocalTexturePackParseTask.h> #include "UpdateController.h" #include "KonamiCode.h" @@ -237,6 +247,7 @@ public: TranslatedAction actionLaunchInstanceOffline; TranslatedAction actionLaunchInstanceDemo; TranslatedAction actionExportInstance; + TranslatedAction actionCreateInstanceShortcut; QVector<TranslatedAction *> all_actions; LabeledToolButton *renameButton = nullptr; @@ -253,6 +264,9 @@ public: QMenu * helpMenu = nullptr; TranslatedToolButton helpMenuButton; TranslatedAction actionClearMetadata; + #ifdef Q_OS_MAC + TranslatedAction actionAddToPATH; + #endif TranslatedAction actionReportBug; TranslatedAction actionDISCORD; TranslatedAction actionMATRIX; @@ -264,6 +278,8 @@ public: TranslatedAction actionLockToolbars; + TranslatedAction actionChangeTheme; + QVector<TranslatedToolButton *> all_toolbuttons; QWidget *centralWidget = nullptr; @@ -290,7 +306,6 @@ public: actionAddInstance = TranslatedAction(MainWindow); actionAddInstance->setObjectName(QStringLiteral("actionAddInstance")); actionAddInstance->setIcon(APPLICATION->getThemedIcon("new")); - actionAddInstance->setIconVisibleInMenu(false); actionAddInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Add Instanc&e...")); actionAddInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Add a new instance.")); actionAddInstance->setShortcut(QKeySequence::New); @@ -350,6 +365,14 @@ public: actionClearMetadata.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Clear cached metadata")); all_actions.append(&actionClearMetadata); + #ifdef Q_OS_MAC + actionAddToPATH = TranslatedAction(MainWindow); + actionAddToPATH->setObjectName(QStringLiteral("actionAddToPATH")); + actionAddToPATH.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Install to &PATH")); + actionAddToPATH.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Install a prismlauncher symlink to /usr/local/bin")); + all_actions.append(&actionAddToPATH); + #endif + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { actionReportBug = TranslatedAction(MainWindow); actionReportBug->setObjectName(QStringLiteral("actionReportBug")); @@ -428,6 +451,11 @@ public: actionLockToolbars.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Lock Toolbars")); actionLockToolbars->setCheckable(true); all_actions.append(&actionLockToolbars); + + actionChangeTheme = TranslatedAction(MainWindow); + actionChangeTheme->setObjectName(QStringLiteral("actionChangeTheme")); + actionChangeTheme.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Themes")); + all_actions.append(&actionChangeTheme); } void createMainToolbar(QMainWindow *MainWindow) @@ -455,6 +483,10 @@ public: helpMenu->addAction(actionClearMetadata); + #ifdef Q_OS_MAC + helpMenu->addAction(actionAddToPATH); + #endif + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { helpMenu->addAction(actionReportBug); } @@ -509,8 +541,6 @@ public: fileMenu->setSeparatorsCollapsible(false); fileMenu->addAction(actionAddInstance); fileMenu->addAction(actionLaunchInstance); - fileMenu->addAction(actionLaunchInstanceOffline); - fileMenu->addAction(actionLaunchInstanceDemo); fileMenu->addAction(actionKillInstance); fileMenu->addAction(actionCloseWindow); fileMenu->addSeparator(); @@ -518,8 +548,9 @@ public: fileMenu->addAction(actionChangeInstGroup); fileMenu->addAction(actionViewSelectedInstFolder); fileMenu->addAction(actionExportInstance); - fileMenu->addAction(actionDeleteInstance); fileMenu->addAction(actionCopyInstance); + fileMenu->addAction(actionDeleteInstance); + fileMenu->addAction(actionCreateInstanceShortcut); fileMenu->addSeparator(); fileMenu->addAction(actionSettings); @@ -528,6 +559,8 @@ public: viewMenu = menuBar->addMenu(tr("&View")); viewMenu->setSeparatorsCollapsible(false); + viewMenu->addAction(actionChangeTheme); + viewMenu->addSeparator(); viewMenu->addAction(actionCAT); viewMenu->addSeparator(); @@ -542,6 +575,9 @@ public: helpMenu = menuBar->addMenu(tr("&Help")); helpMenu->setSeparatorsCollapsible(false); helpMenu->addAction(actionClearMetadata); + #ifdef Q_OS_MAC + helpMenu->addAction(actionAddToPATH); + #endif helpMenu->addSeparator(); helpMenu->addAction(actionAbout); helpMenu->addAction(actionOpenWiki); @@ -555,10 +591,11 @@ public: helpMenu->addAction(actionDISCORD); if (!BuildConfig.SUBREDDIT_URL.isEmpty()) helpMenu->addAction(actionREDDIT); - helpMenu->addSeparator(); if(BuildConfig.UPDATER_ENABLED) + { + helpMenu->addSeparator(); helpMenu->addAction(actionCheckUpdate); - + } MainWindow->setMenuBar(menuBar); } @@ -576,6 +613,7 @@ public: actionOpenWiki->setObjectName(QStringLiteral("actionOpenWiki")); actionOpenWiki.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &Help")); actionOpenWiki.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); + actionOpenWiki->setIcon(APPLICATION->getThemedIcon("help")); connect(actionOpenWiki, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); all_actions.append(&actionOpenWiki); @@ -583,6 +621,7 @@ public: actionNewsMenuBar->setObjectName(QStringLiteral("actionNewsMenuBar")); actionNewsMenuBar.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &News")); actionNewsMenuBar.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); + actionNewsMenuBar->setIcon(APPLICATION->getThemedIcon("news")); connect(actionNewsMenuBar, &QAction::triggered, MainWindow, &MainWindow::on_actionMoreNews_triggered); all_actions.append(&actionNewsMenuBar); } @@ -597,6 +636,7 @@ public: actionExportInstance->setEnabled(enabled); actionDeleteInstance->setEnabled(enabled); actionCopyInstance->setEnabled(enabled); + actionCreateInstanceShortcut->setEnabled(enabled); } void createStatusBar(QMainWindow *MainWindow) @@ -735,6 +775,15 @@ public: actionCopyInstance->setIcon(APPLICATION->getThemedIcon("copy")); all_actions.append(&actionCopyInstance); + actionCreateInstanceShortcut = TranslatedAction(MainWindow); + actionCreateInstanceShortcut->setObjectName(QStringLiteral("actionCreateInstanceShortcut")); + actionCreateInstanceShortcut.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Create Shortcut")); + actionCreateInstanceShortcut.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Creates a shortcut on your desktop to launch the selected instance.")); + //actionCreateInstanceShortcut->setShortcut(QKeySequence(tr("Ctrl+D"))); // TODO + // FIXME missing on Legacy, Flat and Flat (White) + actionCreateInstanceShortcut->setIcon(APPLICATION->getThemedIcon("shortcut")); + all_actions.append(&actionCreateInstanceShortcut); + setInstanceActionsEnabled(false); } @@ -773,6 +822,8 @@ public: instanceToolBar->addAction(actionCopyInstance); instanceToolBar->addAction(actionDeleteInstance); + instanceToolBar->addAction(actionCreateInstanceShortcut); // TODO find better position for this + QLayout * lay = instanceToolBar->layout(); for(int i = 0; i < lay->count(); i++) { @@ -822,6 +873,7 @@ public: createInstanceToolbar(MainWindow); MainWindow->updateToolsMenu(); + MainWindow->updateThemeMenu(); retranslateUi(MainWindow); @@ -1271,6 +1323,38 @@ void MainWindow::updateToolsMenu() ui->actionLaunchInstance->setMenu(launchMenu); } +void MainWindow::updateThemeMenu() +{ + QMenu *themeMenu = ui->actionChangeTheme->menu(); + + if (themeMenu) { + themeMenu->clear(); + } else { + themeMenu = new QMenu(this); + } + + auto themes = APPLICATION->getValidApplicationThemes(); + + QActionGroup* themesGroup = new QActionGroup( this ); + + for (auto* theme : themes) { + QAction * themeAction = themeMenu->addAction(theme->name()); + + themeAction->setCheckable(true); + if (APPLICATION->settings()->get("ApplicationTheme").toString() == theme->id()) { + themeAction->setChecked(true); + } + themeAction->setActionGroup(themesGroup); + + connect(themeAction, &QAction::triggered, [theme]() { + APPLICATION->setApplicationTheme(theme->id(),false); + APPLICATION->settings()->set("ApplicationTheme", theme->id()); + }); + } + + ui->actionChangeTheme->setMenu(themeMenu); +} + void MainWindow::repopulateAccountsMenu() { accountMenu->clear(); @@ -1732,17 +1816,41 @@ void MainWindow::on_actionAddInstance_triggered() void MainWindow::droppedURLs(QList<QUrl> urls) { - for(auto & url:urls) - { - if(url.isLocalFile()) - { - addInstance(url.toLocalFile()); - } - else - { + // NOTE: This loop only processes one dropped file! + for (auto& url : urls) { + // The isLocalFile() check below doesn't work as intended without an explicit scheme. + if (url.scheme().isEmpty()) + url.setScheme("file"); + + if (!url.isLocalFile()) { // probably instance/modpack addInstance(url.toString()); + break; } - // Only process one dropped file... + + auto localFileName = url.toLocalFile(); + QFileInfo localFileInfo(localFileName); + + bool isResourcePack = ResourcePackUtils::validate(localFileInfo); + bool isTexturePack = TexturePackUtils::validate(localFileInfo); + + if (!isResourcePack && !isTexturePack) { // probably instance/modpack + addInstance(localFileName); + break; + } + + ImportResourcePackDialog dlg(this); + + if (dlg.exec() != QDialog::Accepted) + break; + + qDebug() << "Adding resource/texture pack" << localFileName << "to" << dlg.selectedInstanceKey; + + auto inst = APPLICATION->instances()->getInstanceById(dlg.selectedInstanceKey); + auto minecraftInst = std::dynamic_pointer_cast<MinecraftInstance>(inst); + if (isResourcePack) + minecraftInst->resourcePackList()->installResource(localFileName); + else if (isTexturePack) + minecraftInst->texturePackList()->installResource(localFileName); break; } } @@ -1901,6 +2009,7 @@ void MainWindow::globalSettingsClosed() proxymodel->sort(0); updateMainToolBar(); updateToolsMenu(); + updateThemeMenu(); updateStatusCenter(); // This needs to be done to prevent UI elements disappearing in the event the config is changed // but Prism Launcher exits abnormally, causing the window state to never be saved: @@ -1929,6 +2038,29 @@ void MainWindow::on_actionClearMetadata_triggered() APPLICATION->metacache()->SaveNow(); } +#ifdef Q_OS_MAC +void MainWindow::on_actionAddToPATH_triggered() +{ + auto binaryPath = APPLICATION->applicationFilePath(); + auto targetPath = QString("/usr/local/bin/%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); + qDebug() << "Symlinking" << binaryPath << "to" << targetPath; + + QStringList args; + args << "-e"; + args << QString("do shell script \"mkdir -p /usr/local/bin && ln -sf '%1' '%2'\" with administrator privileges") + .arg(binaryPath, targetPath); + auto outcome = QProcess::execute("/usr/bin/osascript", args); + if (!outcome) { + QMessageBox::information(this, tr("Successfully added %1 to PATH").arg(BuildConfig.LAUNCHER_DISPLAYNAME), + tr("%1 was successfully added to your PATH. You can now start it by running `%2`.") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.LAUNCHER_APP_BINARY_NAME)); + } else { + QMessageBox::critical(this, tr("Failed to add %1 to PATH").arg(BuildConfig.LAUNCHER_DISPLAYNAME), + tr("An error occurred while trying to add %1 to PATH").arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + } +} +#endif + void MainWindow::on_actionOpenWiki_triggered() { DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg(""))); @@ -1957,27 +2089,25 @@ void MainWindow::on_actionAbout_triggered() void MainWindow::on_actionDeleteInstance_triggered() { - if (!m_selectedInstance) - { + if (!m_selectedInstance) { return; } auto id = m_selectedInstance->id(); - if (APPLICATION->instances()->trashInstance(id)) { - ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); - return; - } - - auto response = CustomMessageBox::selectable( - this, - tr("CAREFUL!"), - tr("About to delete: %1\nThis is permanent and will completely delete the instance.\n\nAre you sure?").arg(m_selectedInstance->name()), - QMessageBox::Warning, - QMessageBox::Yes | QMessageBox::No, - QMessageBox::No - )->exec(); - if (response == QMessageBox::Yes) - { + + auto response = + CustomMessageBox::selectable(this, tr("CAREFUL!"), + tr("About to delete: %1\nThis may be permanent and will completely delete the instance.\n\nAre you sure?") + .arg(m_selectedInstance->name()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response == QMessageBox::Yes) { + if (APPLICATION->instances()->trashInstance(id)) { + ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); + return; + } + APPLICATION->instances()->deleteInstance(id); } } @@ -2075,6 +2205,145 @@ void MainWindow::on_actionKillInstance_triggered() } } +void MainWindow::on_actionCreateInstanceShortcut_triggered() +{ + if (m_selectedInstance) + { + auto desktopPath = FS::getDesktopDir(); + if (desktopPath.isEmpty()) { + // TODO come up with an alternative solution (open "save file" dialog) + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); + return; + } + +#if defined(Q_OS_MACOS) + QString appPath = QApplication::applicationFilePath(); + if (appPath.startsWith("/private/var/")) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); + return; + } + + if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), + appPath, { "--launch", m_selectedInstance->id() }, + m_selectedInstance->name(), "")) { + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + } + else + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + } +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QString appPath = QApplication::applicationFilePath(); + if (appPath.startsWith("/tmp/.mount_")) { + // AppImage! + appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (appPath.isEmpty()) + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } + else if (appPath.endsWith("/")) + { + appPath.chop(1); + } + } + + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) + { + icon = APPLICATION->icons()->icon("grass"); + } + + QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); + iconFile.close(); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + + QString desktopFilePath = FS::PathCombine(desktopPath, m_selectedInstance->name() + ".desktop"); + QStringList args; + if (DesktopServices::isFlatpak()) { + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(desktopPath); + desktopFilePath = fileDialog.getSaveFileName( + this, tr("Create Shortcut"), desktopFilePath, + tr("Desktop Entries (*.desktop)")); + if (desktopFilePath.isEmpty()) + return; // file dialog canceled by user + appPath = "flatpak"; + QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; + flatpakAppId.remove(".desktop"); + args.append({ "run", flatpakAppId }); + } + args.append({ "--launch", m_selectedInstance->id() }); + if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + } + else + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + } +#elif defined(Q_OS_WIN) + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) + { + icon = APPLICATION->icons()->icon("grass"); + } + + QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); + + // part of fix for weird bug involving the window icon being replaced + // dunno why it happens, but this 2-line fix seems to be enough, so w/e + auto appIcon = APPLICATION->getThemedIcon("logo"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); + iconFile.close(); + + // restore original window icon + QGuiApplication::setWindowIcon(appIcon); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + + if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), + QApplication::applicationFilePath(), { "--launch", m_selectedInstance->id() }, + m_selectedInstance->name(), iconPath)) { + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + } + else + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + } +#else + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); +#endif + } +} + void MainWindow::taskEnd() { QObject *sender = QObject::sender(); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index f9d1f1c7..f96f641d 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -128,6 +128,10 @@ private slots: void on_actionClearMetadata_triggered(); + #ifdef Q_OS_MAC + void on_actionAddToPATH_triggered(); + #endif + void on_actionOpenWiki_triggered(); void on_actionMoreNews_triggered(); @@ -157,6 +161,8 @@ private slots: void on_actionEditInstance_triggered(); + void on_actionCreateInstanceShortcut_triggered(); + void taskEnd(); /** @@ -170,6 +176,8 @@ private slots: void updateToolsMenu(); + void updateThemeMenu(); + void instanceActivated(QModelIndex); void instanceChanged(const QModelIndex ¤t, const QModelIndex &previous); diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 2cf94250..8b49bd1a 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -1,33 +1,72 @@ +// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net> +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// SPDX-FileCopyrightText: 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (C) 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + #include "BlockedModsDialog.h" -#include <QDesktopServices> -#include <QDialogButtonBox> -#include <QPushButton> -#include "Application.h" #include "ui_BlockedModsDialog.h" +#include "Application.h" +#include "modplatform/helpers/HashUtils.h" + #include <QDebug> +#include <QDesktopServices> +#include <QDialogButtonBox> +#include <QDragEnterEvent> +#include <QFileDialog> +#include <QFileInfo> +#include <QPushButton> #include <QStandardPaths> BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods) - : QDialog(parent), ui(new Ui::BlockedModsDialog), mods(mods) + : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods) { + m_hashing_task = shared_qobject_ptr<ConcurrentTask>(new ConcurrentTask(this, "MakeHashesTask", 10)); + connect(m_hashing_task.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished); + ui->setupUi(this); - auto openAllButton = ui->buttonBox->addButton(tr("Open All"), QDialogButtonBox::ActionRole); - connect(openAllButton, &QPushButton::clicked, this, &BlockedModsDialog::openAll); + m_openMissingButton = ui->buttonBox->addButton(tr("Open Missing"), QDialogButtonBox::ActionRole); + connect(m_openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); }); - connect(&watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); + auto downloadFolderButton = ui->buttonBox->addButton(tr("Add Download Folder"), QDialogButtonBox::ActionRole); + connect(downloadFolderButton, &QPushButton::clicked, this, &BlockedModsDialog::addDownloadFolder); - hashing_task = shared_qobject_ptr<ConcurrentTask>(new ConcurrentTask(this, "MakeHashesTask", 10)); + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); - qDebug() << "Mods List: " << mods; + qDebug() << "[Blocked Mods Dialog] Mods List: " << mods; setupWatch(); scanPaths(); this->setWindowTitle(title); - ui->label->setText(text); - ui->labelModsFound->setText(tr("Please download the missing mods.")); + ui->labelDescription->setText(text); + + // force all URL handling as external + connect(ui->textBrowserWatched, &QTextBrowser::anchorClicked, this, [](const QUrl url) { QDesktopServices::openUrl(url); }); + + setAcceptDrops(true); + update(); } @@ -36,20 +75,71 @@ BlockedModsDialog::~BlockedModsDialog() delete ui; } -void BlockedModsDialog::openAll() +void BlockedModsDialog::dragEnterEvent(QDragEnterEvent* e) { - for (auto& mod : mods) { - QDesktopServices::openUrl(mod.websiteUrl); + if (e->mimeData()->hasUrls()) { + e->acceptProposedAction(); } } +void BlockedModsDialog::dropEvent(QDropEvent* e) +{ + for (QUrl& url : e->mimeData()->urls()) { + if (url.scheme().isEmpty()) { // ensure isLocalFile() works correctly + url.setScheme("file"); + } + + if (!url.isLocalFile()) { // can't drop external files here. + continue; + } + + QString filePath = url.toLocalFile(); + qDebug() << "[Blocked Mods Dialog] Dropped file:" << filePath; + addHashTask(filePath); + + // watch for changes + QFileInfo file = QFileInfo(filePath); + QString path = file.dir().absolutePath(); + qDebug() << "[Blocked Mods Dialog] Adding watch path:" << path; + m_watcher.addPath(path); + } + scanPaths(); + update(); +} + +void BlockedModsDialog::done(int r) +{ + QDialog::done(r); + disconnect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); +} + +void BlockedModsDialog::openAll(bool missingOnly) +{ + for (auto& mod : m_mods) { + if (!missingOnly || !mod.matched) { + QDesktopServices::openUrl(mod.websiteUrl); + } + } +} + +void BlockedModsDialog::addDownloadFolder() +{ + QString dir = + QFileDialog::getExistingDirectory(this, tr("Select directory where you downloaded the mods"), + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation), QFileDialog::ShowDirsOnly); + qDebug() << "[Blocked Mods Dialog] Adding watch path:" << dir; + m_watcher.addPath(dir); + scanPath(dir, true); + update(); +} + /// @brief update UI with current status of the blocked mod detection void BlockedModsDialog::update() { QString text; QString span; - for (auto& mod : mods) { + for (auto& mod : m_mods) { if (mod.matched) { // ✔ -> html for HEAVY CHECK MARK : ✔ span = QString(tr("<span style=\"color:green\"> ✔ Found at %1 </span>")).arg(mod.localPath); @@ -60,12 +150,21 @@ void BlockedModsDialog::update() text += QString(tr("%1: <a href='%2'>%2</a> <p>Hash: %3 %4</p> <br/>")).arg(mod.name, mod.websiteUrl, mod.hash, span); } - ui->textBrowser->setText(text); + ui->textBrowserModsListing->setText(text); + + QString watching; + for (auto& dir : m_watcher.directories()) { + watching += QString("<a href=\"%1\">%1</a><br/>").arg(dir); + } + + ui->textBrowserWatched->setText(watching); if (allModsMatched()) { - ui->labelModsFound->setText(tr("All mods found ✔")); + ui->labelModsFound->setText("<span style=\"color:green\">✔</span>" + tr("All mods found")); + m_openMissingButton->setDisabled(true); } else { ui->labelModsFound->setText(tr("Please download the missing mods.")); + m_openMissingButton->setDisabled(false); } } @@ -73,8 +172,9 @@ void BlockedModsDialog::update() /// @param path the path to the changed directory void BlockedModsDialog::directoryChanged(QString path) { - qDebug() << "Directory changed: " << path; - scanPath(path); + qDebug() << "[Blocked Mods Dialog] Directory changed: " << path; + validateMatchedMods(); + scanPath(path, true); } /// @brief add the user downloads folder and the global mods folder to the filesystem watcher @@ -82,22 +182,23 @@ void BlockedModsDialog::setupWatch() { const QString downloadsFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); const QString modsFolder = APPLICATION->settings()->get("CentralModsDir").toString(); - watcher.addPath(downloadsFolder); - watcher.addPath(modsFolder); + m_watcher.addPath(downloadsFolder); + m_watcher.addPath(modsFolder); } /// @brief scan all watched folder void BlockedModsDialog::scanPaths() { - for (auto& dir : watcher.directories()) { - scanPath(dir); + for (auto& dir : m_watcher.directories()) { + scanPath(dir, false); } + runHashTask(); } /// @brief Scan the directory at path, skip paths that do not contain a file name /// of a blocked mod we are looking for /// @param path the directory to scan -void BlockedModsDialog::scanPath(QString path) +void BlockedModsDialog::scanPath(QString path, bool start_task) { QDir scan_dir(path); QDirIterator scan_it(path, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::NoIteratorFlags); @@ -108,17 +209,35 @@ void BlockedModsDialog::scanPath(QString path) continue; } - auto hash_task = Hashing::createBlockedModHasher(file, ModPlatform::Provider::FLAME, "sha1"); + addHashTask(file); + } - qDebug() << "Creating Hash task for path: " << file; + if (start_task) { + runHashTask(); + } +} - connect(hash_task.get(), &Task::succeeded, [this, hash_task, file] { checkMatchHash(hash_task->getResult(), file); }); - connect(hash_task.get(), &Task::failed, [file] { qDebug() << "Failed to hash path: " << file; }); +/// @brief add a hashing task for the file located at path, add the path to the pending set if the hasing task is already running +/// @param path the path to the local file being hashed +void BlockedModsDialog::addHashTask(QString path) +{ + qDebug() << "[Blocked Mods Dialog] adding a Hash task for" << path << "to the pending set."; + m_pending_hash_paths.insert(path); +} - hashing_task->addTask(hash_task); - } +/// @brief add a hashing task for the file located at path and connect it to check that hash against +/// our blocked mods list +/// @param path the path to the local file being hashed +void BlockedModsDialog::buildHashTask(QString path) +{ + auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::Provider::FLAME, "sha1"); + + qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path; + + connect(hash_task.get(), &Task::succeeded, this, [this, hash_task, path] { checkMatchHash(hash_task->getResult(), path); }); + connect(hash_task.get(), &Task::failed, this, [path] { qDebug() << "Failed to hash path: " << path; }); - hashing_task->start(); + m_hashing_task->addTask(hash_task); } /// @brief check if the computed hash for the provided path matches a blocked @@ -129,9 +248,9 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) { bool match = false; - qDebug() << "Checking for match on hash: " << hash << "| From path:" << path; + qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path; - for (auto& mod : mods) { + for (auto& mod : m_mods) { if (mod.matched) { continue; } @@ -140,7 +259,7 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) mod.localPath = path; match = true; - qDebug() << "Hash match found:" << mod.name << hash << "| From path:" << path; + qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path; break; } @@ -156,12 +275,22 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) /// @return boolean: did the path match the name of a blocked mod? bool BlockedModsDialog::checkValidPath(QString path) { - QFileInfo file = QFileInfo(path); - QString filename = file.fileName(); - - for (auto& mod : mods) { - if (mod.name.compare(filename, Qt::CaseInsensitive) == 0) { - qDebug() << "Name match found:" << mod.name << "| From path:" << path; + const QFileInfo file = QFileInfo(path); + const QString filename = file.fileName(); + QString laxFilename(filename); + laxFilename.replace('+', ' '); + + auto compare = [](QString fsfilename, QString metadataFilename) { + return metadataFilename.compare(fsfilename, Qt::CaseInsensitive) == 0; + }; + + for (auto& mod : m_mods) { + if (compare(filename, mod.name)) { + qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; + return true; + } + if (compare(laxFilename, mod.name)) { + qDebug() << "[Blocked Mods Dialog] Lax name match found:" << mod.name << "| From path:" << path; return true; } } @@ -171,7 +300,61 @@ bool BlockedModsDialog::checkValidPath(QString path) bool BlockedModsDialog::allModsMatched() { - return std::all_of(mods.begin(), mods.end(), [](auto const& mod) { return mod.matched; }); + return std::all_of(m_mods.begin(), m_mods.end(), [](auto const& mod) { return mod.matched; }); +} + +/// @brief ensure matched file paths still exist +void BlockedModsDialog::validateMatchedMods() +{ + bool changed = false; + for (auto& mod : m_mods) { + if (mod.matched) { + QFileInfo file = QFileInfo(mod.localPath); + if (!file.exists() || !file.isFile()) { + qDebug() << "[Blocked Mods Dialog] File" << mod.localPath << "for mod" << mod.name + << "has vanshed! marking as not matched."; + mod.localPath = ""; + mod.matched = false; + changed = true; + } + } + } + if (changed) { + update(); + } +} + +/// @brief run hash task or mark a pending run if it is already runing +void BlockedModsDialog::runHashTask() +{ + if (!m_hashing_task->isRunning()) { + m_rehash_pending = false; + + if (!m_pending_hash_paths.isEmpty()) { + qDebug() << "[Blocked Mods Dialog] there are pending hash tasks, building and running tasks"; + + auto path = m_pending_hash_paths.begin(); + while (path != m_pending_hash_paths.end()) { + buildHashTask(*path); + path = m_pending_hash_paths.erase(path); + } + + m_hashing_task->start(); + } + } else { + qDebug() << "[Blocked Mods Dialog] queueing another run of the hashing task"; + qDebug() << "[Blocked Mods Dialog] pending hash tasks:" << m_pending_hash_paths; + m_rehash_pending = true; + } +} + +void BlockedModsDialog::hashTaskFinished() +{ + qDebug() << "[Blocked Mods Dialog] All hash tasks finished"; + if (m_rehash_pending) { + qDebug() << "[Blocked Mods Dialog] task finished with a rehash pending, rerunning"; + runHashTask(); + } } /// qDebug print support for the BlockedMod struct diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index 0a5c90db..014f488a 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -1,53 +1,95 @@ +// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net> +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// SPDX-FileCopyrightText: 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (C) 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + #pragma once #include <QDialog> -#include <QString> #include <QList> +#include <QString> #include <QFileSystemWatcher> -#include "modplatform/helpers/HashUtils.h" - #include "tasks/ConcurrentTask.h" +class QPushButton; + struct BlockedMod { QString name; QString websiteUrl; QString hash; bool matched; QString localPath; - + QString targetFolder; }; QT_BEGIN_NAMESPACE -namespace Ui { class BlockedModsDialog; } +namespace Ui { +class BlockedModsDialog; +} QT_END_NAMESPACE class BlockedModsDialog : public QDialog { -Q_OBJECT + Q_OBJECT -public: - BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, QList<BlockedMod> &mods); + public: + BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods); ~BlockedModsDialog() override; + protected: + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; + + protected slots: + void done(int r) override; -private: - Ui::BlockedModsDialog *ui; - QList<BlockedMod> &mods; - QFileSystemWatcher watcher; - shared_qobject_ptr<ConcurrentTask> hashing_task; + private: + Ui::BlockedModsDialog* ui; + QList<BlockedMod>& m_mods; + QFileSystemWatcher m_watcher; + shared_qobject_ptr<ConcurrentTask> m_hashing_task; + QSet<QString> m_pending_hash_paths; + bool m_rehash_pending; + QPushButton* m_openMissingButton; - void openAll(); + void openAll(bool missingOnly); + void addDownloadFolder(); void update(); void directoryChanged(QString path); void setupWatch(); void scanPaths(); - void scanPath(QString path); + void scanPath(QString path, bool start_task); + void addHashTask(QString path); + void buildHashTask(QString path); void checkMatchHash(QString hash, QString path); + void validateMatchedMods(); + void runHashTask(); + void hashTaskFinished(); bool checkValidPath(QString path); bool allModsMatched(); }; -QDebug operator<<(QDebug debug, const BlockedMod &m); +QDebug operator<<(QDebug debug, const BlockedMod& m); diff --git a/launcher/ui/dialogs/BlockedModsDialog.ui b/launcher/ui/dialogs/BlockedModsDialog.ui index 371549cf..2292b99c 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.ui +++ b/launcher/ui/dialogs/BlockedModsDialog.ui @@ -7,25 +7,47 @@ <x>0</x> <y>0</y> <width>400</width> - <height>455</height> + <height>400</height> </rect> </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>350</height> + </size> + </property> <property name="windowTitle"> <string notr="true">BlockedModsDialog</string> </property> - <layout class="QVBoxLayout" name="verticalLayout"> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,3,0,1,0"> <item> - <widget class="QLabel" name="label"> + <widget class="QLabel" name="labelDescription"> <property name="text"> - <string notr="true"/> + <string notr="true">Placeholder description</string> </property> <property name="textFormat"> <enum>Qt::RichText</enum> </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="labelExplain"> + <property name="text"> + <string><html><head/><body><p>Your configured global mods folder and default downloads folder are automatically checked for the downloaded mods and they will be copied to the instance if found.</p><p>Optionally, you may drag and drop the downloaded mods onto this dialog or add a folder to watch if you did not download the mods to a default location.</p></body></html></string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> </widget> </item> <item> - <widget class="QTextBrowser" name="textBrowser"> + <widget class="QTextBrowser" name="textBrowserModsListing"> <property name="acceptRichText"> <bool>true</bool> </property> @@ -35,6 +57,29 @@ </widget> </item> <item> + <widget class="QLabel" name="labelWatched"> + <property name="text"> + <string>Watched Folders:</string> + </property> + </widget> + </item> + <item> + <widget class="QTextBrowser" name="textBrowserWatched"> + <property name="baseSize"> + <size> + <width>0</width> + <height>12</height> + </size> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>false</bool> + </property> + </widget> + </item> + <item> <layout class="QHBoxLayout" name="bottomBoxH"> <item> <widget class="QLabel" name="labelModsFound"> diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.cpp b/launcher/ui/dialogs/ImportResourcePackDialog.cpp new file mode 100644 index 00000000..e807e926 --- /dev/null +++ b/launcher/ui/dialogs/ImportResourcePackDialog.cpp @@ -0,0 +1,66 @@ +#include "ImportResourcePackDialog.h" +#include "ui_ImportResourcePackDialog.h" + +#include <QFileDialog> +#include <QPushButton> + +#include "Application.h" +#include "InstanceList.h" + +#include <InstanceList.h> +#include "ui/instanceview/InstanceProxyModel.h" +#include "ui/instanceview/InstanceDelegate.h" + +ImportResourcePackDialog::ImportResourcePackDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ImportResourcePackDialog) +{ + ui->setupUi(this); + setWindowModality(Qt::WindowModal); + + auto contentsWidget = ui->instanceView; + contentsWidget->setViewMode(QListView::ListMode); + contentsWidget->setFlow(QListView::LeftToRight); + contentsWidget->setIconSize(QSize(48, 48)); + contentsWidget->setMovement(QListView::Static); + contentsWidget->setResizeMode(QListView::Adjust); + contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); + contentsWidget->setSpacing(5); + contentsWidget->setWordWrap(true); + contentsWidget->setWrapping(true); + // NOTE: We can't have uniform sizes because the text may wrap if it's too long. If we set this, it will cut off the wrapped text. + contentsWidget->setUniformItemSizes(false); + contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + contentsWidget->setItemDelegate(new ListViewDelegate()); + + proxyModel = new InstanceProxyModel(this); + proxyModel->setSourceModel(APPLICATION->instances().get()); + proxyModel->sort(0); + contentsWidget->setModel(proxyModel); + + connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); + connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), + SLOT(selectionChanged(QItemSelection, QItemSelection))); +} + +void ImportResourcePackDialog::activated(QModelIndex index) +{ + selectedInstanceKey = index.data(InstanceList::InstanceIDRole).toString(); + accept(); +} + +void ImportResourcePackDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +{ + if (selected.empty()) + return; + + QString key = selected.first().indexes().first().data(InstanceList::InstanceIDRole).toString(); + if (!key.isEmpty()) { + selectedInstanceKey = key; + } +} + +ImportResourcePackDialog::~ImportResourcePackDialog() +{ + delete ui; +} diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.h b/launcher/ui/dialogs/ImportResourcePackDialog.h new file mode 100644 index 00000000..8356f204 --- /dev/null +++ b/launcher/ui/dialogs/ImportResourcePackDialog.h @@ -0,0 +1,27 @@ +#pragma once + +#include <QDialog> +#include <QItemSelection> + +#include "ui/instanceview/InstanceProxyModel.h" + +namespace Ui { +class ImportResourcePackDialog; +} + +class ImportResourcePackDialog : public QDialog { + Q_OBJECT + + public: + explicit ImportResourcePackDialog(QWidget* parent = 0); + ~ImportResourcePackDialog(); + InstanceProxyModel* proxyModel; + QString selectedInstanceKey; + + private: + Ui::ImportResourcePackDialog* ui; + + private slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); +}; diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.ui b/launcher/ui/dialogs/ImportResourcePackDialog.ui new file mode 100644 index 00000000..20cb9177 --- /dev/null +++ b/launcher/ui/dialogs/ImportResourcePackDialog.ui @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ImportResourcePackDialog</class> + <widget class="QDialog" name="ImportResourcePackDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>676</width> + <height>555</height> + </rect> + </property> + <property name="windowTitle"> + <string>Choose instance to import</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Choose the instance you would like to import this resource pack to.</string> + </property> + </widget> + </item> + <item> + <widget class="QListView" name="instanceView"/> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ImportResourcePackDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ImportResourcePackDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index 05269f62..da73a449 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -44,7 +44,8 @@ void ProgressDialog::setSkipButton(bool present, QString label) void ProgressDialog::on_skipButton_clicked(bool checked) { Q_UNUSED(checked); - task->abort(); + if (ui->skipButton->isEnabled()) // prevent other triggers from aborting + task->abort(); } ProgressDialog::~ProgressDialog() diff --git a/launcher/ui/pages/BasePageContainer.h b/launcher/ui/pages/BasePageContainer.h index f8c7adeb..b41fe12a 100644 --- a/launcher/ui/pages/BasePageContainer.h +++ b/launcher/ui/pages/BasePageContainer.h @@ -1,10 +1,13 @@ #pragma once +class BasePage; + class BasePageContainer { public: virtual ~BasePageContainer(){}; virtual bool selectPage(QString pageId) = 0; + virtual BasePage* getPage(QString pageId) { return nullptr; }; virtual void refreshContainer() = 0; virtual bool requestClose() = 0; }; diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 2cee15bf..81dd4cc1 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -58,9 +58,8 @@ JavaPage::JavaPage(QWidget *parent) : QWidget(parent), ui(new Ui::JavaPage) ui->setupUi(this); ui->tabWidget->tabBar()->hide(); - auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; - ui->maxMemSpinBox->setMaximum(sysMiB); loadSettings(); + updateThresholds(); } JavaPage::~JavaPage() @@ -177,6 +176,11 @@ void JavaPage::on_javaTestBtn_clicked() checker->run(); } +void JavaPage::on_maxMemSpinBox_valueChanged(int i) +{ + updateThresholds(); +} + void JavaPage::checkerFinished() { checker.reset(); @@ -186,3 +190,29 @@ void JavaPage::retranslate() { ui->retranslateUi(this); } + +void JavaPage::updateThresholds() +{ + auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; + unsigned int maxMem = ui->maxMemSpinBox->value(); + + QString iconName; + + if (maxMem >= sysMiB) { + iconName = "status-bad"; + ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); + } else if (maxMem > (sysMiB * 0.9)) { + iconName = "status-yellow"; + ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else { + iconName = "status-good"; + ui->labelMaxMemIcon->setToolTip(""); + } + + { + auto height = ui->labelMaxMemIcon->fontInfo().pixelSize(); + QIcon icon = APPLICATION->getThemedIcon(iconName); + QPixmap pix = icon.pixmap(height, height); + ui->labelMaxMemIcon->setPixmap(pix); + } +} diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h index 64d4098e..2ef6d749 100644 --- a/launcher/ui/pages/global/JavaPage.h +++ b/launcher/ui/pages/global/JavaPage.h @@ -76,6 +76,8 @@ public: bool apply() override; void retranslate() override; + void updateThresholds(); + private: void applySettings(); void loadSettings(); @@ -85,6 +87,7 @@ slots: void on_javaDetectBtn_clicked(); void on_javaTestBtn_clicked(); void on_javaBrowseBtn_clicked(); + void on_maxMemSpinBox_valueChanged(int i); void checkerFinished(); private: diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index 6ccffed4..6749cbe4 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -44,50 +44,38 @@ <property name="title"> <string>Memory</string> </property> - <layout class="QGridLayout" name="gridLayout_2"> - <item row="1" column="1"> - <widget class="QSpinBox" name="maxMemSpinBox"> - <property name="toolTip"> - <string>The maximum amount of memory Minecraft is allowed to use.</string> - </property> - <property name="suffix"> - <string notr="true"> MiB</string> - </property> - <property name="minimum"> - <number>128</number> - </property> - <property name="maximum"> - <number>65536</number> - </property> - <property name="singleStep"> - <number>128</number> + <layout class="QGridLayout" name="gridLayout_2" columnstretch="1,0,0,0"> + <item row="1" column="0"> + <widget class="QLabel" name="labelMaxMem"> + <property name="text"> + <string>Ma&ximum memory allocation:</string> </property> - <property name="value"> - <number>1024</number> + <property name="buddy"> + <cstring>maxMemSpinBox</cstring> </property> </widget> </item> - <item row="0" column="0"> - <widget class="QLabel" name="labelMinMem"> + <item row="2" column="0"> + <widget class="QLabel" name="labelPermGen"> <property name="text"> - <string>&Minimum memory allocation:</string> + <string notr="true">&PermGen:</string> </property> <property name="buddy"> - <cstring>minMemSpinBox</cstring> + <cstring>permGenSpinBox</cstring> </property> </widget> </item> - <item row="1" column="0"> - <widget class="QLabel" name="labelMaxMem"> + <item row="0" column="0"> + <widget class="QLabel" name="labelMinMem"> <property name="text"> - <string>Ma&ximum memory allocation:</string> + <string>&Minimum memory allocation:</string> </property> <property name="buddy"> - <cstring>maxMemSpinBox</cstring> + <cstring>minMemSpinBox</cstring> </property> </widget> </item> - <item row="0" column="1"> + <item row="0" column="2"> <widget class="QSpinBox" name="minMemSpinBox"> <property name="toolTip"> <string>The amount of memory Minecraft is started with.</string> @@ -99,7 +87,7 @@ <number>128</number> </property> <property name="maximum"> - <number>65536</number> + <number>1048576</number> </property> <property name="singleStep"> <number>128</number> @@ -109,17 +97,29 @@ </property> </widget> </item> - <item row="2" column="0"> - <widget class="QLabel" name="labelPermGen"> - <property name="text"> - <string notr="true">&PermGen:</string> + <item row="1" column="2"> + <widget class="QSpinBox" name="maxMemSpinBox"> + <property name="toolTip"> + <string>The maximum amount of memory Minecraft is allowed to use.</string> </property> - <property name="buddy"> - <cstring>permGenSpinBox</cstring> + <property name="suffix"> + <string notr="true"> MiB</string> + </property> + <property name="minimum"> + <number>128</number> + </property> + <property name="maximum"> + <number>1048576</number> + </property> + <property name="singleStep"> + <number>128</number> + </property> + <property name="value"> + <number>1024</number> </property> </widget> </item> - <item row="2" column="1"> + <item row="2" column="2"> <widget class="QSpinBox" name="permGenSpinBox"> <property name="toolTip"> <string>The amount of memory available to store loaded Java classes.</string> @@ -141,6 +141,16 @@ </property> </widget> </item> + <item row="1" column="3"> + <widget class="QLabel" name="labelMaxMemIcon"> + <property name="text"> + <string/> + </property> + <property name="buddy"> + <cstring>maxMemSpinBox</cstring> + </property> + </widget> + </item> </layout> </widget> </item> diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 6661bf0f..cae0635f 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -303,21 +303,27 @@ void LauncherPage::applySettings() s->set("IconTheme", "pe_blue"); break; case 4: - s->set("IconTheme", "OSX"); + s->set("IconTheme", "breeze_light"); break; case 5: - s->set("IconTheme", "iOS"); + s->set("IconTheme", "breeze_dark"); break; case 6: - s->set("IconTheme", "flat"); + s->set("IconTheme", "OSX"); break; case 7: - s->set("IconTheme", "flat_white"); + s->set("IconTheme", "iOS"); break; case 8: - s->set("IconTheme", "multimc"); + s->set("IconTheme", "flat"); break; case 9: + s->set("IconTheme", "flat_white"); + break; + case 10: + s->set("IconTheme", "multimc"); + break; + case 11: s->set("IconTheme", "custom"); break; } @@ -397,7 +403,18 @@ void LauncherPage::loadSettings() m_currentUpdateChannel = s->get("UpdateChannel").toString(); //FIXME: make generic auto theme = s->get("IconTheme").toString(); - QStringList iconThemeOptions{"pe_colored", "pe_light", "pe_dark", "pe_blue", "OSX", "iOS", "flat", "flat_white", "multimc", "custom"}; + QStringList iconThemeOptions{"pe_colored", + "pe_light", + "pe_dark", + "pe_blue", + "breeze_light", + "breeze_dark", + "OSX", + "iOS", + "flat", + "flat_white", + "multimc", + "custom"}; ui->themeComboBox->setCurrentIndex(iconThemeOptions.indexOf(theme)); auto cat = s->get("BackgroundCat").toString(); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 6de644ee..c44718a1 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -287,6 +287,16 @@ </item> <item> <property name="text"> + <string>Breeze Light</string> + </property> + </item> + <item> + <property name="text"> + <string>Breeze Dark</string> + </property> + </item> + <item> + <property name="text"> <string notr="true">OSX</string> </property> </item> diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index b6c873cc..5c919573 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -14,8 +14,6 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared { ui->setupUi(this); - ExternalResourcesPage::runningStateChanged(m_instance && m_instance->isRunning()); - ui->actionsToolbar->insertSpacer(ui->actionViewConfigs); m_filterModel = model->createFilterProxyModel(this); @@ -45,7 +43,6 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared auto selection_model = ui->treeView->selectionModel(); connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current); connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged); - connect(m_instance, &BaseInstance::runningStatusChanged, this, &ExternalResourcesPage::runningStateChanged); } ExternalResourcesPage::~ExternalResourcesPage() @@ -97,14 +94,6 @@ void ExternalResourcesPage::filterTextChanged(const QString& newContents) m_filterModel->setFilterRegularExpression(m_viewFilter); } -void ExternalResourcesPage::runningStateChanged(bool running) -{ - if (m_controlsEnabled == !running) - return; - - m_controlsEnabled = !running; -} - bool ExternalResourcesPage::shouldDisplay() const { return true; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index 8e352cef..11058bf6 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -47,7 +47,6 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { protected slots: void itemActivated(const QModelIndex& index); void filterTextChanged(const QString& newContents); - virtual void runningStateChanged(bool running); virtual void addItem(); virtual void removeItem(); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 5da7f19f..af2ba7c8 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -59,12 +59,12 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent) { m_settings = inst->settings(); ui->setupUi(this); - auto sysMB = Sys::getSystemRam() / Sys::mebibyte; - ui->maxMemSpinBox->setMaximum(sysMB); + connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); loadSettings(); + updateThresholds(); } bool InstanceSettingsPage::shouldDisplay() const @@ -437,6 +437,11 @@ void InstanceSettingsPage::on_javaTestBtn_clicked() checker->run(); } +void InstanceSettingsPage::on_maxMemSpinBox_valueChanged(int i) +{ + updateThresholds(); +} + void InstanceSettingsPage::checkerFinished() { checker.reset(); @@ -447,3 +452,29 @@ void InstanceSettingsPage::retranslate() ui->retranslateUi(this); ui->customCommands->retranslate(); // TODO: why is this seperate from the others? } + +void InstanceSettingsPage::updateThresholds() +{ + auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; + unsigned int maxMem = ui->maxMemSpinBox->value(); + + QString iconName; + + if (maxMem >= sysMiB) { + iconName = "status-bad"; + ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); + } else if (maxMem > (sysMiB * 0.9)) { + iconName = "status-yellow"; + ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else { + iconName = "status-good"; + ui->labelMaxMemIcon->setToolTip(""); + } + + { + auto height = ui->labelMaxMemIcon->fontInfo().pixelSize(); + QIcon icon = APPLICATION->getThemedIcon(iconName); + QPixmap pix = icon.pixmap(height, height); + ui->labelMaxMemIcon->setPixmap(pix); + } +} diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 97d1296f..7450188d 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -77,10 +77,13 @@ public: virtual bool shouldDisplay() const override; void retranslate() override; + void updateThresholds(); + private slots: void on_javaDetectBtn_clicked(); void on_javaTestBtn_clicked(); void on_javaBrowseBtn_clicked(); + void on_maxMemSpinBox_valueChanged(int i); void applySettings(); void loadSettings(); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 8b3c3370..b064367d 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -112,7 +112,14 @@ <property name="checked"> <bool>false</bool> </property> - <layout class="QGridLayout" name="gridLayout_2"> + <layout class="QGridLayout" name="gridLayout_2" columnstretch="1,0,0,0"> + <item row="2" column="0"> + <widget class="QLabel" name="labelPermGen"> + <property name="text"> + <string notr="true">PermGen:</string> + </property> + </widget> + </item> <item row="0" column="0"> <widget class="QLabel" name="labelMinMem"> <property name="text"> @@ -120,10 +127,24 @@ </property> </widget> </item> - <item row="1" column="1"> - <widget class="QSpinBox" name="maxMemSpinBox"> + <item row="1" column="0"> + <widget class="QLabel" name="labelMaxMem"> + <property name="text"> + <string>Maximum memory allocation:</string> + </property> + </widget> + </item> + <item row="3" column="0" colspan="3"> + <widget class="QLabel" name="labelPermgenNote"> + <property name="text"> + <string>Note: Permgen is set automatically by Java 8 and later</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QSpinBox" name="minMemSpinBox"> <property name="toolTip"> - <string>The maximum amount of memory Minecraft is allowed to use.</string> + <string>The amount of memory Minecraft is started with.</string> </property> <property name="suffix"> <string notr="true"> MiB</string> @@ -132,20 +153,20 @@ <number>128</number> </property> <property name="maximum"> - <number>65536</number> + <number>1048576</number> </property> <property name="singleStep"> <number>128</number> </property> <property name="value"> - <number>1024</number> + <number>256</number> </property> </widget> </item> - <item row="0" column="1"> - <widget class="QSpinBox" name="minMemSpinBox"> + <item row="1" column="2"> + <widget class="QSpinBox" name="maxMemSpinBox"> <property name="toolTip"> - <string>The amount of memory Minecraft is started with.</string> + <string>The maximum amount of memory Minecraft is allowed to use.</string> </property> <property name="suffix"> <string notr="true"> MiB</string> @@ -154,17 +175,17 @@ <number>128</number> </property> <property name="maximum"> - <number>65536</number> + <number>1048576</number> </property> <property name="singleStep"> <number>128</number> </property> <property name="value"> - <number>256</number> + <number>1024</number> </property> </widget> </item> - <item row="2" column="1"> + <item row="2" column="2"> <widget class="QSpinBox" name="permGenSpinBox"> <property name="toolTip"> <string>The amount of memory available to store loaded Java classes.</string> @@ -186,24 +207,16 @@ </property> </widget> </item> - <item row="2" column="0"> - <widget class="QLabel" name="labelPermGen"> + <item row="1" column="3"> + <widget class="QLabel" name="labelMaxMemIcon"> <property name="text"> - <string notr="true">PermGen:</string> + <string notr="true"/> </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="labelMaxMem"> - <property name="text"> - <string>Maximum memory allocation:</string> + <property name="alignment"> + <set>Qt::AlignCenter</set> </property> - </widget> - </item> - <item row="3" column="0" colspan="2"> - <widget class="QLabel" name="labelPermgenNote"> - <property name="text"> - <string>Note: Permgen is set automatically by Java 8 and later</string> + <property name="buddy"> + <cstring>maxMemSpinBox</cstring> </property> </widget> </item> diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp new file mode 100644 index 00000000..7a0d234c --- /dev/null +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -0,0 +1,432 @@ +// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "ManagedPackPage.h" +#include "ui_ManagedPackPage.h" + +#include <QListView> +#include <QProxyStyle> + +#include <HoeDown.h> + +#include "Application.h" +#include "BuildConfig.h" +#include "InstanceImportTask.h" +#include "InstanceList.h" +#include "InstanceTask.h" +#include "Json.h" + +#include "modplatform/modrinth/ModrinthPackManifest.h" + +#include "ui/InstanceWindow.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" + +/** This is just to override the combo box popup behavior so that the combo box doesn't take the whole screen. + * ... thanks Qt. + */ +class NoBigComboBoxStyle : public QProxyStyle { + Q_OBJECT + + public: + NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {} + + // clang-format off + int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, QStyleHintReturn* returnData = nullptr) const override + { + if (hint == QStyle::SH_ComboBox_Popup) + return false; + + return QProxyStyle::styleHint(hint, option, widget, returnData); + } + // clang-format on +}; + +ManagedPackPage* ManagedPackPage::createPage(BaseInstance* inst, QString type, QWidget* parent) +{ + if (type == "modrinth") + return new ModrinthManagedPackPage(inst, nullptr, parent); + if (type == "flame" && (APPLICATION->capabilities() & Application::SupportsFlame)) + return new FlameManagedPackPage(inst, nullptr, parent); + + return new GenericManagedPackPage(inst, nullptr, parent); +} + +ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) + : QWidget(parent), m_instance_window(instance_window), ui(new Ui::ManagedPackPage), m_inst(inst) +{ + Q_ASSERT(inst); + + ui->setupUi(this); + + ui->versionsComboBox->setStyle(new NoBigComboBoxStyle(ui->versionsComboBox->style())); + + ui->reloadButton->setVisible(false); + connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool){ + ui->reloadButton->setVisible(false); + + m_loaded = false; + // Pretend we're opening the page again + openedImpl(); + }); +} + +ManagedPackPage::~ManagedPackPage() +{ + delete ui; +} + +void ManagedPackPage::openedImpl() +{ + ui->packName->setText(m_inst->getManagedPackName()); + ui->packVersion->setText(m_inst->getManagedPackVersionName()); + ui->packOrigin->setText(tr("Website: <a href=%1>%2</a> | Pack ID: %3 | Version ID: %4") + .arg(url(), displayName(), m_inst->getManagedPackID(), m_inst->getManagedPackVersionID())); + + parseManagedPack(); +} + +QString ManagedPackPage::displayName() const +{ + auto type = m_inst->getManagedPackType(); + if (type.isEmpty()) + return {}; + if (type == "flame") + type = "CurseForge"; + return type.replace(0, 1, type[0].toUpper()); +} + +QIcon ManagedPackPage::icon() const +{ + return APPLICATION->getThemedIcon(m_inst->getManagedPackType()); +} + +QString ManagedPackPage::helpPage() const +{ + return {}; +} + +void ManagedPackPage::retranslate() +{ + ui->retranslateUi(this); +} + +bool ManagedPackPage::shouldDisplay() const +{ + return m_inst->isManagedPack(); +} + +bool ManagedPackPage::runUpdateTask(InstanceTask* task) +{ + Q_ASSERT(task); + + unique_qobject_ptr<Task> wrapped_task(APPLICATION->instances()->wrapInstanceTask(task)); + + connect(task, &Task::failed, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(task, &Task::succeeded, [this, task]() { + QStringList warnings = task->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + }); + connect(task, &Task::aborted, [this] { + CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) + ->show(); + }); + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(task); + + return task->wasSuccessful(); +} + +void ManagedPackPage::suggestVersion() +{ + ui->updateButton->setText(tr("Update pack")); + ui->updateButton->setDisabled(false); +} + +void ManagedPackPage::setFailState() +{ + qDebug() << "Setting fail state!"; + + // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. + ui->versionsComboBox->blockSignals(true); + ui->versionsComboBox->clear(); + ui->versionsComboBox->addItem(tr("Failed to search for available versions."), {}); + ui->versionsComboBox->blockSignals(false); + + ui->changelogTextBrowser->setText(tr("Failed to request changelog data for this modpack.")); + + ui->updateButton->setText(tr("Cannot update!")); + ui->updateButton->setDisabled(true); + + ui->reloadButton->setVisible(true); +} + +ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) + : ManagedPackPage(inst, instance_window, parent) +{ + Q_ASSERT(inst->isManagedPack()); + connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); + connect(ui->updateButton, &QPushButton::pressed, this, &ModrinthManagedPackPage::update); +} + +// MODRINTH + +void ModrinthManagedPackPage::parseManagedPack() +{ + qDebug() << "Parsing Modrinth pack"; + + // No need for the extra work because we already have everything we need. + if (m_loaded) + return; + + if (m_fetch_job && m_fetch_job->isRunning()) + m_fetch_job->abort(); + + m_fetch_job.reset(new NetJob(QString("Modrinth::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network())); + auto response = std::make_shared<QByteArray>(); + + QString id = m_inst->getManagedPackID(); + + m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response.get())); + + QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + setFailState(); + + return; + } + + try { + Modrinth::loadIndexedVersions(m_pack, doc); + } catch (const JSONValidationError& e) { + qDebug() << *response; + qWarning() << "Error while reading modrinth modpack version: " << e.cause(); + + setFailState(); + return; + } + + // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. + ui->versionsComboBox->blockSignals(true); + ui->versionsComboBox->clear(); + ui->versionsComboBox->blockSignals(false); + + for (auto version : m_pack.versions) { + QString name; + + if (!version.name.contains(version.version)) + name = QString("%1 — %2").arg(version.name, version.version); + else + name = version.name; + + // NOTE: the id from version isn't the same id in the modpack format spec... + // e.g. HexMC's 4.4.0 has versionId 4.0.0 in the modpack index.............. + if (version.version == m_inst->getManagedPackVersionName()) + name.append(tr(" (Current)")); + + ui->versionsComboBox->addItem(name, QVariant(version.id)); + } + + suggestVersion(); + + m_loaded = true; + }); + QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &ModrinthManagedPackPage::setFailState); + QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &ModrinthManagedPackPage::setFailState); + + ui->changelogTextBrowser->setText(tr("Fetching changelogs...")); + + m_fetch_job->start(); +} + +QString ModrinthManagedPackPage::url() const +{ + return "https://modrinth.com/mod/" + m_inst->getManagedPackID(); +} + +void ModrinthManagedPackPage::suggestVersion() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + HoeDown md_parser; + ui->changelogTextBrowser->setHtml(md_parser.process(version.changelog.toUtf8())); + + ManagedPackPage::suggestVersion(); +} + +void ModrinthManagedPackPage::update() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + QMap<QString, QString> extra_info; + // NOTE: Don't use 'm_pack.id' here, since we didn't completely parse all the metadata for the pack, including this field. + extra_info.insert("pack_id", m_inst->getManagedPackID()); + extra_info.insert("pack_version_id", version.id); + extra_info.insert("original_instance_id", m_inst->id()); + + auto extracted = new InstanceImportTask(version.download_url, this, std::move(extra_info)); + + InstanceName inst_name(m_inst->getManagedPackName(), version.version); + inst_name.setName(m_inst->name().replace(m_inst->getManagedPackVersionName(), version.version)); + extracted->setName(inst_name); + + extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + extracted->setIcon(m_inst->iconKey()); + extracted->setConfirmUpdate(false); + + auto did_succeed = runUpdateTask(extracted); + + if (m_instance_window && did_succeed) + m_instance_window->close(); +} + +// FLAME + +FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) + : ManagedPackPage(inst, instance_window, parent) +{ + Q_ASSERT(inst->isManagedPack()); + connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); + connect(ui->updateButton, &QPushButton::pressed, this, &FlameManagedPackPage::update); +} + +void FlameManagedPackPage::parseManagedPack() +{ + qDebug() << "Parsing Flame pack"; + + // We need to tell the user to redownload the pack, since we didn't save the required info previously + if (m_inst->getManagedPackID().isEmpty()) { + setFailState(); + QString message = + tr("<h1>Hey there!</h1>" + "<h4>" + "It seems like your Pack ID is null. This is because of a bug in older versions of the launcher.<br/>" + "Unfortunately, we can't do the proper API requests without this information.<br/>" + "<br/>" + "So, in order for this feature to work, you will need to re-download the modpack from the built-in downloader.<br/>" + "<br/>" + "Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!" + "</h4>"); + + ui->changelogTextBrowser->setHtml(message); + return; + } + + // No need for the extra work because we already have everything we need. + if (m_loaded) + return; + + if (m_fetch_job && m_fetch_job->isRunning()) + m_fetch_job->abort(); + + m_fetch_job.reset(new NetJob(QString("Flame::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network())); + auto response = std::make_shared<QByteArray>(); + + QString id = m_inst->getManagedPackID(); + + m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response.get())); + + QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + setFailState(); + + return; + } + + try { + auto obj = doc.object(); + auto data = Json::ensureArray(obj, "data"); + Flame::loadIndexedPackVersions(m_pack, data); + } catch (const JSONValidationError& e) { + qDebug() << *response; + qWarning() << "Error while reading flame modpack version: " << e.cause(); + + setFailState(); + return; + } + + // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. + ui->versionsComboBox->blockSignals(true); + ui->versionsComboBox->clear(); + ui->versionsComboBox->blockSignals(false); + + for (auto version : m_pack.versions) { + QString name; + + name = version.version; + + if (version.fileId == m_inst->getManagedPackVersionID().toInt()) + name.append(tr(" (Current)")); + + ui->versionsComboBox->addItem(name, QVariant(version.fileId)); + } + + suggestVersion(); + + m_loaded = true; + }); + QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &FlameManagedPackPage::setFailState); + QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &FlameManagedPackPage::setFailState); + + m_fetch_job->start(); +} + +QString FlameManagedPackPage::url() const +{ + // FIXME: We should display the websiteUrl field, but this requires doing the API request first :( + return {}; +} + +void FlameManagedPackPage::suggestVersion() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId)); + + ManagedPackPage::suggestVersion(); +} + +void FlameManagedPackPage::update() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + QMap<QString, QString> extra_info; + extra_info.insert("pack_id", m_inst->getManagedPackID()); + extra_info.insert("pack_version_id", QString::number(version.fileId)); + extra_info.insert("original_instance_id", m_inst->id()); + + auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)); + + extracted->setName(m_inst->name()); + extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + extracted->setIcon(m_inst->iconKey()); + extracted->setConfirmUpdate(false); + + auto did_succeed = runUpdateTask(extracted); + + if (m_instance_window && did_succeed) + m_instance_window->close(); +} + +#include "ManagedPackPage.moc" diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h new file mode 100644 index 00000000..d29a5e88 --- /dev/null +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include "BaseInstance.h" + +#include "modplatform/modrinth/ModrinthAPI.h" +#include "modplatform/modrinth/ModrinthPackManifest.h" + +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlamePackIndex.h" + +#include "ui/pages/BasePage.h" + +#include <QWidget> + +namespace Ui { +class ManagedPackPage; +} + +class InstanceTask; +class InstanceWindow; + +class ManagedPackPage : public QWidget, public BasePage { + Q_OBJECT + + public: + inline static ManagedPackPage* createPage(BaseInstance* inst, QWidget* parent = nullptr) + { + return ManagedPackPage::createPage(inst, inst->getManagedPackType(), parent); + } + + static ManagedPackPage* createPage(BaseInstance* inst, QString type, QWidget* parent = nullptr); + ~ManagedPackPage() override; + + [[nodiscard]] QString displayName() const override; + [[nodiscard]] QIcon icon() const override; + [[nodiscard]] QString helpPage() const override; + [[nodiscard]] QString id() const override { return "managed_pack"; } + [[nodiscard]] bool shouldDisplay() const override; + + void openedImpl() override; + + bool apply() override { return true; } + void retranslate() override; + + /** Gets the necessary information about the managed pack, such as + * available versions*/ + virtual void parseManagedPack(){}; + + /** URL of the managed pack. + * Not the version-specific one. + */ + [[nodiscard]] virtual QString url() const { return {}; }; + + void setInstanceWindow(InstanceWindow* window) { m_instance_window = window; } + + public slots: + /** Gets the current version selection and update the UI, including the update button and the changelog. + */ + virtual void suggestVersion(); + + virtual void update(){}; + + protected slots: + /** Does the necessary UI changes for when something failed. + * + * This includes: + * - Setting an appropriate text on the version selector to indicate a fail; + * - Setting an appropriate text on the changelog text browser to indicate a fail; + * - Disable the update button. + */ + void setFailState(); + + protected: + ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr); + + /** Run the InstanceTask, with a progress dialog and all. + * Similar to MainWindow::instanceFromInstanceTask + * + * Returns whether the task was successful. + */ + bool runUpdateTask(InstanceTask*); + + protected: + InstanceWindow* m_instance_window = nullptr; + + Ui::ManagedPackPage* ui; + BaseInstance* m_inst; + + bool m_loaded = false; +}; + +/** Simple page for when we aren't a managed pack. */ +class GenericManagedPackPage final : public ManagedPackPage { + Q_OBJECT + + public: + GenericManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr) + : ManagedPackPage(inst, instance_window, parent) + {} + ~GenericManagedPackPage() override = default; + + // TODO: We may want to show this page with some useful info at some point. + [[nodiscard]] bool shouldDisplay() const override { return false; }; +}; + +class ModrinthManagedPackPage final : public ManagedPackPage { + Q_OBJECT + + public: + ModrinthManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr); + ~ModrinthManagedPackPage() override = default; + + void parseManagedPack() override; + [[nodiscard]] QString url() const override; + + public slots: + void suggestVersion() override; + + void update() override; + + private: + NetJob::Ptr m_fetch_job = nullptr; + + Modrinth::Modpack m_pack; + ModrinthAPI m_api; +}; + +class FlameManagedPackPage final : public ManagedPackPage { + Q_OBJECT + + public: + FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr); + ~FlameManagedPackPage() override = default; + + void parseManagedPack() override; + [[nodiscard]] QString url() const override; + + public slots: + void suggestVersion() override; + + void update() override; + + private: + NetJob::Ptr m_fetch_job = nullptr; + + Flame::IndexedPack m_pack; + FlameAPI m_api; +}; diff --git a/launcher/ui/pages/instance/ManagedPackPage.ui b/launcher/ui/pages/instance/ManagedPackPage.ui new file mode 100644 index 00000000..bbe44a94 --- /dev/null +++ b/launcher/ui/pages/instance/ManagedPackPage.ui @@ -0,0 +1,193 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ManagedPackPage</class> + <widget class="QWidget" name="ManagedPackPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>731</width> + <height>538</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>9</number> + </property> + <property name="topMargin"> + <number>9</number> + </property> + <property name="rightMargin"> + <number>9</number> + </property> + <property name="bottomMargin"> + <number>9</number> + </property> + <item row="0" column="0"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="packInformationBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Pack information</string> + </property> + <layout class="QFormLayout" name="formLayout_2"> + <item row="0" column="0"> + <layout class="QHBoxLayout" name="packNameLayout"> + <item> + <widget class="QLabel" name="packNameLabel"> + <property name="text"> + <string>Pack name:</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="packName"> + <property name="text"> + <string notr="true">placeholder</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <layout class="QHBoxLayout" name="packVersionLayout"> + <item> + <widget class="QLabel" name="packVersionLabel"> + <property name="text"> + <string>Current version:</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="packVersion"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string notr="true">placeholder</string> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0"> + <layout class="QHBoxLayout" name="packOriginLayout"> + <item> + <widget class="QLabel" name="packOriginLabel"> + <property name="text"> + <string>Provider information:</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="packOrigin"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string notr="true">placeholder</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="updateToVersionLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Update to version:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="versionsComboBox"/> + </item> + <item> + <widget class="QPushButton" name="updateButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Fetching versions...</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QGroupBox" name="changelogBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Changelog</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTextBrowser" name="changelogTextBrowser"> + <property name="placeholderText"> + <string>No changelog available for this version!</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QPushButton" name="reloadButton"> + <property name="text"> + <string>Reload page</string> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index f0106066..0a2e6155 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -108,13 +108,13 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0); }); + connect(m_instance, &BaseInstance::runningStatusChanged, this, &ModFolderPage::runningStateChanged); ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning()); } } void ModFolderPage::runningStateChanged(bool running) { - ExternalResourcesPage::runningStateChanged(running); ui->actionDownloadItem->setEnabled(!running); ui->actionUpdateItem->setEnabled(!running); ui->actionAddItem->setEnabled(!running); diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index c9a55bde..f20adf34 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -53,12 +53,12 @@ class ModFolderPage : public ExternalResourcesPage { virtual QString helpPage() const override { return "Loader-mods"; } virtual bool shouldDisplay() const override; - void runningStateChanged(bool running) override; public slots: bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; private slots: + void runningStateChanged(bool running); void removeItem() override; void installMods(); diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 5e8bd7cc..d64bcb76 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -400,11 +400,11 @@ public: virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override { - return m_servers.size(); + return parent.isValid() ? 0 : m_servers.size(); } int columnCount(const QModelIndex & parent) const override { - return COLUMN_COUNT; + return parent.isValid() ? 0 : COLUMN_COUNT; } Server * at(int index) diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index 37399d49..ee63353a 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -68,7 +68,7 @@ public: } virtual QIcon icon() const override { - return APPLICATION->getThemedIcon("unknown_server"); + return APPLICATION->getThemedIcon("server"); } virtual QString id() const override { diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index a021c633..7f98cba2 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -270,6 +271,7 @@ void VersionPage::updateButtons(int row) ui->actionInstall_mods->setEnabled(controlsEnabled); ui->actionReplace_Minecraft_jar->setEnabled(controlsEnabled); ui->actionAdd_to_Minecraft_jar->setEnabled(controlsEnabled); + ui->actionAdd_Agents->setEnabled(controlsEnabled); } bool VersionPage::reloadPackProfile() @@ -342,6 +344,18 @@ void VersionPage::on_actionReplace_Minecraft_jar_triggered() updateButtons(); } + +void VersionPage::on_actionAdd_Agents_triggered() +{ + QStringList list = GuiUtil::BrowseForFiles("agent", tr("Select agents"), tr("Java agents (*.jar)"), + APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); + + if (!list.isEmpty()) + m_profile->installAgents(list); + + updateButtons(); +} + void VersionPage::on_actionMove_up_triggered() { try diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 979311fc..23d2a1b3 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -82,6 +83,7 @@ private slots: void on_actionMove_down_triggered(); void on_actionAdd_to_Minecraft_jar_triggered(); void on_actionReplace_Minecraft_jar_triggered(); + void on_actionAdd_Agents_triggered(); void on_actionRevert_triggered(); void on_actionEdit_triggered(); void on_actionInstall_mods_triggered(); diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index 14b7cd9f..74b9568a 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -109,6 +109,7 @@ <addaction name="separator"/> <addaction name="actionAdd_to_Minecraft_jar"/> <addaction name="actionReplace_Minecraft_jar"/> + <addaction name="actionAdd_Agents"/> <addaction name="actionAdd_Empty"/> <addaction name="separator"/> <addaction name="actionMinecraftFolder"/> @@ -226,6 +227,14 @@ <string>Replace Minecraft.jar</string> </property> </action> + <action name="actionAdd_Agents"> + <property name="text"> + <string>Add Agents</string> + </property> + <property name="toolTip"> + <string>Add Java agents.</string> + </property> + </action> <action name="actionAdd_Empty"> <property name="text"> <string>Add Empty</string> diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index d2636d87..36840649 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -20,8 +20,8 @@ class ListModel : public QAbstractListModel { ListModel(ModPage* parent); ~ListModel() override; - inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; - inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; + inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); }; + inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; }; inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; auto debugName() const -> QString; @@ -41,12 +41,12 @@ class ListModel : public QAbstractListModel { void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index); virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; - virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {}; + virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); - inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; }; + inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return parent.isValid() ? false : searchState == CanPossiblyFetchMore; }; public slots: void searchRequestFinished(QJsonDocument& doc); diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 234f9f36..677bc4d6 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -262,7 +262,7 @@ void ModPage::openUrl(const QUrl& url) const QString address = url.host() + url.path(); QRegularExpressionMatch match; - const char* page; + QString page; match = modrinth.match(address); if (match.hasMatch()) @@ -276,7 +276,7 @@ void ModPage::openUrl(const QUrl& url) page = "curseforge"; } - if (match.hasMatch()) { + if (!page.isNull()) { const QString slug = match.captured(1); // ensure the user isn't opening the same mod diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index ef9a9268..2ce04068 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -32,12 +32,12 @@ ListModel::~ListModel() int ListModel::rowCount(const QModelIndex &parent) const { - return modpacks.size(); + return parent.isValid() ? 0 : modpacks.size(); } int ListModel::columnCount(const QModelIndex &parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QVariant ListModel::data(const QModelIndex &index, int role) const diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 9138dcbb..cdb4532c 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -75,12 +75,12 @@ QVector<QString> AtlOptionalModListModel::getResult() { } int AtlOptionalModListModel::rowCount(const QModelIndex &parent) const { - return m_mods.size(); + return parent.isValid() ? 0 : m_mods.size(); } int AtlOptionalModListModel::columnCount(const QModelIndex &parent) const { // Enabled, Name, Description - return 3; + return parent.isValid() ? 0 : 3; } QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const { diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index debae8c3..127c3de5 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -15,12 +15,12 @@ ListModel::~ListModel() {} int ListModel::rowCount(const QModelIndex& parent) const { - return modpacks.size(); + return parent.isValid() ? 0 : modpacks.size(); } int ListModel::columnCount(const QModelIndex& parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QVariant ListModel::data(const QModelIndex& index, int role) const diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index a65b6585..f9ac4a78 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -197,12 +197,18 @@ void FlamePage::suggestCurrent() return; } - if (selectedVersion.isEmpty() || selectedVersion == "-1") { + if (m_selected_version_index == -1) { dialog->setSuggestedPack(); return; } - dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this)); + auto version = current.versions.at(m_selected_version_index); + + QMap<QString, QString> extra_info; + extra_info.insert("pack_id", QString::number(current.addonId)); + extra_info.insert("pack_version_id", QString::number(version.fileId)); + + dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info))); QString editedLogoName; editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); listModel->getLogo(current.logoName, current.logoUrl, @@ -211,11 +217,18 @@ void FlamePage::suggestCurrent() void FlamePage::onVersionSelectionChanged(QString data) { - if (data.isNull() || data.isEmpty()) { - selectedVersion = ""; + bool is_blocked = false; + ui->versionSelectionBox->currentData().toInt(&is_blocked); + + if (data.isNull() || data.isEmpty() || is_blocked) { + m_selected_version_index = -1; return; } - selectedVersion = ui->versionSelectionBox->currentData().toString(); + + m_selected_version_index = ui->versionSelectionBox->currentIndex(); + + Q_ASSERT(current.versions.at(m_selected_version_index).downloadUrl == ui->versionSelectionBox->currentData().toString()); + suggestCurrent(); } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 8130e416..8bdca38e 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -99,5 +99,5 @@ private: Flame::ListModel* listModel = nullptr; Flame::IndexedPack current; - QString selectedVersion; + int m_selected_version_index = -1; }; diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp index 3a149944..ce2b2b18 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp @@ -34,12 +34,12 @@ ListModel::~ListModel() int ListModel::rowCount(const QModelIndex &parent) const { - return modpacks.size(); + return parent.isValid() ? 0 : modpacks.size(); } int ListModel::columnCount(const QModelIndex &parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QVariant ListModel::data(const QModelIndex &index, int role) const diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 6f11cc95..6b1f6b89 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -125,12 +125,12 @@ QString ListModel::translatePackType(PackType type) const int ListModel::rowCount(const QModelIndex &parent) const { - return modpacks.size(); + return parent.isValid() ? 0 : modpacks.size(); } int ListModel::columnCount(const QModelIndex &parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QVariant ListModel::data(const QModelIndex &index, int role) const diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 6f33e11e..3be377a1 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -55,8 +55,8 @@ class ModpackListModel : public QAbstractListModel { ModpackListModel(ModrinthPage* parent); ~ModpackListModel() override = default; - inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; - inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; + inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); }; + inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; }; inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; auto debugName() const -> QString; @@ -74,7 +74,7 @@ class ModpackListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); - inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; }; + inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return parent.isValid() ? false : searchState == CanPossiblyFetchMore; }; public slots: void searchRequestFinished(QJsonDocument& doc_all); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 4482774c..8ab2ad1d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -300,7 +300,11 @@ void ModrinthPage::suggestCurrent() for (auto& ver : current.versions) { if (ver.id == selectedVersion) { - dialog->setSuggestedPack(current.name, ver.version, new InstanceImportTask(ver.download_url, this)); + QMap<QString, QString> extra_info; + extra_info.insert("pack_id", current.id); + extra_info.insert("pack_version_id", ver.id); + + dialog->setSuggestedPack(current.name, ver.version, new InstanceImportTask(ver.download_url, this, std::move(extra_info))); auto iconName = current.iconName; m_model->getLogo(iconName, current.iconUrl.toString(), [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 742f4f2a..b2af1ac0 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -80,14 +80,14 @@ QVariant Technic::ListModel::data(const QModelIndex& index, int role) const return QVariant(); } -int Technic::ListModel::columnCount(const QModelIndex&) const +int Technic::ListModel::columnCount(const QModelIndex& parent) const { - return 1; + return parent.isValid() ? 0 : 1; } -int Technic::ListModel::rowCount(const QModelIndex&) const +int Technic::ListModel::rowCount(const QModelIndex& parent) const { - return modpacks.size(); + return parent.isValid() ? 0 : modpacks.size(); } void Technic::ListModel::searchWithTerm(const QString& term) diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index c7c4dbbd..15994319 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -71,6 +71,7 @@ void JavaSettingsWidget::setupUi() m_memoryGroupBox->setObjectName(QStringLiteral("memoryGroupBox")); m_gridLayout_2 = new QGridLayout(m_memoryGroupBox); m_gridLayout_2->setObjectName(QStringLiteral("gridLayout_2")); + m_gridLayout_2->setColumnStretch(0, 1); m_labelMinMem = new QLabel(m_memoryGroupBox); m_labelMinMem->setObjectName(QStringLiteral("labelMinMem")); @@ -80,7 +81,7 @@ void JavaSettingsWidget::setupUi() m_minMemSpinBox->setObjectName(QStringLiteral("minMemSpinBox")); m_minMemSpinBox->setSuffix(QStringLiteral(" MiB")); m_minMemSpinBox->setMinimum(128); - m_minMemSpinBox->setMaximum(m_availableMemory); + m_minMemSpinBox->setMaximum(1048576); m_minMemSpinBox->setSingleStep(128); m_labelMinMem->setBuddy(m_minMemSpinBox); m_gridLayout_2->addWidget(m_minMemSpinBox, 0, 1, 1, 1); @@ -93,11 +94,15 @@ void JavaSettingsWidget::setupUi() m_maxMemSpinBox->setObjectName(QStringLiteral("maxMemSpinBox")); m_maxMemSpinBox->setSuffix(QStringLiteral(" MiB")); m_maxMemSpinBox->setMinimum(128); - m_maxMemSpinBox->setMaximum(m_availableMemory); + m_maxMemSpinBox->setMaximum(1048576); m_maxMemSpinBox->setSingleStep(128); m_labelMaxMem->setBuddy(m_maxMemSpinBox); m_gridLayout_2->addWidget(m_maxMemSpinBox, 1, 1, 1, 1); + m_labelMaxMemIcon = new QLabel(m_memoryGroupBox); + m_labelMaxMemIcon->setObjectName(QStringLiteral("labelMaxMemIcon")); + m_gridLayout_2->addWidget(m_labelMaxMemIcon, 1, 2, 1, 1); + m_labelPermGen = new QLabel(m_memoryGroupBox); m_labelPermGen->setObjectName(QStringLiteral("labelPermGen")); m_labelPermGen->setText(QStringLiteral("PermGen:")); @@ -108,7 +113,7 @@ void JavaSettingsWidget::setupUi() m_permGenSpinBox->setObjectName(QStringLiteral("permGenSpinBox")); m_permGenSpinBox->setSuffix(QStringLiteral(" MiB")); m_permGenSpinBox->setMinimum(64); - m_permGenSpinBox->setMaximum(m_availableMemory); + m_permGenSpinBox->setMaximum(1048576); m_permGenSpinBox->setSingleStep(8); m_gridLayout_2->addWidget(m_permGenSpinBox, 2, 1, 1, 1); m_permGenSpinBox->setVisible(false); @@ -130,6 +135,7 @@ void JavaSettingsWidget::initialize() m_minMemSpinBox->setValue(observedMinMemory); m_maxMemSpinBox->setValue(observedMaxMemory); m_permGenSpinBox->setValue(observedPermGenMemory); + updateThresholds(); } void JavaSettingsWidget::refresh() @@ -210,9 +216,9 @@ int JavaSettingsWidget::permGenSize() const void JavaSettingsWidget::memoryValueChanged(int) { bool actuallyChanged = false; - int min = m_minMemSpinBox->value(); - int max = m_maxMemSpinBox->value(); - int permgen = m_permGenSpinBox->value(); + unsigned int min = m_minMemSpinBox->value(); + unsigned int max = m_maxMemSpinBox->value(); + unsigned int permgen = m_permGenSpinBox->value(); QObject *obj = sender(); if (obj == m_minMemSpinBox && min != observedMinMemory) { @@ -242,6 +248,7 @@ void JavaSettingsWidget::memoryValueChanged(int) if(actuallyChanged) { checkJavaPathOnEdit(m_javaPathTextBox->text()); + updateThresholds(); } } @@ -435,3 +442,26 @@ void JavaSettingsWidget::retranslate() m_permGenSpinBox->setToolTip(tr("The amount of memory available to store loaded Java classes.")); m_javaBrowseBtn->setText(tr("Browse")); } + +void JavaSettingsWidget::updateThresholds() +{ + QString iconName; + + if (observedMaxMemory >= m_availableMemory) { + iconName = "status-bad"; + m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); + } else if (observedMaxMemory > (m_availableMemory * 0.9)) { + iconName = "status-yellow"; + m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else { + iconName = "status-good"; + m_labelMaxMemIcon->setToolTip(""); + } + + { + auto height = m_labelMaxMemIcon->fontInfo().pixelSize(); + QIcon icon = APPLICATION->getThemedIcon(iconName); + QPixmap pix = icon.pixmap(height, height); + m_labelMaxMemIcon->setPixmap(pix); + } +} diff --git a/launcher/ui/widgets/JavaSettingsWidget.h b/launcher/ui/widgets/JavaSettingsWidget.h index 5344e2cd..e4b7c712 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.h +++ b/launcher/ui/widgets/JavaSettingsWidget.h @@ -56,6 +56,8 @@ public: int maxHeapSize() const; QString javaPath() const; + void updateThresholds(); + protected slots: void memoryValueChanged(int); @@ -85,6 +87,7 @@ private: /* data */ QSpinBox *m_maxMemSpinBox = nullptr; QLabel *m_labelMinMem = nullptr; QLabel *m_labelMaxMem = nullptr; + QLabel *m_labelMaxMemIcon = nullptr; QSpinBox *m_minMemSpinBox = nullptr; QLabel *m_labelPermGen = nullptr; QSpinBox *m_permGenSpinBox = nullptr; @@ -92,9 +95,9 @@ private: /* data */ QIcon yellowIcon; QIcon badIcon; - int observedMinMemory = 0; - int observedMaxMemory = 0; - int observedPermGenMemory = 0; + unsigned int observedMinMemory = 0; + unsigned int observedMaxMemory = 0; + unsigned int observedPermGenMemory = 0; QString queuedCheck; uint64_t m_availableMemory = 0ull; shared_qobject_ptr<JavaChecker> m_checker; diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 8d606820..0a06a351 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -130,6 +130,11 @@ bool PageContainer::selectPage(QString pageId) return false; } +BasePage* PageContainer::getPage(QString pageId) +{ + return m_model->findPageEntryById(pageId); +} + void PageContainer::refreshContainer() { m_proxyModel->invalidate(); diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index 80d87a9b..97e294dc 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -79,6 +79,7 @@ public: } virtual bool selectPage(QString pageId) override; + BasePage* getPage(QString pageId) override; void refreshContainer() override; virtual void setParentContainer(BasePageContainer * container) |