GH-885 export dialog for filtering exported files
Includes implementation of a separator based prefix tree and some related bits
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										434
									
								
								application/dialogs/ExportInstanceDialog.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										434
									
								
								application/dialogs/ExportInstanceDialog.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <BaseInstance.h> | ||||
| #include <MMCZip.h> | ||||
| #include <pathutils.h> | ||||
| #include <QFileDialog> | ||||
| #include <QMessageBox> | ||||
| #include <qfilesystemmodel.h> | ||||
|  | ||||
| #include <QSortFilterProxyModel> | ||||
| #include <QDebug> | ||||
| #include <qstack.h> | ||||
| #include <QSaveFile> | ||||
| #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<QFileSystemModel *>(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<QFileSystemModel *>(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<Qt::CheckState>(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<QFileSystemModel *>(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<QModelIndex> 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<QModelIndex> 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<QFileSystemModel *>(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" | ||||
							
								
								
									
										54
									
								
								application/dialogs/ExportInstanceDialog.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								application/dialogs/ExportInstanceDialog.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <QDialog> | ||||
| #include <QModelIndex> | ||||
| #include <memory> | ||||
|  | ||||
| class BaseInstance; | ||||
| class PackIgnoreProxy; | ||||
| typedef std::shared_ptr<BaseInstance> 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); | ||||
| }; | ||||
							
								
								
									
										83
									
								
								application/dialogs/ExportInstanceDialog.ui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								application/dialogs/ExportInstanceDialog.ui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <ui version="4.0"> | ||||
|  <class>ExportInstanceDialog</class> | ||||
|  <widget class="QDialog" name="ExportInstanceDialog"> | ||||
|   <property name="geometry"> | ||||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>720</width> | ||||
|     <height>625</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <property name="windowTitle"> | ||||
|    <string>Export Instance</string> | ||||
|   </property> | ||||
|   <layout class="QVBoxLayout" name="verticalLayout"> | ||||
|    <item> | ||||
|     <widget class="QTreeView" name="treeView"> | ||||
|      <property name="alternatingRowColors"> | ||||
|       <bool>true</bool> | ||||
|      </property> | ||||
|      <property name="selectionMode"> | ||||
|       <enum>QAbstractItemView::ExtendedSelection</enum> | ||||
|      </property> | ||||
|      <property name="sortingEnabled"> | ||||
|       <bool>true</bool> | ||||
|      </property> | ||||
|      <attribute name="headerStretchLastSection"> | ||||
|       <bool>false</bool> | ||||
|      </attribute> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item> | ||||
|     <widget class="QDialogButtonBox" name="buttonBox"> | ||||
|      <property name="orientation"> | ||||
|       <enum>Qt::Horizontal</enum> | ||||
|      </property> | ||||
|      <property name="standardButtons"> | ||||
|       <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|   </layout> | ||||
|  </widget> | ||||
|  <tabstops> | ||||
|   <tabstop>treeView</tabstop> | ||||
|  </tabstops> | ||||
|  <resources/> | ||||
|  <connections> | ||||
|   <connection> | ||||
|    <sender>buttonBox</sender> | ||||
|    <signal>accepted()</signal> | ||||
|    <receiver>ExportInstanceDialog</receiver> | ||||
|    <slot>accept()</slot> | ||||
|    <hints> | ||||
|     <hint type="sourcelabel"> | ||||
|      <x>248</x> | ||||
|      <y>254</y> | ||||
|     </hint> | ||||
|     <hint type="destinationlabel"> | ||||
|      <x>157</x> | ||||
|      <y>274</y> | ||||
|     </hint> | ||||
|    </hints> | ||||
|   </connection> | ||||
|   <connection> | ||||
|    <sender>buttonBox</sender> | ||||
|    <signal>rejected()</signal> | ||||
|    <receiver>ExportInstanceDialog</receiver> | ||||
|    <slot>reject()</slot> | ||||
|    <hints> | ||||
|     <hint type="sourcelabel"> | ||||
|      <x>316</x> | ||||
|      <y>260</y> | ||||
|     </hint> | ||||
|     <hint type="destinationlabel"> | ||||
|      <x>286</x> | ||||
|      <y>274</y> | ||||
|     </hint> | ||||
|    </hints> | ||||
|   </connection> | ||||
|  </connections> | ||||
| </ui> | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										76
									
								
								logic/MMCStrings.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								logic/MMCStrings.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
| } | ||||
							
								
								
									
										8
									
								
								logic/MMCStrings.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								logic/MMCStrings.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <QString> | ||||
|  | ||||
| namespace Strings | ||||
| { | ||||
| 	int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs); | ||||
| } | ||||
| @@ -89,7 +89,7 @@ bool compressFile(QuaZip *zip, QString fileName, QString fileDest) | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
| bool MMCZip::compressSubDir(QuaZip* zip, QString dir, QString origDir, QSet<QString>& added,  QString prefix) | ||||
| bool MMCZip::compressSubDir(QuaZip* zip, QString dir, QString origDir, QSet<QString>& 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, QSet<QStr | ||||
| 	QDir origDirectory(origDir); | ||||
| 	if (dir != origDir) | ||||
| 	{ | ||||
| 		QuaZipFile dirZipFile(zip); | ||||
| 		auto dirPrefix = PathCombine(prefix, origDirectory.relativeFilePath(dir)) + "/"; | ||||
| 		if (!dirZipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(dirPrefix, dir), 0, 0, 0)) | ||||
| 		QString internalDirName = origDirectory.relativeFilePath(dir); | ||||
| 		if(!blacklist || !blacklist->covers(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, QSet<QStr | ||||
| 		{ | ||||
| 			continue; | ||||
| 		} | ||||
| 		if(!compressSubDir(zip,file.absoluteFilePath(),origDir, added, prefix)) | ||||
| 		if(!compressSubDir(zip,file.absoluteFilePath(),origDir, added, prefix, blacklist)) | ||||
| 		{ | ||||
| 			return false; | ||||
| 		} | ||||
| @@ -142,6 +146,10 @@ bool MMCZip::compressSubDir(QuaZip* zip, QString dir, QString origDir, QSet<QStr | ||||
| 		} | ||||
|  | ||||
| 		QString filename = origDirectory.relativeFilePath(file.absoluteFilePath()); | ||||
| 		if(blacklist && blacklist->covers(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<QString> added; | ||||
| 	if (!compressSubDir(&zip, dir, dir, added, prefix)) | ||||
| 	if (!compressSubDir(&zip, dir, dir, added, prefix, blacklist)) | ||||
| 	{ | ||||
| 		QFile::remove(zipFile); | ||||
| 		return false; | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| #include <QFileInfo> | ||||
| #include <QSet> | ||||
| #include "minecraft/Mod.h" | ||||
| #include "SeparatorPrefixTree.h" | ||||
| #include <functional> | ||||
|  | ||||
| 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<QString>& added, QString prefix = QString()); | ||||
| 	bool compressSubDir(QuaZip *zip, QString dir, QString origDir, QSet<QString> &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); | ||||
|   | ||||
							
								
								
									
										298
									
								
								logic/SeparatorPrefixTree.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								logic/SeparatorPrefixTree.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,298 @@ | ||||
| #pragma once | ||||
| #include <QString> | ||||
| #include <QMap> | ||||
| #include <QStringList> | ||||
|  | ||||
| template <char Tseparator> | ||||
| 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<QString,SeparatorPrefixTree<Tseparator>> children; | ||||
| 	bool m_contained = false; | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user