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:
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>
|
Reference in New Issue
Block a user