diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ddeb30588..0e5f3cb3b 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -825,6 +825,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("TPDownloadGeometry", ""); m_settings->registerSetting("ShaderDownloadGeometry", ""); m_settings->registerSetting("DataPackDownloadGeometry", ""); + m_settings->registerSetting("SelectInstanceGeometry", ""); // data pack window // in future, more pages may be added - so this name is chosen to avoid needing migration diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 7d4430fd2..f72445f61 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -298,6 +298,8 @@ set(MINECRAFT_SOURCES minecraft/MinecraftLoadAndCheck.cpp minecraft/MojangVersionFormat.cpp minecraft/MojangVersionFormat.h + minecraft/MultiWorldList.cpp + minecraft/MultiWorldList.h minecraft/Rule.cpp minecraft/Rule.h minecraft/OneSixVersionFormat.cpp @@ -832,6 +834,8 @@ SET(LAUNCHER_SOURCES ui/GuiUtil.cpp ui/MainWindow.h ui/MainWindow.cpp + ui/MultiWorldListPage.cpp + ui/MultiWorldListPage.h ui/InstanceWindow.h ui/InstanceWindow.cpp ui/ViewLogWindow.h diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 1339499c7..27b2da7cf 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -138,7 +138,7 @@ QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const QStringList InstanceList::getLinkedInstancesById(const QString& id) const { QStringList linkedInstances; - for (auto& inst : m_instances) { + for (auto& inst : instances) { if (inst->isLinkedToInstanceId(id)) linkedInstances.append(inst->id()); } @@ -156,7 +156,7 @@ QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) Q_UNUSED(parent); if (row < 0 || row >= count()) return QModelIndex(); - return createIndex(row, column, m_instances.at(row).get()); + return createIndex(row, column, instances.at(row).get()); } QVariant InstanceList::data(const QModelIndex& index, int role) const @@ -279,7 +279,7 @@ void InstanceList::deleteGroup(const GroupId& name) bool removed = false; qDebug() << "Delete group" << name; - for (auto& instance : m_instances) { + for (auto& instance : instances) { const QString& instID = instance->id(); const QString instGroupName = getInstanceGroup(instID); if (instGroupName == name) { @@ -303,7 +303,7 @@ void InstanceList::renameGroup(const QString& src, const QString& dst) bool modified = false; qDebug() << "Rename group" << src << "to" << dst; - for (auto& instance : m_instances) { + for (auto& instance : instances) { const QString& instID = instance->id(); const QString instGroupName = getInstanceGroup(instID); if (instGroupName == src) { @@ -497,7 +497,7 @@ QList InstanceList::discoverInstances() InstanceList::InstListError InstanceList::loadList() { - auto existingIds = getIdMapping(m_instances); + auto existingIds = getIdMapping(instances); std::vector> newList; @@ -525,7 +525,7 @@ InstanceList::InstListError InstanceList::loadList() int currentItem = -1; auto removeNow = [this, &front_bookmark, &back_bookmark, ¤tItem]() { beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); - m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); + instances.erase(instances.begin() + front_bookmark, instances.begin() + back_bookmark + 1); endRemoveRows(); front_bookmark = -1; back_bookmark = currentItem; @@ -560,14 +560,14 @@ InstanceList::InstListError InstanceList::loadList() void InstanceList::updateTotalPlayTime() { totalPlayTime = 0; - for (const auto& itr : m_instances) { + for (const auto& itr : instances) { totalPlayTime += itr->totalTimePlayed(); } } void InstanceList::saveNow() { - for (auto& item : m_instances) { + for (auto& item : instances) { item->saveNow(); } } @@ -576,8 +576,8 @@ void InstanceList::add(std::vector>& t) { beginInsertRows(QModelIndex(), count(), static_cast(count() + t.size() - 1)); for (auto& ptr : t) { - m_instances.push_back(std::move(ptr)); - connect(m_instances.back().get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); + instances.push_back(std::move(ptr)); + connect(instances.back().get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); } endInsertRows(); } @@ -607,11 +607,22 @@ void InstanceList::providerUpdated() } } +QList InstanceList::getAllInstances() const +{ + QList instanceList; + + for (const auto& inst : instances) { + instanceList.append(inst.get()); + } + + return instanceList; +} + BaseInstance* InstanceList::getInstanceById(QString instId) const { if (instId.isEmpty()) return nullptr; - for (auto& inst : m_instances) { + for (auto& inst : instances) { if (inst->id() == instId) { return inst.get(); } @@ -624,7 +635,7 @@ BaseInstance* InstanceList::getInstanceByManagedName(const QString& managed_name if (managed_name.isEmpty()) return {}; - for (auto& instance : m_instances) { + for (auto& instance : instances) { if (instance->getManagedPackName() == managed_name) return instance.get(); } @@ -641,7 +652,7 @@ int InstanceList::getInstIndex(BaseInstance* inst) const { int count = this->count(); for (int i = 0; i < count; i++) { - if (inst == m_instances.at(i).get()) { + if (inst == instances.at(i).get()) { return i; } } @@ -882,7 +893,7 @@ void InstanceList::on_InstFolderChanged([[maybe_unused]] const Setting& setting, m_instDir = newInstDir; m_groupsLoaded = false; beginRemoveRows(QModelIndex(), 0, count()); - m_instances.erase(m_instances.begin(), m_instances.end()); + instances.erase(instances.begin(), instances.end()); endRemoveRows(); emit instancesChanged(); } diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index f0a92d273..c0265c486 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -96,13 +96,14 @@ class InstanceList : public QAbstractListModel { */ enum InstListError { NoError = 0, UnknownError }; - BaseInstance* at(int i) const { return m_instances.at(i).get(); } + BaseInstance* at(int i) const { return instances.at(i).get(); } - int count() const { return static_cast(m_instances.size()); } + int count() const { return static_cast(instances.size()); } InstListError loadList(); void saveNow(); + QList getAllInstances() const; /* O(n) */ BaseInstance* getInstanceById(QString id) const; /* O(n) */ @@ -192,7 +193,7 @@ class InstanceList : public QAbstractListModel { int m_watchLevel = 0; int totalPlayTime = 0; bool m_dirty = false; - std::vector> m_instances; + std::vector> instances; // id -> refs QMap m_groupNameCache; diff --git a/launcher/minecraft/MultiWorldList.cpp b/launcher/minecraft/MultiWorldList.cpp new file mode 100644 index 000000000..280f84215 --- /dev/null +++ b/launcher/minecraft/MultiWorldList.cpp @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MultiWorldList.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "MinecraftInstance.h" +#include "PackProfile.h" +#include "icons/IconList.h" + +MultiWorldList::MultiWorldList(const QList& instances) : QAbstractListModel(), allInstances(instances) +{ + for (BaseInstance* inst : allInstances) { + m_dirs.append(dynamic_cast(inst)->worldDir()); + } + + for (QDir dir : m_dirs) { + FS::ensureFolderPathExists(dir.absolutePath()); + dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + } + + m_watcher = new QFileSystemWatcher(this); + m_isWatching = false; + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &MultiWorldList::directoryChanged); +} + +void MultiWorldList::startWatching() +{ + if (m_isWatching) { + return; + } + update(); + + m_isWatching = true; + + for (const QDir& dir : m_dirs) { + if (m_watcher->addPath(dir.absolutePath())) { + qDebug() << "Started watching" << dir.absolutePath(); + } else { + m_isWatching = false; + qDebug() << "Failed to start watching" << dir.absolutePath(); + } + } +} + +void MultiWorldList::stopWatching() +{ + if (!m_isWatching) { + return; + } + + m_isWatching = false; + + for (QDir dir : m_dirs) { + if (m_watcher->removePath(dir.absolutePath())) { + qDebug() << "Stopped watching" << dir.absolutePath(); + } else { + m_isWatching = true; + qDebug() << "Failed to stop watching" << dir.absolutePath(); + } + } +} + +bool MultiWorldList::update() +{ + if (!isValid()) + return false; + + QList newWorlds; + + for (BaseInstance* inst : allInstances) { + QDir dir = dynamic_cast(inst)->worldDir(); + dir.refresh(); + auto folderContents = dir.entryInfoList(); + // if there are any untracked files... + for (QFileInfo entry : folderContents) { + if (!entry.isDir()) + continue; + + World w(entry); + if (w.isValid()) { + newWorlds.append(InstanceWorld(w, inst)); + } + } + } + + beginResetModel(); + m_worlds.swap(newWorlds); + endResetModel(); + loadWorldsAsync(); + return true; +} + +void MultiWorldList::directoryChanged(QString) +{ + update(); +} + +bool MultiWorldList::isValid() +{ + bool valid = true; + + for (const QDir& dir : m_dirs) { + if (!(dir.exists() && dir.isReadable())) { + valid = false; + } + } + + return valid; +} + +QList MultiWorldList::instDirPaths() const +{ + QList dirList; + + for (BaseInstance const* instance : allInstances) { + dirList.append(QFileInfo(instance->instanceRoot()).absoluteFilePath()); + } + + return dirList; +} + +bool MultiWorldList::deleteWorld(int index) +{ + if (index >= m_worlds.size() || index < 0) + return false; + World& m = m_worlds[index].world; + if (m.destroy()) { + beginRemoveRows(QModelIndex(), index, index); + m_worlds.removeAt(index); + endRemoveRows(); + emit changed(); + return true; + } + return false; +} + +bool MultiWorldList::deleteWorlds(int first, int last) +{ + for (int i = first; i <= last; i++) { + World& m = m_worlds[i].world; + m.destroy(); + } + beginRemoveRows(QModelIndex(), first, last); + m_worlds.erase(m_worlds.begin() + first, m_worlds.begin() + last + 1); + endRemoveRows(); + emit changed(); + return true; +} + +bool MultiWorldList::resetIcon(int row) +{ + if (row >= m_worlds.size() || row < 0) + return false; + World& m = m_worlds[row].world; + if (m.resetIcon()) { + QModelIndex modelIndex = index(row, NameColumn); + emit dataChanged(modelIndex, modelIndex, { MultiWorldList::IconFileRole }); + return true; + } + return false; +} + +int MultiWorldList::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : 5; +} + +QVariant MultiWorldList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= m_worlds.size()) + return QVariant(); + + QLocale locale; + + const auto& instanceWorld = m_worlds[row]; + switch (role) { + case Qt::DisplayRole: + switch (column) { + case NameColumn: + return instanceWorld.world.name(); + + case InstanceColumn: + return instanceWorld.instance->name(); + + case VersionColumn: + static_cast(instanceWorld.instance)->getPackProfile()->reload(Net::Mode::Online); + return static_cast(instanceWorld.instance)->getPackProfile()->getComponentVersion("net.minecraft"); + + case GameModeColumn: + return instanceWorld.world.gameType().toTranslatedString(); + + case LastPlayedColumn: + return instanceWorld.world.lastPlayed(); + + case SizeColumn: + return locale.formattedDataSize(instanceWorld.world.bytes()); + + case InfoColumn: + if (instanceWorld.world.isSymLinkUnder(QFileInfo(instanceWorld.instance->instanceRoot()).absoluteFilePath())) { + return tr("This world is symbolically linked from elsewhere."); + } + if (instanceWorld.world.isMoreThanOneHardLink()) { + return tr("\nThis world is hard linked elsewhere."); + } + return ""; + default: + return QVariant(); + } + + case Qt::UserRole: + if (column == SizeColumn) + return QVariant::fromValue(instanceWorld.world.bytes()); + return data(index, Qt::DisplayRole); + + case Qt::ToolTipRole: { + if (column == InfoColumn) { + if (instanceWorld.world.isSymLinkUnder(QFileInfo(instanceWorld.instance->instanceRoot()).absoluteFilePath())) { + return tr("Warning: This world is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(instanceWorld.world.canonicalFilePath()); + } + if (instanceWorld.world.isMoreThanOneHardLink()) { + return tr("Warning: This world is hard linked elsewhere. Editing it will also change the original."); + } + } + return instanceWorld.world.folderName(); + } + case ObjectRole: { + return QVariant::fromValue((void*)&instanceWorld); + } + case FolderRole: { + return QDir::toNativeSeparators(QDir(dynamic_cast(instanceWorld.instance)->worldDir()).absoluteFilePath(instanceWorld.world.folderName())); + } + case SeedRole: { + return QVariant::fromValue(instanceWorld.world.seed()); + } + case NameRole: { + return instanceWorld.world.name(); + } + case LastPlayedRole: { + return instanceWorld.world.lastPlayed(); + } + case SizeRole: { + return QVariant::fromValue(instanceWorld.world.bytes()); + } + case IconFileRole: { + return instanceWorld.world.iconFile(); + } + case InstanceIconFileRole: { + return APPLICATION->icons()->getIcon(instanceWorld.instance->iconKey()); + } + default: + return QVariant(); + } +} + +QVariant MultiWorldList::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case NameColumn: + return tr("Name"); + case InstanceColumn: + return tr("Instance"); + case VersionColumn: + return tr("Version"); + case GameModeColumn: + return tr("Game Mode"); + case LastPlayedColumn: + return tr("Last Played"); + case SizeColumn: + //: World size on disk + return tr("Size"); + case InfoColumn: + //: special warnings? + return tr("Info"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) { + case NameColumn: + return tr("The name of the world."); + case InstanceColumn: + return tr("The instance the world belongs to."); + case VersionColumn: + return tr("The Minecraft version of the world."); + case GameModeColumn: + return tr("Game mode of the world."); + case LastPlayedColumn: + return tr("Date and time the world was last played."); + case SizeColumn: + return tr("Size of the world on disk."); + case InfoColumn: + return tr("Information and warnings about the world."); + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +QStringList MultiWorldList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +QMimeData* MultiWorldList::mimeData(const QModelIndexList& indexes) const +{ + QList urls; + + for (auto idx : indexes) { + if (idx.column() != 0) + continue; + + int row = idx.row(); + if (row < 0 || row >= this->m_worlds.size()) + continue; + + const World& world = m_worlds[row].world; + + if (!world.isValid() || !world.isOnFS()) + continue; + + QString worldPath = world.container().absoluteFilePath(); + qDebug() << worldPath; + urls.append(QUrl::fromLocalFile(worldPath)); + } + + auto result = new QMimeData(); + result->setUrls(urls); + return result; +} + +Qt::ItemFlags MultiWorldList::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +Qt::DropActions MultiWorldList::supportedDragActions() const +{ + // move to other mod lists or VOID + return Qt::MoveAction; +} + +Qt::DropActions MultiWorldList::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +void MultiWorldList::installWorld(BaseInstance* instance, QFileInfo filename) +{ + qDebug() << "installing:" << filename.absoluteFilePath(); + World w(filename); + if (!w.isValid()) { + return; + } + w.install(QDir(dynamic_cast(instance)->worldDir()).absolutePath()); + update(); +} + +bool MultiWorldList::dropMimeData(const QMimeData* data, + Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + // files dropped from outside? + if (data->hasUrls()) { + bool was_watching = m_isWatching; + if (was_watching) + stopWatching(); + auto urls = data->urls(); + for (auto url : urls) { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + QString filename = url.toLocalFile(); + + QFileInfo worldInfo(filename); + + emit fileDropped(worldInfo); + } + if (was_watching) + startWatching(); + return true; + } + return false; +} + +int64_t MultiWorldList::calculateWorldSize(const QFileInfo& file) +{ + if (file.isFile() && file.suffix() == "zip") { + return file.size(); + } else if (file.isDir()) { + QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); + int64_t total = 0; + while (it.hasNext()) { + it.next(); + total += it.fileInfo().size(); + } + return total; + } + return -1; +} + +void MultiWorldList::loadWorldsAsync() +{ + for (int i = 0; i < m_worlds.size(); ++i) { + auto file = m_worlds.at(i).world.container(); + int row = i; + QThreadPool::globalInstance()->start([this, file, row]() mutable { + auto size = calculateWorldSize(file); + + QMetaObject::invokeMethod( + this, + [this, size, row, file]() { + if (row < m_worlds.size() && m_worlds[row].world.container() == file) { + m_worlds[row].world.setSize(size); + + // Notify views + QModelIndex modelIndex = index(row, SizeColumn); + emit dataChanged(modelIndex, modelIndex, { SizeRole }); + } + }, + Qt::QueuedConnection); + }); + } +} diff --git a/launcher/minecraft/MultiWorldList.h b/launcher/minecraft/MultiWorldList.h new file mode 100644 index 000000000..dc0829349 --- /dev/null +++ b/launcher/minecraft/MultiWorldList.h @@ -0,0 +1,111 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include "BaseInstance.h" +#include "minecraft/World.h" + +class QFileSystemWatcher; + +struct InstanceWorld { + World world; + BaseInstance* instance; +}; + +class MultiWorldList : public QAbstractListModel { + Q_OBJECT + public: + enum Columns { NameColumn, InstanceColumn, VersionColumn, GameModeColumn, LastPlayedColumn, SizeColumn, InfoColumn }; + + enum Roles { ObjectRole = Qt::UserRole + 1, FolderRole, SeedRole, NameRole, InstanceRole, VersionRole, GameModeRole, LastPlayedRole, SizeRole, IconFileRole, InstanceIconFileRole }; + + MultiWorldList(const QList& instances); + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const { return parent.isValid() ? 0 : static_cast(size()); }; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + virtual int columnCount(const QModelIndex& parent) const; + + size_t size() const { return m_worlds.size(); }; + bool empty() const { return size() == 0; } + World& operator[](size_t index) { return m_worlds[index].world; } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /// Install a world from location + void installWorld(BaseInstance* instance, QFileInfo filename); + + /// Deletes the mod at the given index. + virtual bool deleteWorld(int index); + + /// Removes the world icon, if any + virtual bool resetIcon(int index); + + /// Deletes all the selected mods + virtual bool deleteWorlds(int first, int last); + + /// flags, mostly to support drag&drop + virtual Qt::ItemFlags flags(const QModelIndex& index) const; + /// get data for drag action + virtual QMimeData* mimeData(const QModelIndexList& indexes) const; + /// get the supported mime types + virtual QStringList mimeTypes() const; + /// process data from drop action + virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); + /// what drag actions do we support? + static int64_t calculateWorldSize(const QFileInfo& file); + virtual Qt::DropActions supportedDragActions() const; + + /// what drop actions do we support? + virtual Qt::DropActions supportedDropActions() const; + + void startWatching(); + void stopWatching(); + + virtual bool isValid(); + + QList dirs() const { return m_dirs; } + + QList instDirPaths() const; + + const QList& allWorlds() const { return m_worlds; } + + QList getInstances() const { return allInstances; } + + private slots: + void directoryChanged(QString path); + void loadWorldsAsync(); + + signals: + void changed(); + void fileDropped(QFileInfo worldInfo); + + protected: + QFileSystemWatcher* m_watcher; + bool m_isWatching; + QList m_dirs; + QList m_worlds; + + private: + QList allInstances; +}; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 1bdcd3f68..5b2ff9d8e 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -93,6 +93,7 @@ #include "InstanceWindow.h" #include "ui/GuiUtil.h" +#include "ui/MultiWorldListPage.h" #include "ui/ViewLogWindow.h" #include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" @@ -113,6 +114,7 @@ #include "ui/themes/ThemeManager.h" #include "ui/widgets/LabeledToolButton.h" +#include "minecraft/MultiWorldList.h" #include "minecraft/PackProfile.h" #include "minecraft/VersionFile.h" #include "minecraft/WorldList.h" @@ -333,7 +335,16 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi [](const QString& groupName) -> bool { return APPLICATION->instances()->isGroupCollapsed(groupName); }); connect(view, &InstanceView::groupStateChanged, APPLICATION->instances(), &InstanceList::on_GroupStateChanged); ui->horizontalLayout->addWidget(view); + + connect(this, &MainWindow::selectInstance, view, &InstanceView::selectInstance); } + + // All worlds toggle + { + connect(ui->actionAllWorlds, &QAction::toggled, this, &MainWindow::onAllWorldsToggled); + connect(allWorldsPage, &MultiWorldListPage::worldJoined, this, &MainWindow::worldJoined); + } + // The cat background { // set the cat action priority here so you can still see the action in qt designer @@ -842,6 +853,59 @@ QString intListToString(const QList& list) return slist.join(','); } +void MainWindow::onAllWorldsToggled(bool toggled) +{ + toggleAllWorldsScreen(toggled); +} + +void MainWindow::worldJoined(BaseInstance* instance) +{ + ui->actionAllWorlds->setChecked(false); + toggleAllWorldsScreen(false); + emit selectInstance(instance); +} + +void MainWindow::toggleAllWorldsScreen(bool toggled) +{ + if (toggled) { + QList const allInstances = APPLICATION->instances()->getAllInstances(); + + allWorldsList = new MultiWorldList(allInstances); + allWorldsList->update(); + + allWorldsPage = new MultiWorldListPage(allWorldsList); + ui->horizontalLayout->addWidget(allWorldsPage); + + view->setVisible(false); + m_oldInstanceToolbarSetting = ui->instanceToolBar->isVisible(); + ui->instanceToolBar->setVisible(false); + allWorldsPage->setVisible(true); + statusBar()->setVisible(false); + + allWorldsList->startWatching(); + + connect(allWorldsPage, &MultiWorldListPage::worldJoined, this, &MainWindow::worldJoined); + } else { + if (allWorldsList == nullptr || allWorldsPage == nullptr) { + view->setVisible(true); + ui->instanceToolBar->setVisible(m_oldInstanceToolbarSetting); + statusBar()->setVisible(APPLICATION->settings()->get("StatusBarVisible").toBool()); + } else { + allWorldsPage->setVisible(false); + view->setVisible(true); + ui->instanceToolBar->setVisible(m_oldInstanceToolbarSetting); + statusBar()->setVisible(APPLICATION->settings()->get("StatusBarVisible").toBool()); + + allWorldsList->stopWatching(); + + allWorldsPage->deleteLater(); + allWorldsPage = nullptr; + allWorldsList->deleteLater(); + allWorldsList = nullptr; + } + } +} + void MainWindow::onCatToggled(bool state) { setCatBackground(state); @@ -1580,6 +1644,10 @@ void MainWindow::on_actionViewSelectedInstFolder_triggered() void MainWindow::closeEvent(QCloseEvent* event) { + if (view->isVisible()) { + m_oldInstanceToolbarSetting = ui->instanceToolBar->isVisible(); + } + toggleAllWorldsScreen(false); // Save the window state and geometry. APPLICATION->settings()->set("MainWindowState", QString::fromUtf8(saveState().toBase64())); APPLICATION->settings()->set("MainWindowGeometry", QString::fromUtf8(saveGeometry().toBase64())); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 80860deef..601a2e5f0 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -58,6 +58,8 @@ class QLabel; class MinecraftLauncher; class BaseProfilerFactory; class InstanceView; +class MultiWorldList; +class MultiWorldListPage; class KonamiCode; class InstanceTask; class LabeledToolButton; @@ -84,10 +86,14 @@ class MainWindow : public QMainWindow { signals: void isClosing(); + void selectInstance(BaseInstance* instance); + protected: QMenu* createPopupMenu() override; private slots: + void onAllWorldsToggled(bool); + void onCatToggled(bool); void onCatChanged(int); @@ -171,6 +177,8 @@ class MainWindow : public QMainWindow { void taskEnd(); + void worldJoined(BaseInstance* instance); + /** * called when an icon is changed in the icon model. */ @@ -232,10 +240,13 @@ class MainWindow : public QMainWindow { void runModalTask(Task* task); void instanceFromInstanceTask(InstanceTask* task); + void toggleAllWorldsScreen(bool toggled); + private: Ui::MainWindow* ui; // these are managed by Qt's memory management model! InstanceView* view = nullptr; + MultiWorldListPage* allWorldsPage = nullptr; InstanceProxyModel* proxymodel = nullptr; QToolButton* newsLabel = nullptr; QLabel* m_statusLeft = nullptr; @@ -244,8 +255,10 @@ class MainWindow : public QMainWindow { LabeledToolButton* renameButton = nullptr; QToolButton* helpMenuButton = nullptr; KonamiCode* secretEventFilter = nullptr; + MultiWorldList* allWorldsList = nullptr; std::shared_ptr instanceToolbarSetting = nullptr; + bool m_oldInstanceToolbarSetting; unique_qobject_ptr m_newsChecker; diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index e9b9aa442..d2ac79f2f 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -56,6 +56,7 @@ + @@ -175,6 +176,7 @@ + @@ -779,6 +781,20 @@ Open the Java folder in a file browser. Only available if the built-in Java downloader is used. + + + true + + + + + + &All Worlds + + + Toggle All Worlds Screen + + diff --git a/launcher/ui/MultiWorldListPage.cpp b/launcher/ui/MultiWorldListPage.cpp new file mode 100644 index 000000000..76756f4ba --- /dev/null +++ b/launcher/ui/MultiWorldListPage.cpp @@ -0,0 +1,600 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MultiWorldListPage.h" +#include "minecraft/MultiWorldList.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui_MultiWorldListPage.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "FileSystem.h" +#include "tools/MCEditTool.h" + +#include "DesktopServices.h" +#include "ui/GuiUtil.h" + +#include "Application.h" +#include "icons/IconList.h" +#include "pages/instance/DataPackPage.h" + +class MultiWorldListProxyModel : public QSortFilterProxyModel { + Q_OBJECT + + public: + MultiWorldListProxyModel(QObject* parent) : QSortFilterProxyModel(parent) {} + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const + { + QModelIndex sourceIndex = mapToSource(index); + + if (index.column() == 0 && role == Qt::DecorationRole) { + MultiWorldList* worlds = qobject_cast(sourceModel()); + auto iconFile = worlds->data(sourceIndex, MultiWorldList::IconFileRole).toString(); + if (iconFile.isNull()) { + // NOTE: Minecraft uses the same placeholder for servers AND worlds + return QIcon::fromTheme("unknown_server"); + } + return QIcon(iconFile); + } + + if (index.column() == 1 && role == Qt::DecorationRole) { + MultiWorldList* worlds = qobject_cast(sourceModel()); + auto icon = worlds->data(sourceIndex, MultiWorldList::InstanceIconFileRole).value(); + return icon.pixmap(24, 24); + } + + return sourceIndex.data(role); + } +}; + +MultiWorldListPage::MultiWorldListPage(MultiWorldList* worlds, QWidget* parent) + : QMainWindow(parent), ui(new Ui::MultiWorldListPage), m_worlds(worlds) +{ + ui->setupUi(this); + + ui->toolBar->insertSpacer(ui->actionRefresh); + ui->actionJoin->setEnabled(true); + + auto* proxy = new MultiWorldListProxyModel(this); + proxy->setSortCaseSensitivity(Qt::CaseInsensitive); + proxy->setSourceModel(m_worlds); + proxy->setSortRole(Qt::UserRole); + ui->worldTreeView->setSortingEnabled(true); + ui->worldTreeView->setModel(proxy); + ui->worldTreeView->installEventFilter(this); + ui->worldTreeView->setContextMenuPolicy(Qt::CustomContextMenu); + ui->worldTreeView->setIconSize(QSize(64, 64)); + connect(ui->worldTreeView, &QTreeView::customContextMenuRequested, this, &MultiWorldListPage::ShowContextMenu); + + auto head = ui->worldTreeView->header(); + head->setSectionResizeMode(0, QHeaderView::Stretch); + head->setSectionResizeMode(1, QHeaderView::ResizeToContents); + head->setSectionResizeMode(4, QHeaderView::ResizeToContents); + + connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &MultiWorldListPage::worldChanged); + connect(ui->worldTreeView, &QAbstractItemView::doubleClicked, this, &MultiWorldListPage::worldDoubleClicked); + worldChanged(QModelIndex(), QModelIndex()); + + connect(m_worlds, &MultiWorldList::fileDropped, this, &MultiWorldListPage::fileDropped); +} + +void MultiWorldListPage::openedImpl() +{ + m_worlds->startWatching(); + + auto const setting_name = QString("WideBarVisibility_%1").arg(id()); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); + + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); +} + +void MultiWorldListPage::closedImpl() +{ + m_worlds->stopWatching(); + + m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); +} + +MultiWorldListPage::~MultiWorldListPage() +{ + m_worlds->stopWatching(); + delete ui; +} + +void MultiWorldListPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->worldTreeView->mapToGlobal(pos)); + delete menu; +} + +QMenu* MultiWorldListPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction(ui->toolBar->toggleViewAction()); + return filteredMenu; +} + +bool MultiWorldListPage::shouldDisplay() const +{ + return true; +} + +void MultiWorldListPage::retranslate() +{ + ui->retranslateUi(this); +} + +bool MultiWorldListPage::worldListFilter(QKeyEvent* keyEvent) +{ + if (keyEvent->key() == Qt::Key_Delete) { + on_actionRemove_triggered(); + return true; + } + return QWidget::eventFilter(ui->worldTreeView, keyEvent); +} + +bool MultiWorldListPage::eventFilter(QObject* obj, QEvent* ev) +{ + if (ev->type() != QEvent::KeyPress) { + return QWidget::eventFilter(obj, ev); + } + QKeyEvent* keyEvent = static_cast(ev); + if (obj == ui->worldTreeView) + return worldListFilter(keyEvent); + return QWidget::eventFilter(obj, ev); +} + +void MultiWorldListPage::on_actionRemove_triggered() +{ + auto proxiedIndex = getSelectedWorld(); + + if (!proxiedIndex.isValid()) + return; + + auto result = CustomMessageBox::selectable(this, tr("Confirm Deletion"), + tr("You are about to delete \"%1\".\n" + "The world may be gone forever (A LONG TIME).\n\n" + "Are you sure?") + .arg(m_worlds->allWorlds().at(proxiedIndex.row()).world.name()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (result != QMessageBox::Yes) { + return; + } + m_worlds->stopWatching(); + m_worlds->deleteWorld(proxiedIndex.row()); + m_worlds->startWatching(); +} + +void MultiWorldListPage::on_actionView_Folder_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) { + return; + } + + auto worldVariant = m_worlds->data(index, MultiWorldList::ObjectRole); + auto world = (World*)worldVariant.value(); + + DesktopServices::openPath(world->canonicalFilePath(), true); +} + +void MultiWorldListPage::on_actionData_Packs_triggered() +{ + QModelIndex index = getSelectedWorld(); + + if (!index.isValid()) { + return; + } + + if (!worldSafetyNagQuestion(tr("Manage Data Packs"))) + return; + + const QString fullPath = m_worlds->data(index, MultiWorldList::FolderRole).toString(); + const QString folder = FS::PathCombine(fullPath, "datapacks"); + + auto dialog = new QDialog(this); + dialog->setWindowTitle(tr("Data packs for %1").arg(m_worlds->data(index, MultiWorldList::NameRole).toString())); + dialog->setWindowModality(Qt::WindowModal); + + dialog->resize(static_cast(std::max(0.5 * window()->width(), 400.0)), + static_cast(std::max(0.75 * window()->height(), 400.0))); + dialog->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("DataPackDownloadGeometry").toByteArray())); + + GenericPageProvider provider(dialog->windowTitle()); + + auto instance = (static_cast(m_worlds->data(index, MultiWorldList::ObjectRole).value()))->instance; + + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_datapackModel.reset(new DataPackFolderModel(folder, instance, isIndexed, true)); + + provider.addPageCreator([this, instance] { return new DataPackPage(instance, m_datapackModel.get(), this); }); + + auto layout = new QVBoxLayout(dialog); + + auto focusStealer = new QPushButton(dialog); + layout->addWidget(focusStealer); + focusStealer->setDefault(true); + focusStealer->hide(); + + auto pageContainer = new PageContainer(&provider, {}, dialog); + pageContainer->hidePageList(); + layout->addWidget(pageContainer); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::Help); + connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); + connect(buttonBox, &QDialogButtonBox::helpRequested, pageContainer, &PageContainer::help); + layout->addWidget(buttonBox); + + dialog->setLayout(layout); + + dialog->setAttribute(Qt::WA_DeleteOnClose); + + connect(dialog, &QDialog::finished, this, + [dialog]() { APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); }); + + dialog->open(); +} + +void MultiWorldListPage::on_actionReset_Icon_triggered() +{ + auto proxiedIndex = getSelectedWorld(); + + if (!proxiedIndex.isValid()) + return; + + if (m_worlds->resetIcon(proxiedIndex.row())) { + ui->actionReset_Icon->setEnabled(false); + } +} + +QModelIndex MultiWorldListPage::getSelectedWorld() +{ + auto index = ui->worldTreeView->selectionModel()->currentIndex(); + + auto proxy = (QSortFilterProxyModel*)ui->worldTreeView->model(); + return proxy->mapToSource(index); +} + +void MultiWorldListPage::on_actionCopy_Seed_triggered() +{ + QModelIndex index = getSelectedWorld(); + + if (!index.isValid()) { + return; + } + int64_t seed = m_worlds->data(index, MultiWorldList::SeedRole).toLongLong(); + APPLICATION->clipboard()->setText(QString::number(seed)); +} + +void MultiWorldListPage::on_actionMCEdit_triggered() +{ + if (m_mceditStarting) + return; + + auto mcedit = APPLICATION->mcedit(); + + const QString mceditPath = mcedit->path(); + + QModelIndex index = getSelectedWorld(); + + if (!index.isValid()) { + return; + } + + if (!worldSafetyNagQuestion(tr("Open World in MCEdit"))) + return; + + auto fullPath = m_worlds->data(index, MultiWorldList::FolderRole).toString(); + + auto program = mcedit->getProgramPath(); + if (program.size()) { +#ifdef Q_OS_WIN32 + if (!QProcess::startDetached(program, { fullPath }, mceditPath)) { + mceditError(); + } +#else + m_mceditProcess.reset(new LoggedProcess()); + m_mceditProcess->setDetachable(true); + connect(m_mceditProcess.get(), &LoggedProcess::stateChanged, this, &MultiWorldListPage::mceditState); + m_mceditProcess->start(program, { fullPath }); + m_mceditProcess->setWorkingDirectory(mceditPath); + m_mceditStarting = true; +#endif + } else { + QMessageBox::warning(this->parentWidget(), tr("No MCEdit found or set up!"), + tr("You do not have MCEdit set up or it was moved.\nYou can set it up in the global settings.")); + } +} + +void MultiWorldListPage::mceditError() +{ + QMessageBox::warning(this->parentWidget(), tr("MCEdit failed to start!"), + tr("MCEdit failed to start.\nIt may be necessary to reinstall it.")); +} + +void MultiWorldListPage::mceditState(LoggedProcess::State state) +{ + bool failed = false; + switch (state) { + case LoggedProcess::NotRunning: + case LoggedProcess::Starting: + return; + case LoggedProcess::FailedToStart: + case LoggedProcess::Crashed: + case LoggedProcess::Aborted: { + failed = true; + } + /* fallthrough */ + case LoggedProcess::Running: + case LoggedProcess::Finished: { + m_mceditStarting = false; + break; + } + } + if (failed) { + mceditError(); + } +} + +void MultiWorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +{ + QModelIndex index = getSelectedWorld(); + bool enable = index.isValid(); + ui->actionCopy_Seed->setEnabled(enable); + ui->actionMCEdit->setEnabled(enable); + ui->actionRemove->setEnabled(enable); + ui->actionCopy->setEnabled(enable); + ui->actionRename->setEnabled(enable); + ui->actionData_Packs->setEnabled(enable); + ui->actionView_Folder->setEnabled(enable); + ui->actionJoin->setEnabled(enable); + ui->actionJoin_Offline->setEnabled(enable); + ui->actionInstance_Settings->setEnabled(enable); + bool hasIcon = !index.data(MultiWorldList::IconFileRole).isNull(); + ui->actionReset_Icon->setEnabled(enable && hasIcon); +} + +void MultiWorldListPage::on_actionAdd_triggered() +{ + auto list = GuiUtil::BrowseForFiles(displayName(), tr("Select a Minecraft world zip"), tr("Minecraft World Zip File") + " (*.zip)", + QString(), this->parentWidget()); + if (!list.empty()) { + m_worlds->stopWatching(); + for (auto filename : list) { + auto *instance = selectInstance(tr("Select instance to add world '%1' to.").arg(QFileInfo(filename).fileName())); + + if (instance != nullptr) { + m_worlds->installWorld(instance, QFileInfo(filename)); + } + } + m_worlds->startWatching(); + } +} + +bool MultiWorldListPage::isWorldSafe(QModelIndex index) +{ + return !static_cast(m_worlds->data(index, MultiWorldList::ObjectRole).value())->instance->isRunning(); +} + +bool MultiWorldListPage::worldSafetyNagQuestion(const QString& actionType) +{ + if (!isWorldSafe(getSelectedWorld())) { + auto result = QMessageBox::question( + this, actionType, tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?")); + if (result == QMessageBox::No) { + return false; + } + } + return true; +} + +void MultiWorldListPage::on_actionCopy_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) { + return; + } + + if (!worldSafetyNagQuestion(tr("Copy World"))) + return; + + auto worldVariant = m_worlds->data(index, MultiWorldList::ObjectRole); + auto *world = static_cast(worldVariant.value()); + + bool ok = false; + QString name = + QInputDialog::getText(this, tr("World name"), tr("Enter a new name for the copy."), QLineEdit::Normal, world->world.name(), &ok); + + if (ok && name.length() > 0) { + auto *instance = selectInstance(tr("Select instance to copy world to."), world->instance); + + if (instance != nullptr) { + world->world.install((QDir(instance->worldDir())).absolutePath(), name); + m_worlds->update(); + } + } +} + +// TODO: Make this a separate dialog class in launcher/ui/dialogs +Q_DECLARE_METATYPE(BaseInstance*); +MinecraftInstance* MultiWorldListPage::selectInstance(const QString& message, const BaseInstance* preselectedInstance) +{ + auto *dialog = new QDialog(this); + dialog->setWindowTitle(tr("Select Instance")); + + dialog->resize(static_cast(std::max(0.5 * window()->width(), 400.0)), + static_cast(std::max(0.75 * window()->height(), 400.0))); + dialog->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("SelectInstanceGeometry").toByteArray())); + + auto *layout = new QVBoxLayout(dialog); + + layout->addWidget(new QLabel(message)); + + auto *instanceList = new QListWidget(dialog); + + for (auto *instance : m_worlds->getInstances()) { + auto *item = new QListWidgetItem(instanceList); + item->setText(instance->name()); + item->setIcon(APPLICATION->icons()->getIcon(instance->iconKey())); + item->setData(Qt::UserRole, QVariant::fromValue(instance)); + if (instance == preselectedInstance) { + instanceList->setCurrentItem(item); + } + } + + instanceList->sortItems(Qt::AscendingOrder); + + layout->addWidget(instanceList); + + auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); + connect(buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept); + layout->addWidget(buttonBox); + + dialog->setLayout(layout); + + connect(dialog, &QDialog::finished, this, + [dialog]() { APPLICATION->settings()->set("SelectInstanceGeometry", dialog->saveGeometry().toBase64()); }); + + if (dialog->exec() == QDialog::Accepted) { + return static_cast(instanceList->currentItem()->data(Qt::UserRole).value()); + } + + return nullptr; +} + +void MultiWorldListPage::on_actionRename_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) { + return; + } + + if (!worldSafetyNagQuestion(tr("Rename World"))) + return; + + auto worldVariant = m_worlds->data(index, MultiWorldList::ObjectRole); + auto world = (World*)worldVariant.value(); + + bool ok = false; + QString name = QInputDialog::getText(this, tr("World name"), tr("Enter a new world name."), QLineEdit::Normal, world->name(), &ok); + + if (ok && name.length() > 0) { + world->rename(name); + } +} + +void MultiWorldListPage::on_actionInstance_Settings_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) { + return; + } + auto worldVariant = m_worlds->data(index, MultiWorldList::ObjectRole); + auto *world = static_cast(worldVariant.value()); + + if (world->instance->canEdit()) { + APPLICATION->showInstanceWindow(world->instance); + } else { + CustomMessageBox::selectable(this, tr("Instance not editable"), + tr("This instance is not editable. It may be broken, invalid, or too old. Check logs for details."), + QMessageBox::Critical) + ->show(); + } +} + +void MultiWorldListPage::on_actionRefresh_triggered() +{ + m_worlds->update(); +} + +void MultiWorldListPage::worldDoubleClicked(const QModelIndex& index) +{ + auto *proxy = static_cast(ui->worldTreeView->model()); + join(proxy->mapToSource(index), LaunchMode::Normal); +} + +void MultiWorldListPage::fileDropped(const QFileInfo& worldInfo) +{ + auto *instance = selectInstance(tr("Select instance to add world '%1' to.").arg(worldInfo.fileName())); + + if (instance != nullptr) { + if (!QDir(instance->worldDir()).entryInfoList().contains(worldInfo)) { + m_worlds->installWorld(instance, worldInfo); + } + } +} + +void MultiWorldListPage::on_actionJoin_triggered() +{ + QModelIndex index = getSelectedWorld(); + join(index, LaunchMode::Normal); +} + +void MultiWorldListPage::on_actionJoin_Offline_triggered() +{ + QModelIndex index = getSelectedWorld(); + join(index, LaunchMode::Offline); +} + +void MultiWorldListPage::join(const QModelIndex& index, const LaunchMode launchMode) +{ + if (!index.isValid()) { + return; + } + auto worldVariant = m_worlds->data(index, MultiWorldList::ObjectRole); + auto *world = static_cast(worldVariant.value()); + APPLICATION->launch(world->instance, launchMode, std::make_shared(MinecraftTarget::parse(world->world.folderName(), true))); + emit worldJoined(world->instance); +} + +#include "MultiWorldListPage.moc" diff --git a/launcher/ui/MultiWorldListPage.h b/launcher/ui/MultiWorldListPage.h new file mode 100644 index 000000000..4e3e4b466 --- /dev/null +++ b/launcher/ui/MultiWorldListPage.h @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include "minecraft/MinecraftInstance.h" +#include "ui/pages/BasePage.h" + +#include "settings/Setting.h" + +class MultiWorldList; +namespace Ui { +class MultiWorldListPage; +} + +class MultiWorldListPage : public QMainWindow, public BasePage { + Q_OBJECT + + public: + explicit MultiWorldListPage(MultiWorldList* worlds, QWidget* parent = 0); + virtual ~MultiWorldListPage(); + + virtual QString displayName() const override { return tr("Worlds"); } + virtual QIcon icon() const override { return QIcon::fromTheme("worlds"); } + virtual QString id() const override { return "worlds"; } + virtual QString helpPage() const override { return "Worlds"; } + virtual bool shouldDisplay() const override; + void retranslate() override; + + virtual void openedImpl() override; + virtual void closedImpl() override; + + signals: + void worldJoined(BaseInstance* instance); + + protected: + bool eventFilter(QObject* obj, QEvent* ev) override; + bool worldListFilter(QKeyEvent* ev); + QMenu* createPopupMenu() override; + + private: + QModelIndex getSelectedWorld(); + bool isWorldSafe(QModelIndex index); + bool worldSafetyNagQuestion(const QString& actionType); + void mceditError(); + void join(const QModelIndex& index, LaunchMode launchMode); + MinecraftInstance* selectInstance(const QString& message, const BaseInstance* preselectedInstance = nullptr); + + private: + Ui::MultiWorldListPage* ui; + MultiWorldList* m_worlds; + unique_qobject_ptr m_mceditProcess; + bool m_mceditStarting = false; + + std::shared_ptr m_wide_bar_setting = nullptr; + std::unique_ptr m_datapackModel; + + private slots: + void on_actionCopy_Seed_triggered(); + void on_actionMCEdit_triggered(); + void on_actionRemove_triggered(); + void on_actionAdd_triggered(); + void on_actionCopy_triggered(); + void on_actionRename_triggered(); + void on_actionInstance_Settings_triggered(); + void on_actionRefresh_triggered(); + void on_actionView_Folder_triggered(); + void on_actionData_Packs_triggered(); + void on_actionReset_Icon_triggered(); + void worldChanged(const QModelIndex& current, const QModelIndex& previous); + void mceditState(LoggedProcess::State state); + void on_actionJoin_triggered(); + void on_actionJoin_Offline_triggered(); + void worldDoubleClicked(const QModelIndex& index); + void fileDropped(const QFileInfo& worldInfo); + + void ShowContextMenu(const QPoint& pos); +}; diff --git a/launcher/ui/MultiWorldListPage.ui b/launcher/ui/MultiWorldListPage.ui new file mode 100644 index 000000000..5c1a86b75 --- /dev/null +++ b/launcher/ui/MultiWorldListPage.ui @@ -0,0 +1,185 @@ + + + MultiWorldListPage + + + + 0 + 0 + 800 + 600 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + true + + + QAbstractItemView::DragDrop + + + true + + + false + + + false + + + true + + + true + + + QAbstractItemView::ScrollPerPixel + + + false + + + + + + + + Actions + + + Qt::LeftToolBarArea|Qt::RightToolBarArea + + + Qt::ToolButtonTextOnly + + + false + + + RightToolBarArea + + + false + + + + + + + + + + + + + + + + + + + + Add + + + + + Join + + + + + Join Offline + + + + + Rename + + + + + Copy + + + + + Delete + + + + + MCEdit + + + + + Copy Seed + + + + + Refresh + + + + + View Folder + + + + + Reset Icon + + + Remove world icon to make the game re-generate it on next load. + + + + + Data Packs + + + Manage data packs inside the world. + + + + + View Instance + + + View the instance this world belongs to. + + + + + + WideBar + QToolBar +
ui/widgets/WideBar.h
+
+
+ + +
diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index 9a24b7990..90fe7606f 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -207,6 +207,24 @@ void InstanceView::updateGeometries() viewport()->update(); } +void InstanceView::selectInstance(const BaseInstance* instance) const +{ + QModelIndex index; + + for (int row = 0; row < model()->rowCount(); row++) { + for (int col = 0; col < model()->columnCount(); col++) { + auto testIndex = model()->index(row, col); + if (testIndex.data(InstanceList::InstanceIDRole).toString() == instance->id()) { + index = testIndex; + } + } + } + + if (index.isValid()) { + selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); + } +} + bool InstanceView::isIndexHidden(const QModelIndex& index) const { VisualGroup* cat = category(index); diff --git a/launcher/ui/instanceview/InstanceView.h b/launcher/ui/instanceview/InstanceView.h index 5d9dbf729..3fca0c4fa 100644 --- a/launcher/ui/instanceview/InstanceView.h +++ b/launcher/ui/instanceview/InstanceView.h @@ -40,6 +40,8 @@ #include #include #include + +#include "BaseInstance.h" #include "VisualGroup.h" #include "ui/themes/CatPainter.h" @@ -82,6 +84,7 @@ class InstanceView : public QAbstractItemView { public slots: virtual void updateGeometries() override; + void selectInstance(const BaseInstance* instance) const; protected slots: virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) override;