diff --git a/launcher/minecraft/MultiWorldList.cpp b/launcher/minecraft/MultiWorldList.cpp new file mode 100644 index 000000000..48d3a3c18 --- /dev/null +++ b/launcher/minecraft/MultiWorldList.cpp @@ -0,0 +1,454 @@ +// 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 + +MultiWorldList::MultiWorldList(const QList dirs, QList instances) : QAbstractListModel(), m_instances(instances) +{ + for (int i = 0; i < dirs.length(); i++) { // better way to do this? iy + m_dirs[i] = dirs[i]; + } + + 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() //figure out what watchers do / if necessary and do all paths iy +{ + if (m_isWatching) { + return; + } + update(); + m_isWatching = m_watcher->addPath(m_dirs[0].absolutePath()); + if (m_isWatching) { + qDebug() << "Started watching" << m_dirs[0].absolutePath(); + } else { + qDebug() << "Failed to start watching" << m_dirs[0].absolutePath(); + } +} + +void MultiWorldList::stopWatching() //same as above function iy +{ + if (!m_isWatching) { + return; + } + m_isWatching = !m_watcher->removePath(m_dirs[0].absolutePath()); + if (!m_isWatching) { + qDebug() << "Stopped watching" << m_dirs[0].absolutePath(); + } else { + qDebug() << "Failed to stop watching" << m_dirs[0].absolutePath(); + } +} + +bool MultiWorldList::update() +{ + if (!isValid()) + return false; + + QList newWorlds; + + for (QDir dir : m_dirs) { + 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(w); + } + } + } + + beginResetModel(); + m_worlds.swap(newWorlds); + endResetModel(); + loadWorldsAsync(); + return true; +} + +void MultiWorldList::directoryChanged(QString) +{ + update(); +} + +bool MultiWorldList::isValid() //account for all directories +{ + return m_dirs[0].exists() && m_dirs[0].isReadable(); +} + +QList MultiWorldList::instDirPaths() const +{ + QList dirList; + + for (BaseInstance* instance : m_instances) { + 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]; + 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]; + 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]; + 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; + + auto& world = m_worlds[row]; + switch (role) { + case Qt::DisplayRole: + switch (column) { + case NameColumn: + return world.name(); + + case GameModeColumn: + return world.gameType().toTranslatedString(); + + case LastPlayedColumn: + return world.lastPlayed(); + + case SizeColumn: + return locale.formattedDataSize(world.bytes()); + + case InfoColumn: + if (world.isSymLinkUnder(instDirPaths()[0])) { //FIX THIS--NO INDEX 0 iy + return tr("This world is symbolically linked from elsewhere."); + } + if (world.isMoreThanOneHardLink()) { + return tr("\nThis world is hard linked elsewhere."); + } + return ""; + default: + return QVariant(); + } + + case Qt::UserRole: + if (column == SizeColumn) + return QVariant::fromValue(world.bytes()); + return data(index, Qt::DisplayRole); + + case Qt::ToolTipRole: { + if (column == InfoColumn) { + if (world.isSymLinkUnder(instDirPaths()[0])) { //SAME HERE iy + return tr("Warning: This world is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(world.canonicalFilePath()); + } + if (world.isMoreThanOneHardLink()) { + return tr("Warning: This world is hard linked elsewhere. Editing it will also change the original."); + } + } + return world.folderName(); + } + case ObjectRole: { + return QVariant::fromValue((void*)&world); + } + case FolderRole: { + return QDir::toNativeSeparators(dirs()[0].absoluteFilePath(world.folderName())); //SAME HERE iy + } + case SeedRole: { + return QVariant::fromValue(world.seed()); + } + case NameRole: { + return world.name(); + } + case LastPlayedRole: { + return world.lastPlayed(); + } + case SizeRole: { + return QVariant::fromValue(world.bytes()); + } + case IconFileRole: { + return world.iconFile(); + } + 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 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 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]; + + 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(QFileInfo filename) +{ + qDebug() << "installing:" << filename.absoluteFilePath(); + World w(filename); + if (!w.isValid()) { + return; + } + w.install(m_dirs[0].absolutePath()); //more directory stuff iy +} + +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); + + if (!m_dirs[0].entryInfoList().contains(worldInfo)) { //more stuff to fix iy + installWorld(worldInfo); + } + } + if (was_watching) + startWatching(); + return true; + } + return false; +} + +int64_t 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).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].container() == file) { + m_worlds[row].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..eff3bfe22 --- /dev/null +++ b/launcher/minecraft/MultiWorldList.h @@ -0,0 +1,100 @@ +/* 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; + +class MultiWorldList : public QAbstractListModel { + Q_OBJECT + public: + enum Columns { NameColumn, GameModeColumn, LastPlayedColumn, SizeColumn, InfoColumn }; + + enum Roles { ObjectRole = Qt::UserRole + 1, FolderRole, SeedRole, NameRole, GameModeRole, LastPlayedRole, SizeRole, IconFileRole }; + + MultiWorldList(const QList dirs, 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]; } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /// Install a world from location + void installWorld(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? + 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; } + + private slots: + void directoryChanged(QString path); + void loadWorldsAsync(); + + signals: + void changed(); + + protected: + QList m_instances; + QFileSystemWatcher* m_watcher; + bool m_isWatching; + QList m_dirs; + QList m_worlds; +}; diff --git a/launcher/ui/MultiWorldListPage.cpp b/launcher/ui/MultiWorldListPage.cpp new file mode 100644 index 000000000..3698e9356 --- /dev/null +++ b/launcher/ui/MultiWorldListPage.cpp @@ -0,0 +1,477 @@ +// 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_WorldListPage.h" //fix this iy + +#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 "pages/instance/DataPackPage.h" + +class WorldListProxyModel : public QSortFilterProxyModel { + Q_OBJECT + + public: + WorldListProxyModel(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); + } + + return sourceIndex.data(role); + } +}; + +MultiWorldListPage::MultiWorldListPage(MinecraftInstance* inst, MultiWorldList* worlds, QWidget* parent) + : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds) //use MultiWorldListPage.ui to have separate ui from normal world list page iy +{ + ui->setupUi(this); + + ui->toolBar->insertSpacer(ui->actionRefresh); + + WorldListProxyModel* proxy = new WorldListProxyModel(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); + worldChanged(QModelIndex(), QModelIndex()); +} + +void MultiWorldListPage::openedImpl() +{ + m_worlds->startWatching(); + + if (!m_inst || !m_inst->traits().contains("feature:is_quick_play_singleplayer")) { + ui->toolBar->removeAction(ui->actionJoin); + } + + 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()).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() +{ + DesktopServices::openPath(m_worlds->dirs()[0].absolutePath(), true); //change this iy +} + +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()); + + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_datapackModel.reset(new DataPackFolderModel(folder, m_inst, isIndexed, true)); + + provider.addPageCreator([this] { return new DataPackPage(m_inst, 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->exec(); + + APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); +} + +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); + bool hasIcon = !index.data(MultiWorldList::IconFileRole).isNull(); + ui->actionReset_Icon->setEnabled(enable && hasIcon); + + auto supportsJoin = m_inst && m_inst->traits().contains("feature:is_quick_play_singleplayer"); + ui->actionJoin->setEnabled(enable && supportsJoin); + + if (!supportsJoin) { + ui->toolBar->removeAction(ui->actionJoin); + } +} + +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) { + m_worlds->installWorld(QFileInfo(filename)); + } + m_worlds->startWatching(); + } +} + +bool MultiWorldListPage::isWorldSafe(QModelIndex) +{ + return !m_inst->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 = (World*)worldVariant.value(); + bool ok = false; + QString name = + QInputDialog::getText(this, tr("World name"), tr("Enter a new name for the copy."), QLineEdit::Normal, world->name(), &ok); + + if (ok && name.length() > 0) { + world->install(m_worlds->dirs()[0].absolutePath(), name); //you know what to do iy + } +} + +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_actionRefresh_triggered() +{ + m_worlds->update(); +} + +void MultiWorldListPage::on_actionJoin_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) { + return; + } + auto worldVariant = m_worlds->data(index, MultiWorldList::ObjectRole); + auto world = (World*)worldVariant.value(); + APPLICATION->launch(m_inst, LaunchMode::Normal, std::make_shared(MinecraftTarget::parse(world->folderName(), true))); +} + +#include "WorldListPage.moc" //change this iy diff --git a/launcher/ui/MultiWorldListPage.h b/launcher/ui/MultiWorldListPage.h new file mode 100644 index 000000000..e9c9dd530 --- /dev/null +++ b/launcher/ui/MultiWorldListPage.h @@ -0,0 +1,107 @@ +// 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(MinecraftInstance* inst, 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; + + protected: + bool eventFilter(QObject* obj, QEvent* ev) override; + bool worldListFilter(QKeyEvent* ev); + QMenu* createPopupMenu() override; + + protected: + MinecraftInstance* m_inst; + + private: + QModelIndex getSelectedWorld(); + bool isWorldSafe(QModelIndex index); + bool worldSafetyNagQuestion(const QString& actionType); + void mceditError(); + + private: + Ui::WorldListPage* 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_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 ShowContextMenu(const QPoint& pos); +}; diff --git a/launcher/ui/MultiWorldListPage.ui b/launcher/ui/MultiWorldListPage.ui new file mode 100644 index 000000000..874721eee --- /dev/null +++ b/launcher/ui/MultiWorldListPage.ui @@ -0,0 +1,170 @@ + + + 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 + + + + + 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. + + + + + + WideBar + QToolBar +
ui/widgets/WideBar.h
+
+
+ + +