diff options
author | Petr Mrázek <peterix@gmail.com> | 2015-04-19 04:19:29 +0200 |
---|---|---|
committer | Petr Mrázek <peterix@gmail.com> | 2015-04-19 16:14:32 +0200 |
commit | c7c81463fd3ab01c9e096f75e7e8ad8b50902a98 (patch) | |
tree | ab19d0316cd293bcc05bc6b1b6e937c858814b90 /application | |
parent | 6cfac115b1f18b9ff5130b2b9a6d5e2fcf052e6c (diff) | |
download | MultiMC-c7c81463fd3ab01c9e096f75e7e8ad8b50902a98.tar MultiMC-c7c81463fd3ab01c9e096f75e7e8ad8b50902a98.tar.gz MultiMC-c7c81463fd3ab01c9e096f75e7e8ad8b50902a98.tar.lz MultiMC-c7c81463fd3ab01c9e096f75e7e8ad8b50902a98.tar.xz MultiMC-c7c81463fd3ab01c9e096f75e7e8ad8b50902a98.zip |
GH-885 export dialog for filtering exported files
Includes implementation of a separator based prefix tree and some related bits
Diffstat (limited to 'application')
-rw-r--r-- | application/CMakeLists.txt | 3 | ||||
-rw-r--r-- | application/MainWindow.cpp | 26 | ||||
-rw-r--r-- | application/dialogs/ExportInstanceDialog.cpp | 434 | ||||
-rw-r--r-- | application/dialogs/ExportInstanceDialog.h | 54 | ||||
-rw-r--r-- | application/dialogs/ExportInstanceDialog.ui | 83 |
5 files changed, 577 insertions, 23 deletions
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 <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" 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 <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); +}; 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 @@ +<?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> |