// SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 "WorldList.h" #include "Application.h" #include #include #include #include #include #include #include #include WorldList::WorldList(const QString &dir, std::shared_ptr instance) : QAbstractListModel(), m_instance(instance), m_dir(dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); is_watching = false; connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); } void WorldList::startWatching() { if(is_watching) { return; } update(); is_watching = m_watcher->addPath(m_dir.absolutePath()); if (is_watching) { qDebug() << "Started watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to start watching " << m_dir.absolutePath(); } } void WorldList::stopWatching() { if(!is_watching) { return; } is_watching = !m_watcher->removePath(m_dir.absolutePath()); if (!is_watching) { qDebug() << "Stopped watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to stop watching " << m_dir.absolutePath(); } } bool WorldList::update() { if (!isValid()) return false; QList newWorlds; m_dir.refresh(); auto folderContents = m_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(); worlds.swap(newWorlds); endResetModel(); return true; } void WorldList::directoryChanged(QString path) { update(); } bool WorldList::isValid() { return m_dir.exists() && m_dir.isReadable(); } QString WorldList::instDirPath() const { return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); } bool WorldList::deleteWorld(int index) { if (index >= worlds.size() || index < 0) return false; World &m = worlds[index]; if (m.destroy()) { beginRemoveRows(QModelIndex(), index, index); worlds.removeAt(index); endRemoveRows(); emit changed(); return true; } return false; } bool WorldList::deleteWorlds(int first, int last) { for (int i = first; i <= last; i++) { World &m = worlds[i]; m.destroy(); } beginRemoveRows(QModelIndex(), first, last); worlds.erase(worlds.begin() + first, worlds.begin() + last + 1); endRemoveRows(); emit changed(); return true; } bool WorldList::resetIcon(int row) { if (row >= worlds.size() || row < 0) return false; World &m = worlds[row]; if(m.resetIcon()) { emit dataChanged(index(row), index(row), {WorldList::IconFileRole}); return true; } return false; } int WorldList::columnCount(const QModelIndex &parent) const { return parent.isValid()? 0 : 5; } QVariant WorldList::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); int row = index.row(); int column = index.column(); if (row < 0 || row >= worlds.size()) return QVariant(); QLocale locale; auto & world = 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(instDirPath())) { 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: switch (column) { case SizeColumn: return QVariant::fromValue(world.bytes()); default: return data(index, Qt::DisplayRole); } case Qt::ToolTipRole: { if (column == InfoColumn) { if (world.isSymLinkUnder(instDirPath())) { 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(dir().absoluteFilePath(world.folderName())); } 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 WorldList::headerData(int section, 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(); } return QVariant(); } QStringList WorldList::mimeTypes() const { QStringList types; types << "text/uri-list"; return types; } class WorldMimeData : public QMimeData { Q_OBJECT public: WorldMimeData(QList worlds) { m_worlds = worlds; } QStringList formats() const { return QMimeData::formats() << "text/uri-list"; } protected: #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QVariant retrieveData(const QString &mimetype, QMetaType type) const #else QVariant retrieveData(const QString &mimetype, QVariant::Type type) const #endif { QList urls; for(auto &world: m_worlds) { if(!world.isValid() || !world.isOnFS()) continue; QString worldPath = world.container().absoluteFilePath(); qDebug() << worldPath; urls.append(QUrl::fromLocalFile(worldPath)); } const_cast(this)->setUrls(urls); return QMimeData::retrieveData(mimetype, type); } private: QList m_worlds; }; QMimeData *WorldList::mimeData(const QModelIndexList &indexes) const { if (indexes.size() == 0) return new QMimeData(); QList worlds; for(auto idx : indexes) { if(idx.column() != 0) continue; int row = idx.row(); if (row < 0 || row >= this->worlds.size()) continue; worlds.append(this->worlds[row]); } if(!worlds.size()) { return new QMimeData(); } return new WorldMimeData(worlds); } Qt::ItemFlags WorldList::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 WorldList::supportedDragActions() const { // move to other mod lists or VOID return Qt::MoveAction; } Qt::DropActions WorldList::supportedDropActions() const { // copy from outside, move from within and other mod lists return Qt::CopyAction | Qt::MoveAction; } void WorldList::installWorld(QFileInfo filename) { qDebug() << "installing: " << filename.absoluteFilePath(); World w(filename); if(!w.isValid()) { return; } w.install(m_dir.absolutePath()); } bool WorldList::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 = is_watching; 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_dir.entryInfoList().contains(worldInfo)) { installWorld(worldInfo); } } if (was_watching) startWatching(); return true; } return false; } #include "WorldList.moc"