From a1fd50e920eba0f198b898e5df4ff5f60424d355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Wed, 9 Sep 2015 23:53:33 +0200 Subject: [PATCH] GH-1227: World import using drag and drop - zip files and folders --- application/GuiUtil.cpp | 22 ++- application/GuiUtil.h | 2 +- application/pages/LegacyJarModPage.cpp | 2 +- application/pages/ModFolderPage.cpp | 7 +- application/pages/VersionPage.cpp | 2 +- application/pages/WorldListPage.cpp | 20 ++- application/pages/WorldListPage.h | 1 + application/pages/WorldListPage.ui | 37 +++-- depends/util/include/pathutils.h | 2 +- depends/util/src/pathutils.cpp | 25 ++- logic/minecraft/ModList.cpp | 1 + logic/minecraft/World.cpp | 204 ++++++++++++++++--------- logic/minecraft/World.h | 10 +- logic/minecraft/WorldList.cpp | 116 +++++++++++--- logic/minecraft/WorldList.h | 15 +- 15 files changed, 345 insertions(+), 121 deletions(-) diff --git a/application/GuiUtil.cpp b/application/GuiUtil.cpp index 922d38fa8..edfe9951f 100644 --- a/application/GuiUtil.cpp +++ b/application/GuiUtil.cpp @@ -51,14 +51,13 @@ void GuiUtil::setClipboardText(const QString &text) QApplication::clipboard()->setText(text); } -QStringList GuiUtil::BrowseForMods(QString context, QString caption, QString filter, - QWidget *parentWidget) + +QStringList GuiUtil::BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget) { static QMap savedPaths; QFileDialog w(parentWidget, caption); QSet locations; - QString modsFolder = MMC->settings()->get("CentralModsDir").toString(); auto f = [&](QStandardPaths::StandardLocation l) { QString location = QStandardPaths::writableLocation(l); @@ -76,19 +75,30 @@ QStringList GuiUtil::BrowseForMods(QString context, QString caption, QString fil { urls.append(QUrl::fromLocalFile(location)); } - urls.append(QUrl::fromLocalFile(modsFolder)); + urls.append(QUrl::fromLocalFile(defaultPath)); w.setFileMode(QFileDialog::ExistingFiles); w.setAcceptMode(QFileDialog::AcceptOpen); w.setNameFilter(filter); + + QString pathToOpen; if(savedPaths.contains(context)) { - w.setDirectory(savedPaths[context]); + pathToOpen = savedPaths[context]; } else { - w.setDirectory(modsFolder); + pathToOpen = defaultPath; } + if(!pathToOpen.isEmpty()) + { + QFileInfo finfo(pathToOpen); + if(finfo.exists() && finfo.isDir()) + { + w.setDirectory(finfo.absoluteFilePath()); + } + } + w.setSidebarUrls(urls); if (w.exec()) diff --git a/application/GuiUtil.h b/application/GuiUtil.h index 4c8bdaa71..381683e4e 100644 --- a/application/GuiUtil.h +++ b/application/GuiUtil.h @@ -6,5 +6,5 @@ namespace GuiUtil { void uploadPaste(const QString &text, QWidget *parentWidget); void setClipboardText(const QString &text); -QStringList BrowseForMods(QString context, QString caption, QString filter, QWidget *parentWidget); +QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget); } diff --git a/application/pages/LegacyJarModPage.cpp b/application/pages/LegacyJarModPage.cpp index 92977de9c..87a9f3b58 100644 --- a/application/pages/LegacyJarModPage.cpp +++ b/application/pages/LegacyJarModPage.cpp @@ -100,7 +100,7 @@ bool LegacyJarModPage::eventFilter(QObject *obj, QEvent *ev) void LegacyJarModPage::on_addJarBtn_clicked() { - auto list = GuiUtil::BrowseForMods("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), this->parentWidget()); + auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), MMC->settings()->get("CentralModsDir").toString(), this->parentWidget()); if(!list.empty()) { m_jarmods->stopWatching(); diff --git a/application/pages/ModFolderPage.cpp b/application/pages/ModFolderPage.cpp index a70c1620c..9a2a43a48 100644 --- a/application/pages/ModFolderPage.cpp +++ b/application/pages/ModFolderPage.cpp @@ -131,12 +131,13 @@ bool ModFolderPage::eventFilter(QObject *obj, QEvent *ev) void ModFolderPage::on_addModBtn_clicked() { - auto list = GuiUtil::BrowseForMods( + auto list = GuiUtil::BrowseForFiles( m_helpName, tr("Select %1", "Select whatever type of files the page contains. Example: 'Loader Mods'") .arg(m_displayName), - m_filter.arg(m_displayName), this->parentWidget()); + m_filter.arg(m_displayName), MMC->settings()->get("CentralModsDir").toString(), + this->parentWidget()); if (!list.empty()) { m_mods->stopWatching(); @@ -146,8 +147,8 @@ void ModFolderPage::on_addModBtn_clicked() } m_mods->startWatching(); } - } + void ModFolderPage::on_rmModBtn_clicked() { int first, last; diff --git a/application/pages/VersionPage.cpp b/application/pages/VersionPage.cpp index d56bbd283..360916306 100644 --- a/application/pages/VersionPage.cpp +++ b/application/pages/VersionPage.cpp @@ -180,7 +180,7 @@ void VersionPage::on_jarmodBtn_clicked() nagShown = true; } } - auto list = GuiUtil::BrowseForMods("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), this->parentWidget()); + auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), MMC->settings()->get("CentralModsDir").toString(), this->parentWidget()); if(!list.empty()) { m_version->installJarMods(list); diff --git a/application/pages/WorldListPage.cpp b/application/pages/WorldListPage.cpp index 8b95b2b05..be1d58b8f 100644 --- a/application/pages/WorldListPage.cpp +++ b/application/pages/WorldListPage.cpp @@ -25,6 +25,7 @@ #include "MultiMC.h" +#include WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr worlds, QString id, QString iconName, QString displayName, QString helpPage, @@ -67,8 +68,6 @@ WorldListPage::~WorldListPage() bool WorldListPage::shouldDisplay() const { - if (m_inst) - return !m_inst->isRunning(); return true; } @@ -200,3 +199,20 @@ void WorldListPage::worldChanged(const QModelIndex ¤t, const QModelIndex & ui->mcEditBtn->setEnabled(enable); ui->rmWorldBtn->setEnabled(enable); } + +void WorldListPage::on_addBtn_clicked() +{ + auto list = GuiUtil::BrowseForFiles( + m_helpName, + 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(); + } +} \ No newline at end of file diff --git a/application/pages/WorldListPage.h b/application/pages/WorldListPage.h index c79f1be6a..f3111f93e 100644 --- a/application/pages/WorldListPage.h +++ b/application/pages/WorldListPage.h @@ -81,6 +81,7 @@ private slots: void on_copySeedBtn_clicked(); void on_mcEditBtn_clicked(); void on_rmWorldBtn_clicked(); + void on_addBtn_clicked(); void on_viewFolderBtn_clicked(); void worldChanged(const QModelIndex ¤t, const QModelIndex &previous); }; diff --git a/application/pages/WorldListPage.ui b/application/pages/WorldListPage.ui index 9d396fa6d..ecd9709f7 100644 --- a/application/pages/WorldListPage.ui +++ b/application/pages/WorldListPage.ui @@ -48,7 +48,7 @@ true - QAbstractItemView::DropOnly + QAbstractItemView::DragDrop true @@ -63,6 +63,23 @@ + + + + Add + + + + + + + &Remove + + + + + + @@ -77,13 +94,6 @@ - - - - &Remove - - - @@ -112,12 +122,21 @@ + + + LineSeparator + QWidget +
widgets/LineSeparator.h
+ 1 +
+
tabWidget worldTreeView + addBtn + rmWorldBtn mcEditBtn copySeedBtn - rmWorldBtn viewFolderBtn diff --git a/depends/util/include/pathutils.h b/depends/util/include/pathutils.h index f31b96d1e..82d55cd7b 100644 --- a/depends/util/include/pathutils.h +++ b/depends/util/include/pathutils.h @@ -54,7 +54,7 @@ LIBUTIL_EXPORT bool ensureFolderPathExists(QString filenamepath); /** * Copy a folder recursively */ -LIBUTIL_EXPORT bool copyPath(QString src, QString dst, bool follow_symlinks = true); +LIBUTIL_EXPORT bool copyPath(const QString &src, const QString &dst, bool follow_symlinks = true); /** * Delete a folder recursively diff --git a/depends/util/src/pathutils.cpp b/depends/util/src/pathutils.cpp index ce9138be0..35c7f9016 100644 --- a/depends/util/src/pathutils.cpp +++ b/depends/util/src/pathutils.cpp @@ -81,16 +81,24 @@ QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) QString DirNameFromString(QString string, QString inDir) { int num = 0; - QString dirName = RemoveInvalidFilenameChars(string, '-'); - while (QFileInfo(PathCombine(inDir, dirName)).exists()) + QString baseName = RemoveInvalidFilenameChars(string, '-'); + QString dirName; + do { - num++; - dirName = RemoveInvalidFilenameChars(dirName, '-') + QString::number(num); + if(num == 0) + { + dirName = baseName; + } + else + { + dirName = baseName + QString::number(num);; + } // If it's over 9000 if (num > 9000) return ""; - } + num++; + } while (QFileInfo(PathCombine(inDir, dirName)).exists()); return dirName; } @@ -112,7 +120,7 @@ bool ensureFolderPathExists(QString foldernamepath) return success; } -bool copyPath(QString src, QString dst, bool follow_symlinks) +bool copyPath(const QString &src, const QString &dst, bool follow_symlinks) { //NOTE always deep copy on windows. the alternatives are too messy. #if defined Q_OS_WIN32 @@ -127,21 +135,26 @@ bool copyPath(QString src, QString dst, bool follow_symlinks) bool OK = true; + qDebug() << "Looking at " << dir.absolutePath(); foreach(QString f, dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) { QString inner_src = src + QDir::separator() + f; QString inner_dst = dst + QDir::separator() + f; + qDebug() << f << "translates to"<< inner_src << "to" << inner_dst; QFileInfo fileInfo(inner_src); if(!follow_symlinks && fileInfo.isSymLink()) { + qDebug() << "creating symlink" << inner_src << " - " << inner_dst; OK &= QFile::link(fileInfo.symLinkTarget(),inner_dst); } else if (fileInfo.isDir()) { + qDebug() << "recursing" << inner_src << " - " << inner_dst; OK &= copyPath(inner_src, inner_dst, follow_symlinks); } else if (fileInfo.isFile()) { + qDebug() << "copying file" << inner_src << " - " << inner_dst; OK &= QFile::copy(inner_src, inner_dst); } else diff --git a/logic/minecraft/ModList.cpp b/logic/minecraft/ModList.cpp index a77c58907..8f1bc0417 100644 --- a/logic/minecraft/ModList.cpp +++ b/logic/minecraft/ModList.cpp @@ -539,6 +539,7 @@ QMimeData *ModList::mimeData(const QModelIndexList &indexes) const data->setText(params.join('|')); return data; } + bool ModList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { diff --git a/logic/minecraft/World.cpp b/logic/minecraft/World.cpp index 977a03cff..3b66f113e 100644 --- a/logic/minecraft/World.cpp +++ b/logic/minecraft/World.cpp @@ -20,10 +20,13 @@ #include #include "GZip.h" +#include #include #include #include #include +#include +#include World::World(const QFileInfo &file) { @@ -32,8 +35,20 @@ World::World(const QFileInfo &file) void World::repath(const QFileInfo &file) { - m_file = file; + m_containerFile = file; m_folderName = file.fileName(); + if(file.isFile() && file.suffix() == "zip") + { + readFromZip(file); + } + else if(file.isDir()) + { + readFromFS(file); + } +} + +void World::readFromFS(const QFileInfo &file) +{ QDir worldDir(file.filePath()); is_valid = file.isDir() && worldDir.exists("level.dat"); if(!is_valid) @@ -48,66 +63,123 @@ void World::repath(const QFileInfo &file) { return; } + QFileInfo finfo(fullFilePath); + levelDatTime = finfo.lastModified(); + parseLevelDat(f.readAll()); +} +void World::readFromZip(const QFileInfo &file) +{ + QuaZip zip(file.absoluteFilePath()); + is_valid = zip.open(QuaZip::mdUnzip); + if (!is_valid) + { + return; + } + QuaZipFile zippedFile(&zip); + // read the install profile + is_valid = zip.setCurrentFile("level.dat"); + if (!is_valid) + { + return; + } + is_valid = zippedFile.open(QIODevice::ReadOnly); + QuaZipFileInfo64 levelDatInfo; + zippedFile.getFileInfo(&levelDatInfo); + auto modTime = levelDatInfo.getNTFSmTime(); + if(!modTime.isValid()) + { + modTime = levelDatInfo.dateTime; + } + levelDatTime = modTime; + if (!is_valid) + { + return; + } + parseLevelDat(zippedFile.readAll()); + zippedFile.close(); +} + +bool World::install(QString to) +{ + auto finalPath = PathCombine(to, DirNameFromString(m_actualName, to)); + if(!ensureFolderPathExists(finalPath)) + { + return false; + } + if(m_containerFile.isFile()) + { + // FIXME: check if this is OK. + return !MMCZip::extractDir(m_containerFile.absoluteFilePath(), finalPath).isEmpty(); + } + else if(m_containerFile.isDir()) + { + QString from = m_containerFile.filePath(); + return copyPath(from, finalPath); + } + return false; +} + +static QString read_string (nbt::value& parent, const char * name, const QString & fallback = QString()) +{ + try + { + auto &namedValue = parent.at(name); + if(namedValue.get_type() != nbt::tag_type::String) + { + return fallback; + } + auto & tag_str = namedValue.as(); + return QString::fromStdString(tag_str.get()); + } + catch(std::out_of_range e) + { + // fallback for old world formats + qWarning() << "String NBT tag" << name << "could not be found. Defaulting to" << fallback; + return fallback; + } + catch(std::bad_cast e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to string. Defaulting to" << fallback; + return fallback; + } +}; + +static int64_t read_long (nbt::value& parent, const char * name, const int64_t & fallback = 0) +{ + try + { + auto &namedValue = parent.at(name); + if(namedValue.get_type() != nbt::tag_type::Long) + { + return fallback; + } + auto & tag_str = namedValue.as(); + return tag_str.get(); + } + catch(std::out_of_range e) + { + // fallback for old world formats + qWarning() << "Long NBT tag" << name << "could not be found. Defaulting to" << fallback; + return fallback; + } + catch(std::bad_cast e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to long. Defaulting to" << fallback; + return fallback; + } +}; + +void World::parseLevelDat(QByteArray data) +{ QByteArray output; - is_valid = GZip::inflate(f.readAll(), output); + is_valid = GZip::inflate(data, output); if(!is_valid) { return; } - f.close(); - - auto read_string = [](nbt::value& parent, const char * name, const QString & fallback = QString()) -> QString - { - try - { - auto &namedValue = parent.at(name); - if(namedValue.get_type() != nbt::tag_type::String) - { - return fallback; - } - auto & tag_str = namedValue.as(); - return QString::fromStdString(tag_str.get()); - } - catch(std::out_of_range e) - { - // fallback for old world formats - qWarning() << "String NBT tag" << name << "could not be found. Defaulting to" << fallback; - return fallback; - } - catch(std::bad_cast e) - { - // type mismatch - qWarning() << "NBT tag" << name << "could not be converted to string. Defaulting to" << fallback; - return fallback; - } - }; - - auto read_long = [](nbt::value& parent, const char * name, const int64_t & fallback = 0) -> int64_t - { - try - { - auto &namedValue = parent.at(name); - if(namedValue.get_type() != nbt::tag_type::Long) - { - return fallback; - } - auto & tag_str = namedValue.as(); - return tag_str.get(); - } - catch(std::out_of_range e) - { - // fallback for old world formats - qWarning() << "Long NBT tag" << name << "could not be found. Defaulting to" << fallback; - return fallback; - } - catch(std::bad_cast e) - { - // type mismatch - qWarning() << "NBT tag" << name << "could not be converted to long. Defaulting to" << fallback; - return fallback; - } - }; try { @@ -138,8 +210,7 @@ void World::repath(const QFileInfo &file) int64_t temp = read_long(val, "LastPlayed", 0); if(temp == 0) { - QFileInfo finfo(fullFilePath); - m_lastPlayed = finfo.lastModified(); + m_lastPlayed = levelDatTime; } else { @@ -164,11 +235,11 @@ bool World::replace(World &with) { if (!destroy()) return false; - bool success = copyPath(with.m_file.filePath(), m_file.path()); + bool success = copyPath(with.m_containerFile.filePath(), m_containerFile.path()); if (success) { m_folderName = with.m_folderName; - m_file.refresh(); + m_containerFile.refresh(); } return success; } @@ -176,18 +247,15 @@ bool World::replace(World &with) bool World::destroy() { if(!is_valid) return false; - if (m_file.isDir()) + if (m_containerFile.isDir()) { - QDir d(m_file.filePath()); - if (d.removeRecursively()) - { - return true; - } - return false; + QDir d(m_containerFile.filePath()); + return d.removeRecursively(); } - else + else if(m_containerFile.isFile()) { - return false; + QFile file(m_containerFile.absoluteFilePath()); + return file.remove(); } return true; } diff --git a/logic/minecraft/World.h b/logic/minecraft/World.h index 91cb2a832..27184e051 100644 --- a/logic/minecraft/World.h +++ b/logic/minecraft/World.h @@ -48,15 +48,23 @@ public: // change the world's filesystem path (used by world lists for *MAGIC* purposes) void repath(const QFileInfo &file); + bool install(QString to); + // WEAK compare operator - used for replacing worlds bool operator==(const World &other) const; bool strongCompare(const World &other) const; +private: + void readFromZip(const QFileInfo &file); + void readFromFS(const QFileInfo &file); + void parseLevelDat(QByteArray data); + protected: - QFileInfo m_file; + QFileInfo m_containerFile; QString m_folderName; QString m_actualName; + QDateTime levelDatTime; QDateTime m_lastPlayed; int64_t m_randomSeed = 0; bool is_valid = false; diff --git a/logic/minecraft/WorldList.cpp b/logic/minecraft/WorldList.cpp index 7066093c6..1c25f3b10 100644 --- a/logic/minecraft/WorldList.cpp +++ b/logic/minecraft/WorldList.cpp @@ -62,40 +62,28 @@ void WorldList::stopWatching() } } -void WorldList::internalSort(QList &what) -{ - auto predicate = [](const World &left, const World &right) - { - return left.folderName().localeAwareCompare(right.folderName()) < 0; - }; - std::sort(what.begin(), what.end(), predicate); -} - bool WorldList::update() { if (!isValid()) return false; - QList orderedWorlds; QList newWorlds; m_dir.refresh(); auto folderContents = m_dir.entryInfoList(); // if there are any untracked files... - if (folderContents.size()) + for (QFileInfo entry : folderContents) { - // the order surely changed! - for (auto entry : folderContents) + if(!entry.isDir()) + continue; + + World w(entry); + if(w.isValid()) { - World w(entry); - if(w.isValid()) { - newWorlds.append(w); - } + newWorlds.append(w); } - internalSort(newWorlds); - orderedWorlds.append(newWorlds); } beginResetModel(); - worlds.swap(orderedWorlds); + worlds.swap(newWorlds); endResetModel(); return true; } @@ -232,6 +220,7 @@ QStringList WorldList::mimeTypes() const { QStringList types; types << "text/plain"; + types << "text/uri-list"; return types; } @@ -250,3 +239,90 @@ QMimeData *WorldList::mimeData(const QModelIndexList &indexes) const data->setText(QString::number(row)); return data; } + +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, int row, int column, + 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); + installWorld(worldInfo); + } + if (was_watching) + startWatching(); + return true; + } + /* + else if (data->hasText()) + { + QString sourcestr = data->text(); + auto list = sourcestr.split('|'); + if (list.size() != 2) + return false; + QString remoteId = list[0]; + int remoteIndex = list[1].toInt(); + qDebug() << "move: " << sourcestr; + // no moving of things between two lists + if (remoteId != m_list_id) + return false; + // no point moving to the same place... + if (row == remoteIndex) + return false; + // otherwise, move the mod :D + moveModTo(remoteIndex, row); + return true; + } + */ + return false; + +} diff --git a/logic/minecraft/WorldList.h b/logic/minecraft/WorldList.h index 7f119e81d..90c7c6ed0 100644 --- a/logic/minecraft/WorldList.h +++ b/logic/minecraft/WorldList.h @@ -19,6 +19,7 @@ #include #include #include +#include #include "minecraft/World.h" #include "multimc_logic_export.h" @@ -71,16 +72,28 @@ public: /// 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); /// 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(); @@ -97,8 +110,6 @@ public: return worlds; } -private: - void internalSort(QList &what); private slots: void directoryChanged(QString path);