/* Copyright 2013-2018 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 "MMCStrings.h" #include "SeparatorPrefixTree.h" #include "MultiMC.h" #include #include 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(FS::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))); ui->treeView->sortByColumn(0, Qt::AscendingOrder); connect(proxyModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int))); model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); model->setRootPath(root); auto headerView = ui->treeView->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); } ExportInstanceDialog::~ExportInstanceDialog() { delete ui; } /// Save icon to instance's folder is needed void SaveIcon(InstancePtr m_instance) { auto iconKey = m_instance->iconKey(); auto iconList = MMC->icons(); auto mmcIcon = iconList->icon(iconKey); if(mmcIcon) { bool saveIcon = false; switch(mmcIcon->type()) { case IconType::FileBased: case IconType::Transient: saveIcon = true; default: break; } if(saveIcon) { auto & image = mmcIcon->m_images[mmcIcon->type()]; auto & icon = image.icon; auto sizes = icon.availableSizes(); if(sizes.size() == 0) { return; } auto areaOf = [](QSize size) { return size.width() * size.height(); }; QSize largest = sizes[0]; // find variant with largest area for(auto size: sizes) { if(areaOf(largest) < areaOf(size)) { largest = size; } } auto pixmap = icon.pixmap(largest); pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png")); } } } bool ExportInstanceDialog::doExport() { auto name = FS::RemoveInvalidFilenameChars(m_instance->name()); const QString output = QFileDialog::getSaveFileName( this, tr("Export %1").arg(m_instance->name()), FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr, QFileDialog::DontConfirmOverwrite); if (output.isEmpty()) { 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; } } SaveIcon(m_instance); auto & blocked = proxyModel->blockedPaths(); using std::placeholders::_1; if (!JlCompress::compressDir(output, m_instance->instanceRoot(), name, std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) { 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 FS::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 data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8(); auto filename = ignoreFileName(); try { FS::write(filename, data); } catch (Exception & e) { qWarning() << e.cause(); } } #include "ExportInstanceDialog.moc"