diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 42343ff8ff..3e3e1f2654 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -59,6 +59,7 @@ #include "ui/pages/global/AccountListPage.h" #include "ui/pages/global/CustomCommandsPage.h" #include "ui/pages/global/EnvironmentVariablesPage.h" +#include "ui/pages/global/ExternalInstancePage.h" #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" @@ -237,7 +238,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { { "a", "profile" }, "Use the account specified by its profile name (only valid in combination with --launch)", "profile" }, { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" }, { { "I", "import" }, "Import instance or resource from specified local path or URL", "url" }, - { "show", "Opens the window for the specified instance (by instance ID)", "show" } }); + { "show", "Opens the window for the specified instance (by instance ID)", "show" }, + { "settings", "Override the configuration entry in the configuration file. Usage:--settings key1=value1&key2=value2...", + "settings" } }); // Has to be positional for some OS to handle that properly parser.addPositionalArgument("URL", "Import the resource(s) at the given URL(s) (same as -I / --import)", "[URL...]"); @@ -253,6 +256,19 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_instanceIdToShowWindowOf = parser.value("show"); + for (const auto& setting1 : parser.values("settings")) { + for (const auto& setting2 : setting1.split("&")) { + auto index = setting2.indexOf('='); + if (index != -1) { + QString key = setting2.left(index); + QString value = setting2.right(setting2.length() - index - 1); + m_settingsOverride.append(QPair(key, value)); + } else { + std::cerr << "Error format, please provide as key=value." << std::endl; + } + } + } + for (auto url : parser.values("import")) { m_urlsToImport.append(normalizeImportUrl(url)); } @@ -297,23 +313,30 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) adjustedBy = "Command line"; dataPath = dirParam; } else { - QDir foo; - if (DesktopServices::isSnap()) { - foo = QDir(getenv("SNAP_USER_COMMON")); + auto varName = (BuildConfig.LAUNCHER_NAME.toUpper() + "_COMMON_DIR"); + auto env = QString(qEnvironmentVariable(varName.toUtf8().constData())); + if (!env.isEmpty() && FS::ensureFolderPathExists(env)) { + dataPath = QDir(env).absolutePath(); + adjustedBy = "Environment variable" + varName; } else { - foo = QDir(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); - } + QDir foo; + if (DesktopServices::isSnap()) { + foo = QDir(getenv("SNAP_USER_COMMON")); + } else { + foo = QDir(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); + } - dataPath = foo.absolutePath(); - adjustedBy = "Persistent data path"; + dataPath = foo.absolutePath(); + adjustedBy = "Persistent data path"; #ifndef Q_OS_MACOS - if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { - dataPath = m_rootPath; - adjustedBy = "Portable data path"; - m_portable = true; - } + if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { + dataPath = m_rootPath; + adjustedBy = "Portable data path"; + m_portable = true; + } #endif + } } if (!FS::ensureFolderPathExists(dataPath)) { @@ -754,10 +777,27 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // FTBApp instances m_settings->registerSetting("FTBAppInstancesPath", ""); + // The command line sets the override + { + for (auto& setting1 : m_settingsOverride) { + if (m_settings->contains(setting1.first)) { + auto setting2 = m_settings->get(setting1.first); + if (setting2.isValid()) { + qDebug() << "The <> of setting option has an override option from the command line, <> -> <>" << setting1.first + << setting2 << setting1.second; + m_settings->registerConstant(setting1.first, setting1.second); + } else { + qDebug() << "The <> of setting option does not exist. skip." << setting1.first; + } + } + } + } + // Init page provider { m_globalSettingsProvider = std::make_shared(tr("Settings")); m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); diff --git a/launcher/Application.h b/launcher/Application.h index 7669e08ec3..deb904a9cc 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -292,6 +292,7 @@ class Application : public QApplication { QString m_profileToUse; bool m_liveCheck = false; QList m_urlsToImport; + QList> m_settingsOverride; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; }; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 99acf8fc57..e372a40de4 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -413,6 +413,8 @@ set(SETTINGS_SOURCES settings/OverrideSetting.h settings/PassthroughSetting.cpp settings/PassthroughSetting.h + settings/ConstantSetting.cpp + settings/ConstantSetting.h settings/Setting.cpp settings/Setting.h settings/SettingsObject.cpp @@ -887,6 +889,8 @@ SET(LAUNCHER_SOURCES # GUI - global settings pages ui/pages/global/AccountListPage.cpp ui/pages/global/AccountListPage.h + ui/pages/global/ExternalInstancePage.cpp + ui/pages/global/ExternalInstancePage.h ui/pages/global/CustomCommandsPage.cpp ui/pages/global/CustomCommandsPage.h ui/pages/global/EnvironmentVariablesPage.cpp @@ -1124,6 +1128,7 @@ qt_wrap_ui(LAUNCHER_UI ui/setupwizard/PasteWizardPage.ui ui/setupwizard/ThemeWizardPage.ui ui/pages/global/AccountListPage.ui + ui/pages/global/ExternalInstancePage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui ui/pages/global/APIPage.ui diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index c884a4f12d..d498c0a1d4 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -66,6 +66,7 @@ #endif const static int GROUP_FILE_FORMAT_VERSION = 1; +const static int EXT_INST_FILE_FORMAT_VERSION = 1; InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent) : QAbstractListModel(parent), m_globalSettings(settings) @@ -79,10 +80,10 @@ InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, Q connect(this, &InstanceList::instancesChanged, this, &InstanceList::providerUpdated); // NOTE: canonicalPath requires the path to exist. Do not move this above the creation block! - m_instDir = QDir(instDir).canonicalPath(); + m_instRootDir = QDir(instDir).canonicalPath(); m_watcher = new QFileSystemWatcher(this); connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &InstanceList::instanceDirContentsChanged); - m_watcher->addPath(m_instDir); + m_watcher->addPath(m_instRootDir); } InstanceList::~InstanceList() {} @@ -425,11 +426,11 @@ static QMap getIdMapping(const QList& return out; } -QList InstanceList::discoverInstances() +QList discoverInstancesFormDir(QString& path) { - qDebug() << "Discovering instances in" << m_instDir; + qDebug() << "Discovering instances in" << path; QList out; - QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); + QDirIterator iter(path, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); while (iter.hasNext()) { QString subDir = iter.next(); QFileInfo dirInfo(subDir); @@ -438,7 +439,7 @@ QList InstanceList::discoverInstances() // if it is a symlink, ignore it if it goes to the instance folder if (dirInfo.isSymLink()) { QFileInfo targetInfo(dirInfo.symLinkTarget()); - QFileInfo instDirInfo(m_instDir); + QFileInfo instDirInfo(path); if (targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) { qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder"; continue; @@ -448,33 +449,140 @@ QList InstanceList::discoverInstances() out.append(id); qDebug() << "Found instance ID" << id; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - instanceSet = QSet(out.begin(), out.end()); -#else - instanceSet = out.toSet(); -#endif - m_instancesProbed = true; return out; } +void InstanceList::saveExtInstDir() +{ + qDebug() << "Will save external instance directory now."; + + WatchLock foo(m_watcher, m_instRootDir); + QString extInstDirFileName = m_instRootDir + "/extinstdir.json"; + + QJsonObject jsonObject; + jsonObject.insert("formatVersion", QJsonValue(QString("1"))); + + QJsonArray extArray; + for (auto& string : m_extInstDir) { + if (!QFileInfo(string).isDir()) { + qWarning() << "Skip" << string << "because it is not a Directory."; + continue; + } + extArray.append(string); + } + + jsonObject.insert("ext", extArray); + QJsonDocument doc(jsonObject); + try { + FS::write(extInstDirFileName, doc.toJson()); + qDebug() << "External instance directory saved."; + } catch (const FS::FileSystemException& e) { + qCritical() << "Failed to write external instance directory file :" << e.cause(); + } +} + +void InstanceList::loadExtInstDir() +{ + qDebug() << "Will load external instance directory now."; + + QString extInstDirFileName = m_instRootDir + "/extinstdir.json"; + + if (!QFileInfo(extInstDirFileName).exists()) + return; + + QByteArray jsonData; + try { + jsonData = FS::read(extInstDirFileName); + } catch (const FS::FileSystemException& e) { + qCritical() << "Failed to read external instance directory file :" << e.cause(); + return; + } + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error); + + // if the json was bad, fail + if (error.error != QJsonParseError::NoError) { + qCritical() << QString("Failed to parse external instance directory file: %1 at offset %2") + .arg(error.errorString(), QString::number(error.offset)) + .toUtf8(); + return; + } + + // if the root of the json wasn't an object, fail + if (!jsonDoc.isObject()) { + qWarning() << "Invalid external instance directory file. Root entry should be an object."; + return; + } + + QJsonObject rootObj = jsonDoc.object(); + + // Make sure the format version matches, otherwise fail. + if (rootObj.value("formatVersion").toVariant().toInt() != EXT_INST_FILE_FORMAT_VERSION) + return; + + // Get the ext instance dir. if it's not an object, fail + if (!rootObj.value("ext").isArray()) { + qWarning() << "Invalid external instance directory JSON: 'ext' should be an object."; + return; + } + + m_extInstDir.clear(); + + QJsonArray extArray = rootObj.value("ext").toArray(); + for (auto value : extArray) { + auto string = value.toString(); + if (!QFileInfo(string).isDir()) { + qWarning() << "Skip" << string << "because it is not a Directory."; + continue; + } + m_extInstDir.append(string); + } + m_extInstDirLoaded = true; + qDebug() << "External instance directory loaded."; +} + InstanceList::InstListError InstanceList::loadList() { auto existingIds = getIdMapping(m_instances); QList newList; - for (auto& id : discoverInstances()) { - if (existingIds.contains(id)) { - auto instPair = existingIds[id]; - existingIds.remove(id); - qDebug() << "Should keep and soft-reload" << id; - } else { - InstancePtr instPtr = loadInstance(id); - if (instPtr) { - newList.append(instPtr); + if (!m_extInstDirLoaded) { + loadExtInstDir(); + } + QList allInstDir; + allInstDir.append(m_instRootDir); + allInstDir.append(m_extInstDir); + + QList allInst; + for (auto& path : allInstDir) { + auto list = discoverInstancesFormDir(path); + for (auto& id : list) { + if (allInst.contains(id)) { + qWarning() << "The" << id << "of the same name already exists in the previous directory, so this instance of" << path + << "is skipped."; + continue; + } + allInst.append(id); + if (existingIds.contains(id)) { + auto instPair = existingIds[id]; + existingIds.remove(id); + qDebug() << "Should keep and soft-reload" << id; + } else { + InstancePtr instPtr = loadInstance(id, path); + if (instPtr) { + newList.append(instPtr); + } } } } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + instanceSet = QSet(allInst.begin(), allInst.end()); +#else + instanceSet = allInst.toSet(); +#endif + m_instancesProbed = true; // TODO: looks like a general algorithm with a few specifics inserted. Do something about it. if (!existingIds.isEmpty()) { @@ -620,13 +728,13 @@ void InstanceList::propertiesChanged(BaseInstance* inst) } } -InstancePtr InstanceList::loadInstance(const InstanceId& id) +InstancePtr InstanceList::loadInstance(const InstanceId& id, const QString& instDir) { if (!m_groupsLoaded) { loadGroupList(); } - auto instanceRoot = FS::PathCombine(m_instDir, id); + auto instanceRoot = FS::PathCombine(instDir, id); auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); InstancePtr inst; @@ -671,8 +779,8 @@ void InstanceList::saveGroupList() qDebug() << "Group saving prevented because we don't know the full list of instances yet."; return; } - WatchLock foo(m_watcher, m_instDir); - QString groupFileName = m_instDir + "/instgroups.json"; + WatchLock foo(m_watcher, m_instRootDir); + QString groupFileName = m_instRootDir + "/instgroups.json"; QMap> reverseGroupMap; for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) { const QString& id = iter.key(); @@ -722,7 +830,7 @@ void InstanceList::loadGroupList() { qDebug() << "Will load group list now."; - QString groupFileName = m_instDir + "/instgroups.json"; + QString groupFileName = m_instRootDir + "/instgroups.json"; // if there's no group file, fail if (!QFileInfo(groupFileName).exists()) @@ -817,15 +925,22 @@ void InstanceList::instanceDirContentsChanged(const QString& path) void InstanceList::on_InstFolderChanged([[maybe_unused]] const Setting& setting, QVariant value) { QString newInstDir = QDir(value.toString()).canonicalPath(); - if (newInstDir != m_instDir) { + if (newInstDir != m_instRootDir) { if (m_groupsLoaded) { saveGroupList(); + m_groupsLoaded = false; + } + if (m_extInstDirLoaded) { + saveExtInstDir(); + m_extInstDirLoaded = false; + } + m_instRootDir = newInstDir; + + if (count() > 0) { + beginRemoveRows(QModelIndex(), 0, count() - 1); + m_instances.erase(m_instances.begin(), m_instances.end()); + endRemoveRows(); } - m_instDir = newInstDir; - m_groupsLoaded = false; - beginRemoveRows(QModelIndex(), 0, count()); - m_instances.erase(m_instances.begin(), m_instances.end()); - endRemoveRows(); emit instancesChanged(); } } @@ -936,13 +1051,13 @@ QString InstanceList::getStagedInstancePath() QString key = QUuid::createUuid().toString(QUuid::WithoutBraces); QString tempDir = ".LAUNCHER_TEMP/"; QString relPath = FS::PathCombine(tempDir, key); - QDir rootPath(m_instDir); - auto path = FS::PathCombine(m_instDir, relPath); + QDir rootPath(m_instRootDir); + auto path = FS::PathCombine(m_instRootDir, relPath); if (!rootPath.mkpath(relPath)) { return QString(); } #ifdef Q_OS_WIN32 - auto tempPath = FS::PathCombine(m_instDir, tempDir); + auto tempPath = FS::PathCombine(m_instRootDir, tempDir); SetFileAttributesA(tempPath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); #endif return path; @@ -965,14 +1080,14 @@ bool InstanceList::commitStagedInstance(const QString& path, if (should_override) { instID = commiting.originalInstanceID(); } else { - instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir); + instID = FS::DirNameFromString(instanceName.modifiedName(), m_instRootDir); } Q_ASSERT(!instID.isEmpty()); { - WatchLock lock(m_watcher, m_instDir); - QString destination = FS::PathCombine(m_instDir, instID); + WatchLock lock(m_watcher, m_instRootDir); + QString destination = FS::PathCombine(m_instRootDir, instID); if (should_override) { if (!FS::overrideFolder(destination, path)) { @@ -1009,5 +1124,18 @@ int InstanceList::getTotalPlayTime() updateTotalPlayTime(); return totalPlayTime; } +QStringList InstanceList::getExtInstDir() +{ + if (!m_extInstDirLoaded) + loadExtInstDir(); + return m_extInstDir; +} +void InstanceList::setExtInstDir(const QStringList& newValue) +{ + if (m_extInstDir == newValue) + return; + m_extInstDir = newValue; + saveExtInstDir(); +} #include "InstanceList.moc" diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index 5ddddee95d..96c14f390b 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -94,6 +94,9 @@ class InstanceList : public QAbstractListModel { int count() const { return m_instances.count(); } + QStringList getExtInstDir(); + void setExtInstDir(const QStringList& newValue); + InstListError loadList(); void saveNow(); @@ -176,8 +179,9 @@ class InstanceList : public QAbstractListModel { void add(const QList& list); void loadGroupList(); void saveGroupList(); - QList discoverInstances(); - InstancePtr loadInstance(const InstanceId& id); + void loadExtInstDir(); + void saveExtInstDir(); + InstancePtr loadInstance(const InstanceId& id, const QString& instDir); void increaseGroupCount(const QString& group); void decreaseGroupCount(const QString& group); @@ -191,7 +195,7 @@ class InstanceList : public QAbstractListModel { QMap m_groupNameCache; SettingsObjectPtr m_globalSettings; - QString m_instDir; + QString m_instRootDir; QFileSystemWatcher* m_watcher; // FIXME: this is so inefficient that looking at it is almost painful. QSet m_collapsedGroups; @@ -201,4 +205,6 @@ class InstanceList : public QAbstractListModel { bool m_instancesProbed = false; QStack m_trashHistory; + QStringList m_extInstDir; + bool m_extInstDirLoaded = false; }; diff --git a/launcher/settings/ConstantSetting.cpp b/launcher/settings/ConstantSetting.cpp new file mode 100644 index 0000000000..e80479d558 --- /dev/null +++ b/launcher/settings/ConstantSetting.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 初夏同学 <2411829240@qq.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 . + */ + +#include "ConstantSetting.h" + +#include +ConstantSetting::ConstantSetting(std::shared_ptr origin, QVariant defVal) : Setting(origin->configKeys(), std::move(defVal)) +{ + m_other = origin; +} +QVariant ConstantSetting::get() const +{ + return m_defVal; +} +void ConstantSetting::set(QVariant value) {} +void ConstantSetting::reset() {} diff --git a/launcher/settings/ConstantSetting.h b/launcher/settings/ConstantSetting.h new file mode 100644 index 0000000000..63bddd3940 --- /dev/null +++ b/launcher/settings/ConstantSetting.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 初夏同学 <2411829240@qq.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 . + */ + +#pragma once + +#include "Setting.h" + +/*! + * \brief A setting used for command line. It cannot be modified or saved. + */ +class ConstantSetting : public Setting { + Q_OBJECT + public: + explicit ConstantSetting(std::shared_ptr origin, QVariant defVal = QVariant()); + + virtual QVariant get() const; + virtual void set(QVariant value); + virtual void reset(); + + protected: + std::shared_ptr m_other; +}; diff --git a/launcher/settings/SettingsObject.cpp b/launcher/settings/SettingsObject.cpp index 1e5dce251e..526c576ee6 100644 --- a/launcher/settings/SettingsObject.cpp +++ b/launcher/settings/SettingsObject.cpp @@ -15,6 +15,7 @@ #include "settings/SettingsObject.h" #include +#include "ConstantSetting.h" #include "PassthroughSetting.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" @@ -54,6 +55,19 @@ std::shared_ptr SettingsObject::registerPassthrough(std::shared_ptr SettingsObject::registerConstant(const QString& id, QVariant val) +{ + if (!contains(id)) { + qCritical() << QString("Failed to register setting %1. ID does not exists.").arg(id); + return nullptr; // Fail + } + auto constant = std::make_shared(m_settings[id], val); + constant->m_storage = this; + // connectSignals(*constant); // Don't need it + m_settings[id] = constant; + return constant; +} + std::shared_ptr SettingsObject::registerSetting(QStringList synonyms, QVariant defVal) { if (synonyms.empty()) diff --git a/launcher/settings/SettingsObject.h b/launcher/settings/SettingsObject.h index f133f2f7fc..94d759be3f 100644 --- a/launcher/settings/SettingsObject.h +++ b/launcher/settings/SettingsObject.h @@ -76,6 +76,14 @@ class SettingsObject : public QObject { */ std::shared_ptr registerPassthrough(std::shared_ptr original, std::shared_ptr gate); + /*! + * Register a constant setting. Only Settings that are already in place can be overridden. + * This setting will not be saved to a file. No changes will be made to the original + * Settings (including those stored in files). + * \return A valid Setting shared pointer if successful. + */ + std::shared_ptr registerConstant(const QString& id, QVariant val); + /*! * Registers the given setting with this SettingsObject and connects the necessary signals. * diff --git a/launcher/ui/pages/global/ExternalInstancePage.cpp b/launcher/ui/pages/global/ExternalInstancePage.cpp new file mode 100644 index 0000000000..5fafe4d256 --- /dev/null +++ b/launcher/ui/pages/global/ExternalInstancePage.cpp @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 初夏同学 <2411829240@qq.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 . + */ + +#include "ExternalInstancePage.h" +#include "ui_ExternalInstancePage.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "BuildConfig.h" +#include "DesktopServices.h" +#include "FileSystem.h" +#include "InstanceList.h" +class EditWidget : public QWidget { + Q_OBJECT + public: + explicit EditWidget(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()) : QWidget(parent, f) + { + auto* layout = new QHBoxLayout(this); + m_lineEdit = new QLineEdit(this); + m_lineEdit->installEventFilter(const_cast(this)); + auto* pushButton = new QPushButton(tr("Open"), this); + connect(pushButton, &QPushButton::clicked, [=]() { + QString raw_dir = QFileDialog::getExistingDirectory(pushButton, tr("External Instance Folder"), m_lineEdit->text()); + if (!raw_dir.isEmpty()) + m_lineEdit->setText(raw_dir); + }); + + layout->addWidget(m_lineEdit); + layout->addWidget(pushButton); + + layout->setContentsMargins(0, 0, 0, 0); + this->setLayout(layout); + }; + + ~EditWidget() override = default; + bool eventFilter(QObject* obj, QEvent* event) override + { + if (event->type() == QEvent::KeyPress) { + auto* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { + emit editDone(); + return true; + } + } + return false; + } + + inline void setData(const QString& data) { m_lineEdit->setText(data); } + inline QString getData() { return m_lineEdit->text(); } + + signals: + void editDone(); + + private: + QLineEdit* m_lineEdit; +}; + +class FolderButtonDelegate : public QStyledItemDelegate { + Q_OBJECT + public: + explicit FolderButtonDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) {} + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override + { + Q_UNUSED(index); + Q_UNUSED(option); + auto* editorWidget = new EditWidget(parent); + connect(editorWidget, &EditWidget::editDone, this, &FolderButtonDelegate::editDone); + + return editorWidget; + } + + void setEditorData(QWidget* editor, const QModelIndex& index) const override + { + auto text = index.data(Qt::EditRole).toString(); + qobject_cast(editor)->setData(text); + } + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override + { + auto text = qobject_cast(editor)->getData(); + if (ExternalInstancePage::verifyInstDirPath(text)) { + QString cooked_dir = FS::NormalizePath(text); + if (!dynamic_cast(model)->stringList().contains(cooked_dir)) + model->setData(index, cooked_dir, Qt::EditRole); + } + } + void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const override + { + Q_UNUSED(index); + editor->setGeometry(option.rect); + } + + public slots: + void editDone() + { + auto* editor = qobject_cast(sender()); + emit commitData(editor); + emit closeEditor(editor); + }; +}; + +ExternalInstancePage::ExternalInstancePage(QWidget* parent) : QMainWindow(parent), ui(new Ui::ExternalInstancePage) +{ + ui->setupUi(this); + ui->listView->setEmptyString( + tr("Welcome!\n" + "You can add external instance directories here. (The directory where the instance folder is stored, not the instance folder " + "itself.)")); + ui->listView->setEmptyMode(VersionListView::String); + ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); + ui->listView->header()->setSectionResizeMode(QHeaderView::Stretch); + ui->listView->setHeaderHidden(true); + ui->listView->setEditTriggers(QAbstractItemView::DoubleClicked); + + m_model = new QStringListModel(this); + + ui->listView->setModel(m_model); + + ui->listView->setItemDelegate(new FolderButtonDelegate(ui->listView)); + connect(ui->listView, &VersionListView::customContextMenuRequested, this, &ExternalInstancePage::ShowContextMenu); +} + +ExternalInstancePage::~ExternalInstancePage() +{ + delete ui; +} + +void ExternalInstancePage::retranslate() +{ + ui->retranslateUi(this); +} + +void ExternalInstancePage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->listView->mapToGlobal(pos)); + delete menu; +} + +void ExternalInstancePage::changeEvent(QEvent* event) +{ + if (event->type() == QEvent::LanguageChange) { + ui->retranslateUi(this); + } + QMainWindow::changeEvent(event); +} + +QMenu* ExternalInstancePage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction(ui->toolBar->toggleViewAction()); + return filteredMenu; +} + +inline bool ExternalInstancePage::verifyInstDirPath(const QString& raw_dir) +{ + bool result = false; + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { + QString cooked_dir = FS::NormalizePath(raw_dir); + if (FS::checkProblemticPathJava(QDir(cooked_dir))) { + QMessageBox warning; + warning.setText( + tr("You're trying to specify an instance folder which\'s path " + "contains at least one \'!\'. " + "Java is known to cause problems if that is the case, your " + "instances (probably) won't start!")); + warning.setInformativeText( + tr("Do you really want to use this path? " + "Selecting \"No\" will close this and not alter your instance path.")); + warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + int result1 = warning.exec(); + if (result1 == QMessageBox::Ok) { + result = true; + } + } else if (APPLICATION->settings()->get("InstanceDir").toString() == cooked_dir) { + QMessageBox warning; + warning.setText( + tr("The external instance directory cannot be set to the root directory!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + warning.setStandardButtons(QMessageBox::Cancel); + warning.exec(); + } else if (DesktopServices::isFlatpak() && raw_dir.startsWith("/run/user")) { + QMessageBox warning; + warning.setText(tr("You're trying to specify an instance folder " + "which was granted temporarily via Flatpak.\n" + "This is known to cause problems. " + "After a restart the launcher might break, " + "because it will no longer have access to that directory.\n\n" + "Granting %1 access to it via Flatseal is recommended.") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + warning.setInformativeText(tr("Do you want to proceed anyway?")); + warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + int result1 = warning.exec(); + if (result1 == QMessageBox::Ok) { + result = true; + } + } else { + result = true; + } + } + return result; +} + +void ExternalInstancePage::on_actionAddExtInst_triggered() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("External Instance Folder")); + + if (verifyInstDirPath(raw_dir)) { + QString cooked_dir = FS::NormalizePath(raw_dir); + m_model->insertRow(m_model->rowCount()); + auto index = m_model->index(m_model->rowCount() - 1, 0); + m_model->setData(index, cooked_dir, Qt::DisplayRole); + ui->listView->setCurrentIndex(index); + } +} + +void ExternalInstancePage::on_actionRemove_triggered() +{ + auto index = ui->listView->currentIndex(); + + m_model->removeRow(index.row()); +} + +void ExternalInstancePage::on_actionHide_triggered() {} +bool ExternalInstancePage::apply() +{ + if (m_rootInstDir == APPLICATION->settings()->get("InstanceDir").toString()) + APPLICATION->instances()->setExtInstDir(m_model->stringList()); + + return true; +} +void ExternalInstancePage::openedImpl() +{ + m_rootInstDir = APPLICATION->settings()->get("InstanceDir").toString(); + m_model->setStringList(APPLICATION->instances()->getExtInstDir()); +} +#include "ExternalInstancePage.moc" \ No newline at end of file diff --git a/launcher/ui/pages/global/ExternalInstancePage.h b/launcher/ui/pages/global/ExternalInstancePage.h new file mode 100644 index 0000000000..55a361d182 --- /dev/null +++ b/launcher/ui/pages/global/ExternalInstancePage.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 初夏同学 <2411829240@qq.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 . + */ + +#pragma once + +#include +#include + +#include "ui/pages/BasePage.h" + +#include "Application.h" +class QStringListModel; + +namespace Ui { +class ExternalInstancePage; +} + +class ExternalInstancePage : public QMainWindow, public BasePage { + Q_OBJECT + + public: + inline static bool verifyInstDirPath(const QString& raw_dir); + + explicit ExternalInstancePage(QWidget* parent = 0); + ~ExternalInstancePage(); + + QString displayName() const override { return tr("External Instance"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("viewfolder"); } + QString id() const override { return "external-Instance-directory"; } + QString helpPage() const override { return "Getting-Started#adding-an-account"; } + void retranslate() override; + bool apply() override; + void openedImpl() override; + public slots: + void on_actionRemove_triggered(); + void on_actionAddExtInst_triggered(); + void on_actionHide_triggered(); + + protected slots: + void ShowContextMenu(const QPoint& pos); + + private: + void changeEvent(QEvent* event) override; + QMenu* createPopupMenu() override; + + QString m_rootInstDir; + QStringListModel* m_model; + Ui::ExternalInstancePage* ui; +}; diff --git a/launcher/ui/pages/global/ExternalInstancePage.ui b/launcher/ui/pages/global/ExternalInstancePage.ui new file mode 100644 index 0000000000..20dc5de91c --- /dev/null +++ b/launcher/ui/pages/global/ExternalInstancePage.ui @@ -0,0 +1,71 @@ + + + ExternalInstancePage + + + + 0 + 0 + 800 + 600 + + + + + + + + true + + + false + + + false + + + true + + + false + + + + + + + + RightToolBarArea + + + false + + + + + + + Remove + + + + + Add Instance Dir + + + + + + VersionListView + QTreeView +
ui/widgets/VersionListView.h
+
+ + WideBar + QToolBar +
ui/widgets/WideBar.h
+
+
+ + +