From c7c81463fd3ab01c9e096f75e7e8ad8b50902a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 19 Apr 2015 04:19:29 +0200 Subject: [PATCH] GH-885 export dialog for filtering exported files Includes implementation of a separator based prefix tree and some related bits --- application/CMakeLists.txt | 3 + application/MainWindow.cpp | 26 +- application/dialogs/ExportInstanceDialog.cpp | 434 +++++++++++++++++++ application/dialogs/ExportInstanceDialog.h | 54 +++ application/dialogs/ExportInstanceDialog.ui | 83 ++++ logic/CMakeLists.txt | 5 + logic/MMCStrings.cpp | 76 ++++ logic/MMCStrings.h | 8 + logic/MMCZip.cpp | 26 +- logic/MMCZip.h | 6 +- logic/SeparatorPrefixTree.h | 298 +++++++++++++ 11 files changed, 985 insertions(+), 34 deletions(-) create mode 100644 application/dialogs/ExportInstanceDialog.cpp create mode 100644 application/dialogs/ExportInstanceDialog.h create mode 100644 application/dialogs/ExportInstanceDialog.ui create mode 100644 logic/MMCStrings.cpp create mode 100644 logic/MMCStrings.h create mode 100644 logic/SeparatorPrefixTree.h diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index 3643feee..b9a671b3 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -207,6 +207,8 @@ SET(MULTIMC_SOURCES dialogs/CustomMessageBox.h dialogs/EditAccountDialog.cpp dialogs/EditAccountDialog.h + dialogs/ExportInstanceDialog.cpp + dialogs/ExportInstanceDialog.h dialogs/IconPickerDialog.cpp dialogs/IconPickerDialog.h dialogs/LoginDialog.cpp @@ -290,6 +292,7 @@ SET(MULTIMC_UIS dialogs/IconPickerDialog.ui dialogs/AccountSelectDialog.ui dialogs/EditAccountDialog.ui + dialogs/ExportInstanceDialog.ui dialogs/LoginDialog.ui dialogs/UpdateDialog.ui dialogs/NotificationDialog.ui diff --git a/application/MainWindow.cpp b/application/MainWindow.cpp index ff890d6e..ca6754b5 100644 --- a/application/MainWindow.cpp +++ b/application/MainWindow.cpp @@ -349,6 +349,7 @@ namespace Ui { #include "dialogs/UpdateDialog.h" #include "dialogs/EditAccountDialog.h" #include "dialogs/NotificationDialog.h" +#include "dialogs/ExportInstanceDialog.h" #include "pages/global/MultiMCPage.h" #include "pages/global/ExternalToolsPage.h" @@ -1475,29 +1476,8 @@ void MainWindow::on_actionExportInstance_triggered() { if (m_selectedInstance) { - auto name = RemoveInvalidFilenameChars(m_selectedInstance->name()); - - const QString output = QFileDialog::getSaveFileName(this, tr("Export %1") - .arg(m_selectedInstance->name()), - PathCombine(QDir::homePath(), name + ".zip") , "Zip (*.zip)"); - if (output.isNull()) - { - return; - } - if (QFile::exists(output)) - { - int ret = QMessageBox::question(this, tr("Overwrite?"), tr("This file already exists. Do you want to overwrite it?"), - QMessageBox::No, QMessageBox::Yes); - if (ret == QMessageBox::No) - { - return; - } - } - - if (!MMCZip::compressDir(output, m_selectedInstance->instanceRoot(), name)) - { - QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); - } + ExportInstanceDialog dlg(m_selectedInstance, this); + dlg.exec(); } } diff --git a/application/dialogs/ExportInstanceDialog.cpp b/application/dialogs/ExportInstanceDialog.cpp new file mode 100644 index 00000000..5d24c54b --- /dev/null +++ b/application/dialogs/ExportInstanceDialog.cpp @@ -0,0 +1,434 @@ +/* Copyright 2013-2015 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 "ExportInstanceDialog.h" +#include "ui_ExportInstanceDialog.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include "MMCStrings.h" +#include "SeparatorPrefixTree.h" + +class PackIgnoreProxy : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + PackIgnoreProxy(InstancePtr instance, QObject *parent) : QSortFilterProxyModel(parent) + { + m_instance = instance; + } + // NOTE: Sadly, we have to do sorting ourselves. + bool lessThan(const QModelIndex &left, const QModelIndex &right) const + { + QFileSystemModel *fsm = qobject_cast(sourceModel()); + if (!fsm) + { + return QSortFilterProxyModel::lessThan(left, right); + } + bool asc = sortOrder() == Qt::AscendingOrder ? true : false; + + QFileInfo leftFileInfo = fsm->fileInfo(left); + QFileInfo rightFileInfo = fsm->fileInfo(right); + + if (!leftFileInfo.isDir() && rightFileInfo.isDir()) + { + return !asc; + } + if (leftFileInfo.isDir() && !rightFileInfo.isDir()) + { + return asc; + } + + // sort and proxy model breaks the original model... + if (sortColumn() == 0) + { + return Strings::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), + Qt::CaseInsensitive) < 0; + } + if (sortColumn() == 1) + { + auto leftSize = leftFileInfo.size(); + auto rightSize = rightFileInfo.size(); + if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) + { + return Strings::naturalCompare(leftFileInfo.fileName(), + rightFileInfo.fileName(), + Qt::CaseInsensitive) < 0 + ? asc + : !asc; + } + return leftSize < rightSize; + } + return QSortFilterProxyModel::lessThan(left, right); + } + + virtual Qt::ItemFlags flags(const QModelIndex &index) const + { + if (!index.isValid()) + return Qt::NoItemFlags; + + auto sourceIndex = mapToSource(index); + Qt::ItemFlags flags = sourceIndex.flags(); + if (index.column() == 0) + { + flags |= Qt::ItemIsUserCheckable; + if (sourceIndex.model()->hasChildren(sourceIndex)) + { + flags |= Qt::ItemIsTristate; + } + } + + return flags; + } + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const + { + QModelIndex sourceIndex = mapToSource(index); + + if (index.column() == 0 && role == Qt::CheckStateRole) + { + QFileSystemModel *fsm = qobject_cast(sourceModel()); + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + auto cover = blocked.cover(blockedPath); + if (!cover.isNull()) + { + return QVariant(Qt::Unchecked); + } + else if (blocked.exists(blockedPath)) + { + return QVariant(Qt::PartiallyChecked); + } + else + { + return QVariant(Qt::Checked); + } + } + + return sourceIndex.data(role); + } + + virtual bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole) + { + if (index.column() == 0 && role == Qt::CheckStateRole) + { + Qt::CheckState state = static_cast(value.toInt()); + return setFilterState(index, state); + } + + QModelIndex sourceIndex = mapToSource(index); + return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role); + } + + QString relPath(const QString &path) const + { + QString prefix = QDir().absoluteFilePath(m_instance->instanceRoot()); + prefix += '/'; + if (!path.startsWith(prefix)) + { + return QString(); + } + return path.mid(prefix.size()); + } + + bool setFilterState(QModelIndex index, Qt::CheckState state) + { + QFileSystemModel *fsm = qobject_cast(sourceModel()); + + if (!fsm) + { + return false; + } + + QModelIndex sourceIndex = mapToSource(index); + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + bool changed = false; + if (state == Qt::Unchecked) + { + // blocking a path + auto &node = blocked.insert(blockedPath); + // get rid of all blocked nodes below + node.clear(); + changed = true; + } + else if (state == Qt::Checked || state == Qt::PartiallyChecked) + { + if (!blocked.remove(blockedPath)) + { + auto cover = blocked.cover(blockedPath); + qDebug() << "Blocked by cover" << cover; + // uncover + blocked.remove(cover); + // block all contents, except for any cover + QModelIndex rootIndex = + fsm->index(PathCombine(m_instance->instanceRoot(), cover)); + QModelIndex doing = rootIndex; + int row = 0; + QStack todo; + while (1) + { + auto node = doing.child(row, 0); + if (!node.isValid()) + { + if (!todo.size()) + { + break; + } + else + { + doing = todo.pop(); + row = 0; + continue; + } + } + auto relpath = relPath(fsm->filePath(node)); + if (blockedPath.startsWith(relpath)) // cover found? + { + // continue processing cover later + todo.push(node); + } + else + { + // or just block this one. + blocked.insert(relpath); + } + row++; + } + } + changed = true; + } + if (changed) + { + // update the thing + emit dataChanged(index, index, {Qt::CheckStateRole}); + // update everything above index + QModelIndex up = index.parent(); + while (1) + { + if (!up.isValid()) + break; + emit dataChanged(up, up, {Qt::CheckStateRole}); + up = up.parent(); + } + // and everything below the index + QModelIndex doing = index; + int row = 0; + QStack todo; + while (1) + { + auto node = doing.child(row, 0); + if (!node.isValid()) + { + if (!todo.size()) + { + break; + } + else + { + doing = todo.pop(); + row = 0; + continue; + } + } + emit dataChanged(node, node, {Qt::CheckStateRole}); + todo.push(node); + row++; + } + // siblings and unrelated nodes are ignored + } + return true; + } + + bool shouldExpand(QModelIndex index) + { + QModelIndex sourceIndex = mapToSource(index); + QFileSystemModel *fsm = qobject_cast(sourceModel()); + if (!fsm) + { + return false; + } + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + auto found = blocked.find(blockedPath); + if(found) + { + return !found->leaf(); + } + return false; + } + + void setBlockedPaths(QStringList paths) + { + beginResetModel(); + blocked.clear(); + blocked.insert(paths); + endResetModel(); + } + + const SeparatorPrefixTree<'/'> & blockedPaths() const + { + return blocked; + } + +protected: + bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const + { + Q_UNUSED(source_parent) + + // adjust the columns you want to filter out here + // return false for those that will be hidden + if (source_column == 2 || source_column == 3) + return false; + + return true; + } + +private: + InstancePtr m_instance; + SeparatorPrefixTree<'/'> blocked; +}; + +ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent) + : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance) +{ + ui->setupUi(this); + auto model = new QFileSystemModel(this); + proxyModel = new PackIgnoreProxy(m_instance, this); + loadPackIgnore(); + proxyModel->setSourceModel(model); + auto root = instance->instanceRoot(); + ui->treeView->setModel(proxyModel); + ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root))); + + connect(proxyModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int))); + + model->setRootPath(root); + auto headerView = ui->treeView->header(); + headerView->setSectionResizeMode(QHeaderView::ResizeToContents); + headerView->setSectionResizeMode(0, QHeaderView::Stretch); +} + +ExportInstanceDialog::~ExportInstanceDialog() +{ + delete ui; +} + +bool ExportInstanceDialog::doExport() +{ + auto name = RemoveInvalidFilenameChars(m_instance->name()); + + const QString output = QFileDialog::getSaveFileName( + this, tr("Export %1").arg(m_instance->name()), + PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)"); + if (output.isNull()) + { + return false; + } + if (QFile::exists(output)) + { + int ret = + QMessageBox::question(this, tr("Overwrite?"), + tr("This file already exists. Do you want to overwrite it?"), + QMessageBox::No, QMessageBox::Yes); + if (ret == QMessageBox::No) + { + return false; + } + } + + if (!MMCZip::compressDir(output, m_instance->instanceRoot(), name, &proxyModel->blockedPaths())) + { + QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); + return false; + } + return true; +} + +void ExportInstanceDialog::done(int result) +{ + savePackIgnore(); + if (result == QDialog::Accepted) + { + if (doExport()) + { + QDialog::done(QDialog::Accepted); + return; + } + else + { + return; + } + } + QDialog::done(result); +} + +void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) +{ + //WARNING: possible off-by-one? + for(int i = top; i < bottom; i++) + { + auto node = parent.child(i, 0); + if(proxyModel->shouldExpand(node)) + { + auto expNode = node.parent(); + if(!expNode.isValid()) + { + continue; + } + ui->treeView->expand(node); + } + } +} + +QString ExportInstanceDialog::ignoreFileName() +{ + return PathCombine(m_instance->instanceRoot(), ".packignore"); +} + +void ExportInstanceDialog::loadPackIgnore() +{ + auto filename = ignoreFileName(); + QFile ignoreFile(filename); + if(!ignoreFile.open(QIODevice::ReadOnly)) + { + return; + } + auto data = ignoreFile.readAll(); + auto string = QString::fromUtf8(data); + proxyModel->setBlockedPaths(string.split('\n', QString::SkipEmptyParts)); +} + +void ExportInstanceDialog::savePackIgnore() +{ + auto filename = ignoreFileName(); + QSaveFile ignoreFile(filename); + if(!ignoreFile.open(QIODevice::WriteOnly)) + { + ignoreFile.cancelWriting(); + } + auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8(); + ignoreFile.write(data); + ignoreFile.commit(); +} + +#include "ExportInstanceDialog.moc" diff --git a/application/dialogs/ExportInstanceDialog.h b/application/dialogs/ExportInstanceDialog.h new file mode 100644 index 00000000..021927a3 --- /dev/null +++ b/application/dialogs/ExportInstanceDialog.h @@ -0,0 +1,54 @@ +/* Copyright 2013-2015 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 + +class BaseInstance; +class PackIgnoreProxy; +typedef std::shared_ptr InstancePtr; + +namespace Ui +{ +class ExportInstanceDialog; +} + +class ExportInstanceDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ExportInstanceDialog(InstancePtr instance, QWidget *parent = 0); + ~ExportInstanceDialog(); + + virtual void done(int result); + +private: + bool doExport(); + void loadPackIgnore(); + void savePackIgnore(); + QString ignoreFileName(); + +private: + Ui::ExportInstanceDialog *ui; + InstancePtr m_instance; + PackIgnoreProxy * proxyModel; + +private slots: + void rowsInserted(QModelIndex parent, int top, int bottom); +}; diff --git a/application/dialogs/ExportInstanceDialog.ui b/application/dialogs/ExportInstanceDialog.ui new file mode 100644 index 00000000..bcd4e84a --- /dev/null +++ b/application/dialogs/ExportInstanceDialog.ui @@ -0,0 +1,83 @@ + + + ExportInstanceDialog + + + + 0 + 0 + 720 + 625 + + + + Export Instance + + + + + + true + + + QAbstractItemView::ExtendedSelection + + + true + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + treeView + + + + + buttonBox + accepted() + ExportInstanceDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ExportInstanceDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/logic/CMakeLists.txt b/logic/CMakeLists.txt index e3b52ec5..6389159e 100644 --- a/logic/CMakeLists.txt +++ b/logic/CMakeLists.txt @@ -17,6 +17,11 @@ SET(LOGIC_SOURCES MMCError.h MMCZip.h MMCZip.cpp + MMCStrings.h + MMCStrings.cpp + + # Prefix tree where node names are strings between separators + SeparatorPrefixTree.h # WARNING: globals live here Env.h diff --git a/logic/MMCStrings.cpp b/logic/MMCStrings.cpp new file mode 100644 index 00000000..c50d596e --- /dev/null +++ b/logic/MMCStrings.cpp @@ -0,0 +1,76 @@ +#include "MMCStrings.h" + +/// TAKEN FROM Qt, because it doesn't expose it intelligently +static inline QChar getNextChar(const QString &s, int location) +{ + return (location < s.length()) ? s.at(location) : QChar(); +} + +/// TAKEN FROM Qt, because it doesn't expose it intelligently +int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs) +{ + for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2) + { + // skip spaces, tabs and 0's + QChar c1 = getNextChar(s1, l1); + while (c1.isSpace()) + c1 = getNextChar(s1, ++l1); + QChar c2 = getNextChar(s2, l2); + while (c2.isSpace()) + c2 = getNextChar(s2, ++l2); + + if (c1.isDigit() && c2.isDigit()) + { + while (c1.digitValue() == 0) + c1 = getNextChar(s1, ++l1); + while (c2.digitValue() == 0) + c2 = getNextChar(s2, ++l2); + + int lookAheadLocation1 = l1; + int lookAheadLocation2 = l2; + int currentReturnValue = 0; + // find the last digit, setting currentReturnValue as we go if it isn't equal + for (QChar lookAhead1 = c1, lookAhead2 = c2; + (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); + lookAhead1 = getNextChar(s1, ++lookAheadLocation1), + lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) + { + bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit(); + bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit(); + if (!is1ADigit && !is2ADigit) + break; + if (!is1ADigit) + return -1; + if (!is2ADigit) + return 1; + if (currentReturnValue == 0) + { + if (lookAhead1 < lookAhead2) + { + currentReturnValue = -1; + } + else if (lookAhead1 > lookAhead2) + { + currentReturnValue = 1; + } + } + } + if (currentReturnValue != 0) + return currentReturnValue; + } + if (cs == Qt::CaseInsensitive) + { + if (!c1.isLower()) + c1 = c1.toLower(); + if (!c2.isLower()) + c2 = c2.toLower(); + } + int r = QString::localeAwareCompare(c1, c2); + if (r < 0) + return -1; + if (r > 0) + return 1; + } + // The two strings are the same (02 == 2) so fall back to the normal sort + return QString::compare(s1, s2, cs); +} diff --git a/logic/MMCStrings.h b/logic/MMCStrings.h new file mode 100644 index 00000000..b2dd3912 --- /dev/null +++ b/logic/MMCStrings.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace Strings +{ + int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs); +} diff --git a/logic/MMCZip.cpp b/logic/MMCZip.cpp index e2c6d1f5..75f49ced 100644 --- a/logic/MMCZip.cpp +++ b/logic/MMCZip.cpp @@ -89,7 +89,7 @@ bool compressFile(QuaZip *zip, QString fileName, QString fileDest) return true; } -bool MMCZip::compressSubDir(QuaZip* zip, QString dir, QString origDir, QSet& added, QString prefix) +bool MMCZip::compressSubDir(QuaZip* zip, QString dir, QString origDir, QSet& added, QString prefix, const SeparatorPrefixTree <'/'> * blacklist) { if (!zip) return false; if (zip->getMode()!=QuaZip::mdCreate && zip->getMode()!=QuaZip::mdAppend && zip->getMode()!=QuaZip::mdAdd) @@ -106,13 +106,17 @@ bool MMCZip::compressSubDir(QuaZip* zip, QString dir, QString origDir, QSetcovers(internalDirName)) { - return false; + QuaZipFile dirZipFile(zip); + auto dirPrefix = PathCombine(prefix, origDirectory.relativeFilePath(dir)) + "/"; + if (!dirZipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(dirPrefix, dir), 0, 0, 0)) + { + return false; + } + dirZipFile.close(); } - dirZipFile.close(); } QFileInfoList files = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden); @@ -122,7 +126,7 @@ bool MMCZip::compressSubDir(QuaZip* zip, QString dir, QString origDir, QSetcovers(filename)) + { + continue; + } if(prefix.size()) { filename = PathCombine(prefix, filename); @@ -305,7 +313,7 @@ bool MMCZip::metaInfFilter(QString key) return true; } -bool MMCZip::compressDir(QString zipFile, QString dir, QString prefix) +bool MMCZip::compressDir(QString zipFile, QString dir, QString prefix, const SeparatorPrefixTree <'/'> * blacklist) { QuaZip zip(zipFile); QDir().mkpath(QFileInfo(zipFile).absolutePath()); @@ -316,7 +324,7 @@ bool MMCZip::compressDir(QString zipFile, QString dir, QString prefix) } QSet added; - if (!compressSubDir(&zip, dir, dir, added, prefix)) + if (!compressSubDir(&zip, dir, dir, added, prefix, blacklist)) { QFile::remove(zipFile); return false; diff --git a/logic/MMCZip.h b/logic/MMCZip.h index e1f2ba3a..0107d7a6 100644 --- a/logic/MMCZip.h +++ b/logic/MMCZip.h @@ -4,6 +4,7 @@ #include #include #include "minecraft/Mod.h" +#include "SeparatorPrefixTree.h" #include class QuaZip; @@ -18,7 +19,8 @@ namespace MMCZip * \param recursive Whether to pack sub-directories as well or only files. * \return true if success, false otherwise. */ - bool compressSubDir(QuaZip* zip, QString dir, QString origDir, QSet& added, QString prefix = QString()); + bool compressSubDir(QuaZip *zip, QString dir, QString origDir, QSet &added, + QString prefix = QString(), const SeparatorPrefixTree <'/'> * blacklist = nullptr); /** * Compress a whole directory. @@ -27,7 +29,7 @@ namespace MMCZip * \param recursive Whether to pack the subdirectories as well, or just regular files. * \return true if success, false otherwise. */ - bool compressDir(QString zipFile, QString dir, QString prefix = QString()); + bool compressDir(QString zipFile, QString dir, QString prefix = QString(), const SeparatorPrefixTree <'/'> * blacklist = nullptr); /// filter function for @mergeZipFiles - passthrough bool noFilter(QString key); diff --git a/logic/SeparatorPrefixTree.h b/logic/SeparatorPrefixTree.h new file mode 100644 index 00000000..fd149af0 --- /dev/null +++ b/logic/SeparatorPrefixTree.h @@ -0,0 +1,298 @@ +#pragma once +#include +#include +#include + +template +class SeparatorPrefixTree +{ +public: + SeparatorPrefixTree(QStringList paths) + { + insert(paths); + } + + SeparatorPrefixTree(bool contained = false) + { + m_contained = contained; + } + + void insert(QStringList paths) + { + for(auto &path: paths) + { + insert(path); + } + } + + /// insert an exact path into the tree + SeparatorPrefixTree & insert(QString path) + { + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + children[path] = SeparatorPrefixTree(true); + return children[path]; + } + else + { + auto prefix = path.left(sepIndex); + if(!children.contains(prefix)) + { + children[prefix] = SeparatorPrefixTree(false); + } + return children[prefix].insert(path.mid(sepIndex + 1)); + } + } + + /// is the path fully contained in the tree? + bool contains(QString path) const + { + auto node = find(path); + return node != nullptr; + } + + /// does the tree cover a path? That means the prefix of the path is contained in the tree + bool covers(QString path) const + { + // if we found some valid node, it's good enough. the tree covers the path + if(m_contained) + { + return true; + } + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + auto found = children.find(path); + if(found == children.end()) + { + return false; + } + return (*found).covers(QString()); + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if(found == children.end()) + { + return false; + } + return (*found).covers(path.mid(sepIndex + 1)); + } + } + + /// return the contained path that covers the path specified + QString cover(QString path) const + { + // if we found some valid node, it's good enough. the tree covers the path + if(m_contained) + { + return QString(""); + } + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + auto found = children.find(path); + if(found == children.end()) + { + return QString(); + } + auto nested = (*found).cover(QString()); + if(nested.isNull()) + { + return nested; + } + if(nested.isEmpty()) + return path; + return path + Tseparator + nested; + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if(found == children.end()) + { + return QString(); + } + auto nested = (*found).cover(path.mid(sepIndex + 1)); + if(nested.isNull()) + { + return nested; + } + if(nested.isEmpty()) + return prefix; + return prefix + Tseparator + nested; + } + } + + /// Does the path-specified node exist in the tree? It does not have to be contained. + bool exists(QString path) const + { + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + auto found = children.find(path); + if(found == children.end()) + { + return false; + } + return true; + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if(found == children.end()) + { + return false; + } + return (*found).exists(path.mid(sepIndex + 1)); + } + } + + /// find a node in the tree by name + const SeparatorPrefixTree * find(QString path) const + { + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + auto found = children.find(path); + if(found == children.end()) + { + return nullptr; + } + return &(*found); + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if(found == children.end()) + { + return nullptr; + } + return (*found).find(path.mid(sepIndex + 1)); + } + } + + /// is this a leaf node? + bool leaf() const + { + return children.isEmpty(); + } + + /// is this node actually contained in the tree, or is it purely structural? + bool contained() const + { + return m_contained; + } + + /// Remove a path from the tree + bool remove(QString path) + { + return removeInternal(path) != Failed; + } + + /// Clear all children of this node tree node + void clear() + { + children.clear(); + } + + QStringList toStringList() const + { + QStringList collected; + // collecting these is more expensive. + auto iter = children.begin(); + while(iter != children.end()) + { + QStringList list = iter.value().toStringList(); + for(int i = 0; i < list.size(); i++) + { + list[i] = iter.key() + Tseparator + list[i]; + } + collected.append(list); + if((*iter).m_contained) + { + collected.append(iter.key()); + } + iter++; + } + return collected; + } +private: + enum Removal + { + Failed, + Succeeded, + HasChildren + }; + Removal removeInternal(QString path = QString()) + { + if(path.isEmpty()) + { + if(!m_contained) + { + // remove all children - we are removing a prefix + clear(); + return Succeeded; + } + m_contained = false; + if(children.size()) + { + return HasChildren; + } + return Succeeded; + } + Removal remStatus = Failed; + QString childToRemove; + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + childToRemove = path; + auto found = children.find(childToRemove); + if(found == children.end()) + { + return Failed; + } + remStatus = (*found).removeInternal(); + } + else + { + childToRemove = path.left(sepIndex); + auto found = children.find(childToRemove); + if(found == children.end()) + { + return Failed; + } + remStatus = (*found).removeInternal(path.mid(sepIndex + 1)); + } + switch (remStatus) + { + case Failed: + case HasChildren: + { + return remStatus; + } + case Succeeded: + { + children.remove(childToRemove); + if(m_contained) + { + return HasChildren; + } + if(children.size()) + { + return HasChildren; + } + return Succeeded; + } + } + return Failed; + } + +private: + QMap> children; + bool m_contained = false; +};