diff --git a/kerfuffle/archiveentry.cpp b/kerfuffle/archiveentry.cpp index 65286716..956cc9e9 100644 --- a/kerfuffle/archiveentry.cpp +++ b/kerfuffle/archiveentry.cpp @@ -1,229 +1,247 @@ /* * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archiveentry.h" +#include + namespace Kerfuffle { Archive::Entry::Entry(QObject *parent, const QString &fullPath, const QString &rootNode) : QObject(parent) , rootNode(rootNode) , compressedSizeIsSet(true) , m_parent(qobject_cast(parent)) , m_size(0) , m_compressedSize(0) , m_isDirectory(false) , m_isExecutable(false) , m_isPasswordProtected(false) { if (!fullPath.isEmpty()) setFullPath(fullPath); } Archive::Entry::~Entry() { } void Archive::Entry::copyMetaData(const Archive::Entry *sourceEntry) { setProperty("fullPath", sourceEntry->property("fullPath")); setProperty("permissions", sourceEntry->property("permissions")); setProperty("owner", sourceEntry->property("owner")); setProperty("group", sourceEntry->property("group")); setProperty("size", sourceEntry->property("size")); setProperty("compressedSize", sourceEntry->property("compressedSize")); setProperty("link", sourceEntry->property("link")); setProperty("ratio", sourceEntry->property("ratio")); setProperty("CRC", sourceEntry->property("CRC")); setProperty("BLAKE2", sourceEntry->property("BLAKE2")); setProperty("method", sourceEntry->property("method")); setProperty("version", sourceEntry->property("version")); setProperty("timestamp", sourceEntry->property("timestamp").toDateTime()); setProperty("isDirectory", sourceEntry->property("isDirectory")); setProperty("isPasswordProtected", sourceEntry->property("isPasswordProtected")); } QVector Archive::Entry::entries() { Q_ASSERT(isDir()); return m_entries; } const QVector Archive::Entry::entries() const { Q_ASSERT(isDir()); return m_entries; } void Archive::Entry::setEntryAt(int index, Entry *value) { Q_ASSERT(isDir()); Q_ASSERT(index < m_entries.count()); m_entries[index] = value; } void Archive::Entry::appendEntry(Entry *entry) { Q_ASSERT(isDir()); m_entries.append(entry); } void Archive::Entry::removeEntryAt(int index) { Q_ASSERT(isDir()); Q_ASSERT(index < m_entries.count()); m_entries.remove(index); } Archive::Entry *Archive::Entry::getParent() const { return m_parent; } void Archive::Entry::setParent(Archive::Entry *parent) { m_parent = parent; } void Archive::Entry::setFullPath(const QString &fullPath) { m_fullPath = fullPath; #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const QStringList pieces = m_fullPath.split(QLatin1Char('/'), QString::SkipEmptyParts); #else const QStringList pieces = m_fullPath.split(QLatin1Char('/'), Qt::SkipEmptyParts); #endif m_name = pieces.isEmpty() ? QString() : pieces.last(); } QString Archive::Entry::fullPath(PathFormat format) const { if (format == NoTrailingSlash && m_fullPath.endsWith(QLatin1Char('/'))) { return m_fullPath.left(m_fullPath.size() - 1); } else { return m_fullPath; } } QString Archive::Entry::name() const { return m_name; } void Archive::Entry::setIsDirectory(const bool isDirectory) { m_isDirectory = isDirectory; } bool Archive::Entry::isDir() const { return m_isDirectory; } void Archive::Entry::setIsExecutable(const bool isExecutable) { m_isExecutable = isExecutable; } bool Archive::Entry::isExecutable() const { return m_isExecutable; } int Archive::Entry::row() const { if (getParent()) { return getParent()->entries().indexOf(const_cast(this)); } return 0; } Archive::Entry *Archive::Entry::find(const QString &name) const { for (Entry *entry : qAsConst(m_entries)) { if (entry && (entry->name() == name)) { return entry; } } return nullptr; } Archive::Entry *Archive::Entry::findByPath(const QStringList &pieces, int index) const { if (index == pieces.count()) { return nullptr; } Entry *next = find(pieces.at(index)); if (index == pieces.count() - 1) { return next; } if (next && next->isDir()) { return next->findByPath(pieces, index + 1); } return nullptr; } void Archive::Entry::countChildren(uint &dirs, uint &files) const { dirs = files = 0; if (!isDir()) { return; } const auto archiveEntries = entries(); for (auto entry : archiveEntries) { if (entry->isDir()) { dirs++; } else { files++; } } } +QIcon Archive::Entry::icon() const +{ + if (m_icon.isNull()) { + QMimeDatabase db; + + if (m_isDirectory) { + static QIcon directoryIcon = QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()); + m_icon = directoryIcon; + } else { + m_icon = QIcon::fromTheme(db.mimeTypeForFile(m_name, QMimeDatabase::MatchMode::MatchExtension).iconName()); + } + } + + return m_icon; +} + bool Archive::Entry::operator==(const Archive::Entry &right) const { return m_fullPath == right.m_fullPath; } QDebug operator<<(QDebug d, const Kerfuffle::Archive::Entry &entry) { d.nospace() << "Entry(" << entry.property("fullPath"); if (!entry.rootNode.isEmpty()) { d.nospace() << "," << entry.rootNode; } d.nospace() << ")"; return d.space(); } QDebug operator<<(QDebug d, const Kerfuffle::Archive::Entry *entry) { d.nospace() << "Entry(" << entry->property("fullPath"); if (!entry->rootNode.isEmpty()) { d.nospace() << "," << entry->rootNode; } d.nospace() << ")"; return d.space(); } } diff --git a/kerfuffle/archiveentry.h b/kerfuffle/archiveentry.h index 606ee7c9..4be1de02 100644 --- a/kerfuffle/archiveentry.h +++ b/kerfuffle/archiveentry.h @@ -1,140 +1,142 @@ /* * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVEENTRY_H #define ARCHIVEENTRY_H #include "archive_kerfuffle.h" #include - +#include namespace Kerfuffle { enum PathFormat { NoTrailingSlash, WithTrailingSlash }; class Archive::Entry : public QObject { Q_OBJECT /** * Meta data related to one entry in a compressed archive. * * When creating a plugin, information about every single entry in * an archive is contained in an ArchiveEntry, and metadata * is set with the entries in this enum. * * Please notice that not all archive formats support all the properties * below, so set those that are available. */ Q_PROPERTY(QString fullPath MEMBER m_fullPath WRITE setFullPath) Q_PROPERTY(QString name READ name) Q_PROPERTY(QString permissions MEMBER m_permissions) Q_PROPERTY(QString owner MEMBER m_owner) Q_PROPERTY(QString group MEMBER m_group) Q_PROPERTY(qulonglong size MEMBER m_size) Q_PROPERTY(qulonglong compressedSize MEMBER m_compressedSize) Q_PROPERTY(QString link MEMBER m_link) Q_PROPERTY(QString ratio MEMBER m_ratio) Q_PROPERTY(QString CRC MEMBER m_CRC) Q_PROPERTY(QString BLAKE2 MEMBER m_BLAKE2) Q_PROPERTY(QString method MEMBER m_method) Q_PROPERTY(QString version MEMBER m_version) Q_PROPERTY(QDateTime timestamp MEMBER m_timestamp) Q_PROPERTY(bool isDirectory MEMBER m_isDirectory WRITE setIsDirectory) Q_PROPERTY(bool isExecutable MEMBER m_isExecutable WRITE setIsExecutable) Q_PROPERTY(bool isPasswordProtected MEMBER m_isPasswordProtected) public: explicit Entry(QObject *parent = nullptr, const QString &fullPath = {}, const QString &rootNode = {}); ~Entry() override; void copyMetaData(const Archive::Entry *sourceEntry); QVector entries(); const QVector entries() const; void setEntryAt(int index, Entry *value); void appendEntry(Entry *entry); void removeEntryAt(int index); Entry *getParent() const; void setParent(Entry *parent); void setFullPath(const QString &fullPath); QString fullPath(PathFormat format = WithTrailingSlash) const; QString name() const; void setIsDirectory(const bool isDirectory); bool isDir() const; void setIsExecutable(const bool isExecutable); bool isExecutable() const; int row() const; Entry *find(const QString &name) const; Entry *findByPath(const QStringList & pieces, int index = 0) const; + QIcon icon() const; /** * Fills @p dirs and @p files with the number of directories and files * in the entry (both will be 0 if the entry is not a directory). */ void countChildren(uint &dirs, uint &files) const; bool operator==(const Archive::Entry &right) const; public: QString rootNode; bool compressedSizeIsSet; private: QVector m_entries; QString m_name; Entry *m_parent; QString m_fullPath; QString m_permissions; QString m_owner; QString m_group; qulonglong m_size; qulonglong m_compressedSize; QString m_link; QString m_ratio; QString m_CRC; QString m_BLAKE2; QString m_method; QString m_version; QDateTime m_timestamp; bool m_isDirectory; bool m_isExecutable; bool m_isPasswordProtected; + mutable QIcon m_icon; }; QDebug KERFUFFLE_EXPORT operator<<(QDebug d, const Kerfuffle::Archive::Entry &entry); QDebug KERFUFFLE_EXPORT operator<<(QDebug d, const Kerfuffle::Archive::Entry *entry); } Q_DECLARE_METATYPE(Kerfuffle::Archive::Entry*) #endif //ARCHIVEENTRY_H diff --git a/part/archivemodel.cpp b/part/archivemodel.cpp index 132a0b25..28117f5b 100644 --- a/part/archivemodel.cpp +++ b/part/archivemodel.cpp @@ -1,941 +1,926 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2010-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "archivemodel.h" #include "ark_debug.h" #include "jobs.h" #include #include #include #include #include #include #include #include #include using namespace Kerfuffle; // Used to speed up the loading of large archives. static Archive::Entry *s_previousMatch = nullptr; Q_GLOBAL_STATIC(QStringList, s_previousPieces) ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent) : QAbstractItemModel(parent) , m_dbusPathName(dbusPathName) , m_numberOfFiles(0) , m_numberOfFolders(0) , m_fileEntryListed(false) { initRootEntry(); // Mappings between column indexes and entry properties. m_propertiesMap = { { FullPath, "fullPath" }, { Size, "size" }, { CompressedSize, "compressedSize" }, { Permissions, "permissions" }, { Owner, "owner" }, { Group, "group" }, { Ratio, "ratio" }, { CRC, "CRC" }, { BLAKE2, "BLAKE2" }, { Method, "method" }, { Version, "version" }, { Timestamp, "timestamp" }, }; } ArchiveModel::~ArchiveModel() { } QVariant ArchiveModel::data(const QModelIndex &index, int role) const { if (index.isValid()) { Archive::Entry *entry = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: { // TODO: complete the columns. int column = m_showColumns.at(index.column()); switch (column) { case FullPath: return entry->name(); case Size: if (entry->isDir()) { uint dirs; uint files; entry->countChildren(dirs, files); return KIO::itemsSummaryString(dirs + files, files, dirs, 0, false); } else if (!entry->property("link").toString().isEmpty()) { return QVariant(); } else { return KIO::convertSize(entry->property("size").toULongLong()); } case CompressedSize: if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); if (compressedSize != 0) { return KIO::convertSize(compressedSize); } else { return QVariant(); } } case Ratio: // TODO: Use entry->metaData()[Ratio] when available. if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); qulonglong size = entry->property("size").toULongLong(); if (compressedSize == 0 || size == 0) { return QVariant(); } else { int ratio = int(100 * ((double)size - compressedSize) / size); return QString(QString::number(ratio) + QStringLiteral(" %")); } } case Timestamp: { const QDateTime timeStamp = entry->property("timestamp").toDateTime(); return QLocale().toString(timeStamp, QLocale::ShortFormat); } default: return entry->property(m_propertiesMap[column].constData()); } } case Qt::DecorationRole: if (index.column() == 0) { - const Archive::Entry *e = static_cast(index.internalPointer()); + Archive::Entry *e = static_cast(index.internalPointer()); QIcon::Mode mode = (filesToMove.contains(e->fullPath())) ? QIcon::Disabled : QIcon::Normal; - return m_entryIcons.value(e->fullPath(NoTrailingSlash)).pixmap(QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize), mode); + return e->icon().pixmap(QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize), mode); } return QVariant(); case Qt::FontRole: { QFont f; f.setItalic(entry->property("isPasswordProtected").toBool()); return f; } default: return QVariant(); } } return QVariant(); } Qt::ItemFlags ArchiveModel::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); if (index.isValid()) { return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | defaultFlags; } return Qt::NoItemFlags; } QVariant ArchiveModel::headerData(int section, Qt::Orientation, int role) const { if (role == Qt::DisplayRole) { if (section >= m_showColumns.size()) { qCDebug(ARK) << "WEIRD: showColumns.size = " << m_showColumns.size() << " and section = " << section; return QVariant(); } int columnId = m_showColumns.at(section); switch (columnId) { case FullPath: return i18nc("Name of a file inside an archive", "Name"); case Size: return i18nc("Uncompressed size of a file inside an archive", "Size"); case CompressedSize: return i18nc("Compressed size of a file inside an archive", "Compressed"); case Ratio: return i18nc("Compression rate of file", "Rate"); case Owner: return i18nc("File's owner username", "Owner"); case Group: return i18nc("File's group", "Group"); case Permissions: return i18nc("File permissions", "Mode"); case CRC: return i18nc("CRC hash code", "CRC checksum"); case BLAKE2: return i18nc("BLAKE2 hash code", "BLAKE2 checksum"); case Method: return i18nc("Compression method", "Method"); case Version: // TODO: what exactly is a file version? return i18nc("File version", "Version"); case Timestamp: return i18nc("Timestamp", "Date"); default: return i18nc("Unnamed column", "??"); } } return QVariant(); } QModelIndex ArchiveModel::index(int row, int column, const QModelIndex &parent) const { if (hasIndex(row, column, parent)) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : m_rootEntry.data(); Q_ASSERT(parentEntry->isDir()); const Archive::Entry *item = parentEntry->entries().value(row, nullptr); if (item != nullptr) { return createIndex(row, column, const_cast(item)); } } return QModelIndex(); } QModelIndex ArchiveModel::parent(const QModelIndex &index) const { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->getParent() && (item->getParent() != m_rootEntry.data())) { return createIndex(item->getParent()->row(), 0, item->getParent()); } } return QModelIndex(); } Archive::Entry *ArchiveModel::entryForIndex(const QModelIndex &index) { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); return item; } return nullptr; } int ArchiveModel::rowCount(const QModelIndex &parent) const { if (parent.column() <= 0) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : m_rootEntry.data(); if (parentEntry && parentEntry->isDir()) { return parentEntry->entries().count(); } } return 0; } int ArchiveModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_showColumns.size(); } Qt::DropActions ArchiveModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList ArchiveModel::mimeTypes() const { QStringList types; // MIME types we accept for dragging (eg. Dolphin -> Ark). types << QStringLiteral("text/uri-list") << QStringLiteral("text/plain") << QStringLiteral("text/x-moz-url"); // MIME types we accept for dropping (eg. Ark -> Dolphin). types << QStringLiteral("application/x-kde-ark-dndextract-service") << QStringLiteral("application/x-kde-ark-dndextract-path"); return types; } QMimeData *ArchiveModel::mimeData(const QModelIndexList &indexes) const { Q_UNUSED(indexes) QMimeData *mimeData = new QMimeData; mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-service"), QDBusConnection::sessionBus().baseService().toUtf8()); mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-path"), m_dbusPathName.toUtf8()); return mimeData; } bool ArchiveModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { Q_UNUSED(action) if (!data->hasUrls()) { return false; } if (archive()->isReadOnly() || (archive()->encryptionType() != Archive::Unencrypted && archive()->password().isEmpty())) { emit messageWidget(KMessageWidget::Error, i18n("Adding files is not supported for this archive.")); return false; } QStringList paths; const auto urls = data->urls(); for (const QUrl &url : urls) { if (!url.isLocalFile()) { emit messageWidget(KMessageWidget::Error, i18n("You can only add local files to an archive.")); return false; } paths << url.toLocalFile(); } const Archive::Entry *entry = nullptr; QModelIndex droppedOnto = index(row, column, parent); if (droppedOnto.isValid()) { entry = entryForIndex(droppedOnto); if (!entry->isDir()) { entry = entry->getParent(); } } emit droppedFiles(paths, entry); return true; } // For a rationale, see bugs #194241, #241967 and #355839 QString ArchiveModel::cleanFileName(const QString& fileName) { // Skip entries with filename "/" or "//" or "." // "." is present in ISO files. static QRegularExpression pattern(QStringLiteral("/+|\\.")); QRegularExpressionMatch match; if (fileName.contains(pattern, &match) && match.captured() == fileName) { qCDebug(ARK) << "Skipping entry with filename" << fileName; return QString(); } else if (fileName.startsWith(QLatin1String("./"))) { return fileName.mid(2); } return fileName; } void ArchiveModel::initRootEntry() { m_rootEntry.reset(new Archive::Entry()); m_rootEntry->setProperty("isDirectory", true); } Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry, InsertBehaviour behaviour) { #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList pieces = entry->fullPath().split(QLatin1Char('/'), QString::SkipEmptyParts); #else QStringList pieces = entry->fullPath().split(QLatin1Char('/'), Qt::SkipEmptyParts); #endif if (pieces.isEmpty()) { return nullptr; } pieces.removeLast(); // Used to speed up loading of large archives. if (s_previousMatch) { // The number of path elements must be the same for the shortcut // to work. if (s_previousPieces->count() == pieces.count()) { bool equal = true; // Check if all pieces match. for (int i = 0; i < s_previousPieces->count(); ++i) { if (s_previousPieces->at(i) != pieces.at(i)) { equal = false; break; } } // If match return it. if (equal) { return s_previousMatch; } } } Archive::Entry *parent = m_rootEntry.data(); for (const QString &piece : qAsConst(pieces)) { Archive::Entry *entry = parent->find(piece); if (!entry) { // Directory entry will be traversed later (that happens for some archive formats, 7z for instance). // We have to create one before, in order to construct tree from its children, // and then delete the existing one (see ArchiveModel::newEntry). entry = new Archive::Entry(parent); entry->setProperty("fullPath", (parent == m_rootEntry.data()) ? QString(piece + QLatin1Char('/')) : QString(parent->fullPath(WithTrailingSlash) + piece + QLatin1Char('/'))); entry->setProperty("isDirectory", true); insertEntry(entry, behaviour); } if (!entry->isDir()) { Archive::Entry *e = new Archive::Entry(parent); e->copyMetaData(entry); // Maybe we have both a file and a directory of the same name. // We avoid removing previous entries unless necessary. insertEntry(e, behaviour); } parent = entry; } s_previousMatch = parent; *s_previousPieces = pieces; return parent; } QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry) { Q_ASSERT(entry); if (entry != m_rootEntry.data()) { Q_ASSERT(entry->getParent()); Q_ASSERT(entry->getParent()->isDir()); return createIndex(entry->row(), 0, entry); } return QModelIndex(); } void ArchiveModel::slotEntryRemoved(const QString & path) { const QString entryFileName(cleanFileName(path)); if (entryFileName.isEmpty()) { return; } #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) Archive::Entry *entry = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts)); #else Archive::Entry *entry = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/'), Qt::SkipEmptyParts)); #endif if (entry) { Archive::Entry *parent = entry->getParent(); QModelIndex index = indexForEntry(entry); Q_UNUSED(index); beginRemoveRows(indexForEntry(parent), entry->row(), entry->row()); - m_entryIcons.remove(parent->entries().at(entry->row())->fullPath(NoTrailingSlash)); parent->removeEntryAt(entry->row()); endRemoveRows(); } } void ArchiveModel::slotUserQuery(Kerfuffle::Query *query) { query->execute(); } void ArchiveModel::slotNewEntry(Archive::Entry *entry) { newEntry(entry, NotifyViews); } void ArchiveModel::slotListEntry(Archive::Entry *entry) { newEntry(entry, DoNotNotifyViews); } void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behaviour) { if (receivedEntry->fullPath().isEmpty()) { qCDebug(ARK) << "Weird, received empty entry (no filename) - skipping"; return; } // If there are no columns registered, then populate columns from entry. If the first entry // is a directory we check again for the first file entry to ensure all relevant columms are shown. if (m_showColumns.isEmpty() || !m_fileEntryListed) { QList toInsert; const auto size = receivedEntry->property("size").toULongLong(); const auto compressedSize = receivedEntry->property("compressedSize").toULongLong(); for (auto i = m_propertiesMap.begin(); i != m_propertiesMap.end(); ++i) { // Singlefile plugin doesn't report the uncompressed size. if (i.key() == Size && size == 0 && compressedSize > 0) { continue; } if (!receivedEntry->property(i.value().constData()).toString().isEmpty()) { if (i.key() != CompressedSize || receivedEntry->compressedSizeIsSet) { if (!m_showColumns.contains(i.key())) { toInsert << i.key(); } } } } if (behaviour == NotifyViews) { beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1); } m_showColumns << toInsert; if (behaviour == NotifyViews) { endInsertColumns(); } m_fileEntryListed = !receivedEntry->isDir(); } // #194241: Filenames such as "./file" should be displayed as "file" // #241967: Entries called "/" should be ignored // #355839: Entries called "//" should be ignored QString entryFileName = cleanFileName(receivedEntry->fullPath()); if (entryFileName.isEmpty()) { // The entry contains only "." or "./" return; } receivedEntry->setProperty("fullPath", entryFileName); // For some archive formats (e.g. AppImage and RPM) paths of folders do not // contain a trailing slash, so we append it. if (receivedEntry->property("isDirectory").toBool() && !receivedEntry->property("fullPath").toString().endsWith(QLatin1Char('/'))) { receivedEntry->setProperty("fullPath", QString(receivedEntry->property("fullPath").toString() + QLatin1Char('/'))); qCDebug(ARK) << "Trailing slash appended to entry:" << receivedEntry->property("fullPath"); } // Skip already created entries. Archive::Entry *existing = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/'))); if (existing) { existing->setProperty("fullPath", entryFileName); // Multi-volume files are repeated at least in RAR archives. // In that case, we need to sum the compressed size for each volume qulonglong currentCompressedSize = existing->property("compressedSize").toULongLong(); existing->setProperty("compressedSize", currentCompressedSize + receivedEntry->property("compressedSize").toULongLong()); return; } // Find parent entry, creating missing directory Archive::Entry's in the process. Archive::Entry *parent = parentFor(receivedEntry, behaviour); // Create an Archive::Entry. #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const QStringList path = entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts); #else const QStringList path = entryFileName.split(QLatin1Char('/'), Qt::SkipEmptyParts); #endif Archive::Entry *entry = parent->find(path.last()); if (entry) { entry->copyMetaData(receivedEntry); entry->setProperty("fullPath", entryFileName); } else { receivedEntry->setParent(parent); insertEntry(receivedEntry, behaviour); } } void ArchiveModel::slotLoadingFinished(KJob *job) { std::sort(m_showColumns.begin(), m_showColumns.end()); if (!job->error()) { qCDebug(ARK) << "Showing columns: " << m_showColumns; m_archive.reset(qobject_cast(job)->archive()); beginResetModel(); endResetModel(); } emit loadingFinished(job); } void ArchiveModel::insertEntry(Archive::Entry *entry, InsertBehaviour behaviour) { Q_ASSERT(entry); Archive::Entry *parent = entry->getParent(); Q_ASSERT(parent); if (behaviour == NotifyViews) { beginInsertRows(indexForEntry(parent), parent->entries().count(), parent->entries().count()); } parent->appendEntry(entry); if (behaviour == NotifyViews) { endInsertRows(); } - - // Save an icon for each newly added entry. - QMimeDatabase db; - QIcon icon; - entry->isDir() - ? icon = QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()) - : icon = QIcon::fromTheme(db.mimeTypeForFile(entry->fullPath()).iconName()); - m_entryIcons.insert(entry->fullPath(NoTrailingSlash), icon); } Kerfuffle::Archive* ArchiveModel::archive() const { return m_archive.data(); } void ArchiveModel::reset() { m_archive.reset(nullptr); s_previousMatch = nullptr; s_previousPieces->clear(); initRootEntry(); // TODO: make sure if it's ok to not have calls to beginRemoveColumns here m_showColumns.clear(); beginResetModel(); endResetModel(); } void ArchiveModel::createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent) { reset(); m_archive.reset(Archive::createEmpty(path, mimeType, parent)); } KJob *ArchiveModel::loadArchive(const QString &path, const QString &mimeType, QObject *parent) { reset(); auto loadJob = Archive::load(path, mimeType, parent); connect(loadJob, &KJob::result, this, &ArchiveModel::slotLoadingFinished); connect(loadJob, &Job::newEntry, this, &ArchiveModel::slotListEntry); connect(loadJob, &Job::userQuery, this, &ArchiveModel::slotUserQuery); emit loadingStarted(); return loadJob; } ExtractJob* ArchiveModel::extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { QVector files({file}); return extractFiles(files, destinationDir, options); } ExtractJob* ArchiveModel::extractFiles(const QVector& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { Q_ASSERT(m_archive); ExtractJob *newJob = m_archive->extractFiles(files, destinationDir, options); connect(newJob, &ExtractJob::userQuery, this, &ArchiveModel::slotUserQuery); return newJob; } Kerfuffle::PreviewJob *ArchiveModel::preview(Archive::Entry *file) const { Q_ASSERT(m_archive); PreviewJob *job = m_archive->preview(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenJob *ArchiveModel::open(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenJob *job = m_archive->open(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenWithJob *ArchiveModel::openWith(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenWithJob *job = m_archive->openWith(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } AddJob* ArchiveModel::addFiles(QVector &entries, const Archive::Entry *destination, const CompressionOptions& options) { if (!m_archive) { return nullptr; } if (!m_archive->isReadOnly()) { AddJob *job = m_archive->addFiles(entries, destination, options); connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return nullptr; } Kerfuffle::MoveJob *ArchiveModel::moveFiles(QVector &entries, Archive::Entry *destination, const CompressionOptions &options) { if (!m_archive) { return nullptr; } if (!m_archive->isReadOnly()) { MoveJob *job = m_archive->moveFiles(entries, destination, options); connect(job, &MoveJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &MoveJob::userQuery, this, &ArchiveModel::slotUserQuery); connect(job, &MoveJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); connect(job, &MoveJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); return job; } return nullptr; } Kerfuffle::CopyJob *ArchiveModel::copyFiles(QVector &entries, Archive::Entry *destination, const CompressionOptions &options) { if (!m_archive) { return nullptr; } if (!m_archive->isReadOnly()) { CopyJob *job = m_archive->copyFiles(entries, destination, options); connect(job, &CopyJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &CopyJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return nullptr; } DeleteJob* ArchiveModel::deleteFiles(QVector entries) { Q_ASSERT(m_archive); if (!m_archive->isReadOnly()) { DeleteJob *job = m_archive->deleteFiles(entries); connect(job, &DeleteJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); connect(job, &DeleteJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); connect(job, &DeleteJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return nullptr; } void ArchiveModel::encryptArchive(const QString &password, bool encryptHeader) { if (!m_archive) { return; } m_archive->encrypt(password, encryptHeader); } bool ArchiveModel::conflictingEntries(QList &conflictingEntries, const QStringList &entries, bool allowMerging) const { bool error = false; // We can't accept destination as an argument, because it can be a new entry path for renaming. const Archive::Entry *destination; { #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList destinationParts = entries.first().split(QLatin1Char('/'), QString::SkipEmptyParts); #else QStringList destinationParts = entries.first().split(QLatin1Char('/'), Qt::SkipEmptyParts); #endif destinationParts.removeLast(); if (destinationParts.count() > 0) { destination = m_rootEntry->findByPath(destinationParts); } else { destination = m_rootEntry.data(); } } const Archive::Entry *lastDirEntry = destination; QString skippedDirPath; for (const QString &entry : entries) { if (skippedDirPath.count() > 0 && entry.startsWith(skippedDirPath)) { continue; } else { skippedDirPath.clear(); } while (!entry.startsWith(lastDirEntry->fullPath())) { lastDirEntry = lastDirEntry->getParent(); } bool isDir = entry.right(1) == QLatin1String("/"); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const Archive::Entry *archiveEntry = lastDirEntry->find(entry.split(QLatin1Char('/'), QString::SkipEmptyParts).last()); #else const Archive::Entry *archiveEntry = lastDirEntry->find(entry.split(QLatin1Char('/'), Qt::SkipEmptyParts).last()); #endif if (archiveEntry != nullptr) { if (archiveEntry->isDir() != isDir || !allowMerging) { if (isDir) { skippedDirPath = lastDirEntry->fullPath(); } if (!error) { conflictingEntries.clear(); error = true; } conflictingEntries << archiveEntry; } else { if (isDir) { lastDirEntry = archiveEntry; } else if (!error) { conflictingEntries << archiveEntry; } } } else if (isDir) { skippedDirPath = entry; } } return error; } bool ArchiveModel::hasDuplicatedEntries(const QStringList &entries) { QStringList tempList; for (const QString &entry : entries) { if (tempList.contains(entry)) { return true; } tempList << entry; } return false; } QMap ArchiveModel::entryMap(const QVector &entries) { QMap map; for (Archive::Entry *entry : entries) { map.insert(entry->fullPath(), entry); } return map; } -const QHash ArchiveModel::entryIcons() const -{ - return m_entryIcons; -} - void ArchiveModel::slotCleanupEmptyDirs() { QList queue; QList nodesToDelete; // Add root nodes. for (int i = 0; i < rowCount(); ++i) { queue.append(QPersistentModelIndex(index(i, 0))); } // Breadth-first traverse. while (!queue.isEmpty()) { QPersistentModelIndex node = queue.takeFirst(); Archive::Entry *entry = entryForIndex(node); if (!hasChildren(node)) { if (entry->fullPath().isEmpty()) { nodesToDelete << node; } } else { for (int i = 0; i < rowCount(node); ++i) { queue.append(QPersistentModelIndex(index(i, 0, node))); } } } for (const QPersistentModelIndex& node : qAsConst(nodesToDelete)) { Archive::Entry *rawEntry = static_cast(node.internalPointer()); qCDebug(ARK) << "Delete with parent entries " << rawEntry->getParent()->entries() << " and row " << rawEntry->row(); beginRemoveRows(parent(node), rawEntry->row(), rawEntry->row()); - m_entryIcons.remove(rawEntry->getParent()->entries().at(rawEntry->row())->fullPath(NoTrailingSlash)); rawEntry->getParent()->removeEntryAt(rawEntry->row()); endRemoveRows(); } } void ArchiveModel::countEntriesAndSize() { // This function is used to count the number of folders/files and // the total compressed size. This is needed for PropertiesDialog // to update the corresponding values after adding/deleting files. // When ArchiveModel has been properly fixed, this code can likely // be removed. m_numberOfFiles = 0; m_numberOfFolders = 0; m_uncompressedSize = 0; QElapsedTimer timer; timer.start(); traverseAndCountDirNode(m_rootEntry.data()); qCDebug(ARK) << "Time to count entries and size:" << timer.elapsed() << "ms"; } void ArchiveModel::traverseAndCountDirNode(Archive::Entry *dir) { const auto entries = dir->entries(); for (Archive::Entry *entry : entries) { if (entry->isDir()) { traverseAndCountDirNode(entry); m_numberOfFolders++; } else { m_numberOfFiles++; m_uncompressedSize += entry->property("size").toULongLong(); } } } qulonglong ArchiveModel::numberOfFiles() const { return m_numberOfFiles; } qulonglong ArchiveModel::numberOfFolders() const { return m_numberOfFolders; } qulonglong ArchiveModel::uncompressedSize() const { return m_uncompressedSize; } QList ArchiveModel::shownColumns() const { return m_showColumns; } QMap ArchiveModel::propertiesMap() const { return m_propertiesMap; } diff --git a/part/archivemodel.h b/part/archivemodel.h index 9f68da15..9fda29ca 100644 --- a/part/archivemodel.h +++ b/part/archivemodel.h @@ -1,204 +1,202 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef ARCHIVEMODEL_H #define ARCHIVEMODEL_H #include "archiveentry.h" #include #include #include using Kerfuffle::Archive; namespace Kerfuffle { class Query; } /** * Meta data related to one entry in a compressed archive. * * This is used for indexing entry properties as numbers * and for determining data displaying order in part's view. */ enum EntryMetaDataType { FullPath, /**< The entry's file name */ Size, /**< The entry's original size */ CompressedSize, /**< The compressed size for the entry */ Permissions, /**< The entry's permissions */ Owner, /**< The user the entry belongs to */ Group, /**< The user group the entry belongs to */ Ratio, /**< The compression ratio for the entry */ CRC, /**< The entry's CRC */ BLAKE2, /**< The entry's BLAKE2 */ Method, /**< The compression method used on the entry */ Version, /**< The archiver version needed to extract the entry */ Timestamp /**< The timestamp for the current entry */ }; class ArchiveModel: public QAbstractItemModel { Q_OBJECT public: explicit ArchiveModel(const QString &dbusPathName, QObject *parent = nullptr); ~ArchiveModel() override; QVariant data(const QModelIndex &index, int role) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; //drag and drop related Qt::DropActions supportedDropActions() const override; QStringList mimeTypes() const override; QMimeData *mimeData(const QModelIndexList & indexes) const override; bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override; void reset(); void createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent); KJob* loadArchive(const QString &path, const QString &mimeType, QObject *parent); Kerfuffle::Archive *archive() const; QList shownColumns() const; QMap propertiesMap() const; Archive::Entry *entryForIndex(const QModelIndex &index); Kerfuffle::ExtractJob* extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::ExtractJob* extractFiles(const QVector& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::PreviewJob* preview(Archive::Entry *file) const; Kerfuffle::OpenJob* open(Archive::Entry *file) const; Kerfuffle::OpenWithJob* openWith(Archive::Entry *file) const; Kerfuffle::AddJob* addFiles(QVector &entries, const Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::MoveJob* moveFiles(QVector &entries, Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::CopyJob* copyFiles(QVector &entries, Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::DeleteJob* deleteFiles(QVector entries); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encryptArchive(const QString &password, bool encryptHeader); void countEntriesAndSize(); qulonglong numberOfFiles() const; qulonglong numberOfFolders() const; qulonglong uncompressedSize() const; /** * Constructs a list of conflicting entries. * * @param conflictingEntries Reference to the empty mutable entries list, which will be constructed. * If the method returns false, this list will contain only entries which produce a critical conflict. * @param entries New entries paths list. * @param allowMerging Boolean variable indicating whether merging is permitted. * If true, existing entries won't generate an error. * * @return Boolean variable indicating whether conflicts are not critical (true for not critical, * false for critical). For example, if there are both "some/file" (not a directory) and "some/file/" (a directory) * entries for both new and existing paths, the method will return false. Also, if merging is not allowed, * this method will return false for entries with the same path and types. */ bool conflictingEntries(QList &conflictingEntries, const QStringList &entries, bool allowMerging) const; static bool hasDuplicatedEntries(const QStringList &entries); static QMap entryMap(const QVector &entries); - const QHash entryIcons() const; - QMap filesToMove; QMap filesToCopy; Q_SIGNALS: void loadingStarted(); void loadingFinished(KJob *); void extractionFinished(bool success); void error(const QString& error, const QString& details); void droppedFiles(const QStringList& files, const Archive::Entry*); void messageWidget(KMessageWidget::MessageType type, const QString& msg); private Q_SLOTS: void slotNewEntry(Archive::Entry *entry); void slotListEntry(Archive::Entry *entry); void slotLoadingFinished(KJob *job); void slotEntryRemoved(const QString & path); void slotUserQuery(Kerfuffle::Query *query); void slotCleanupEmptyDirs(); private: /** * Strips file names that start with './'. * * For more information, see bug 194241. * * @param fileName The file name that will be stripped. * * @return @p fileName without the leading './' */ QString cleanFileName(const QString& fileName); void initRootEntry(); enum InsertBehaviour { NotifyViews, DoNotNotifyViews }; Archive::Entry *parentFor(const Kerfuffle::Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews); QModelIndex indexForEntry(Archive::Entry *entry); static bool compareAscending(const QModelIndex& a, const QModelIndex& b); static bool compareDescending(const QModelIndex& a, const QModelIndex& b); /** * Insert the node @p node into the model, ensuring all views are notified * of the change. */ void insertEntry(Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews); void newEntry(Kerfuffle::Archive::Entry *receivedEntry, InsertBehaviour behaviour); void traverseAndCountDirNode(Archive::Entry *dir); QList m_showColumns; QScopedPointer m_archive; QScopedPointer m_rootEntry; QHash m_entryIcons; QMap m_propertiesMap; QString m_dbusPathName; qulonglong m_numberOfFiles; qulonglong m_numberOfFolders; qulonglong m_uncompressedSize; // Whether a file entry has been listed. Used to ensure all relevant columns are shown, // since directories might have fewer columns than files. bool m_fileEntryListed; }; #endif // ARCHIVEMODEL_H diff --git a/part/overwritedialog.cpp b/part/overwritedialog.cpp index 0987afdd..6764d8e2 100644 --- a/part/overwritedialog.cpp +++ b/part/overwritedialog.cpp @@ -1,67 +1,67 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "overwritedialog.h" #include using namespace Kerfuffle; -OverwriteDialog::OverwriteDialog(QWidget *parent, const QList &entries, const QHash &icons, bool error) +OverwriteDialog::OverwriteDialog(QWidget *parent, const QList &entries, bool error) : QDialog(parent) , m_buttonBox(QDialogButtonBox::Cancel, Qt::Horizontal) { m_vBoxLayout.addLayout(&m_messageLayout); m_vBoxLayout.addWidget(&m_entriesList); m_vBoxLayout.addWidget(&m_buttonBox); m_messageLayout.addWidget(&m_messageIcon); m_messageLayout.addWidget(&m_messageText); m_messageIcon.setPixmap(QIcon::fromTheme(QStringLiteral("dialog-warning")).pixmap(QSize(64, 64))); if (error) { m_messageText.setText(i18n("Files with the following paths already exist. Remove them if you really want to overwrite.")); } else { m_messageText.setText(i18n("Files with the following paths already exist. Do you want to continue overwriting them?")); m_buttonBox.addButton(QDialogButtonBox::Ok); } connect(&m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(&m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); for (const Archive::Entry *entry : entries) { - QListWidgetItem *item = new QListWidgetItem(icons.value(entry->fullPath(NoTrailingSlash)), entry->fullPath(NoTrailingSlash)); + QListWidgetItem *item = new QListWidgetItem(entry->icon(), entry->fullPath(NoTrailingSlash)); m_entriesList.addItem(item); } setLayout(&m_vBoxLayout); setFixedSize(window()->sizeHint()); } OverwriteDialog::~OverwriteDialog() { } diff --git a/part/overwritedialog.h b/part/overwritedialog.h index a1e4c0e9..107c1484 100644 --- a/part/overwritedialog.h +++ b/part/overwritedialog.h @@ -1,58 +1,58 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef OVERWRITEDIALOG_H #define OVERWRITEDIALOG_H #include "archiveentry.h" #include #include #include #include #include #include class OverwriteDialog : public QDialog { Q_OBJECT public: - explicit OverwriteDialog(QWidget *parent, const QList &entries, const QHash &icons, bool error = false); + explicit OverwriteDialog(QWidget *parent, const QList &entries, bool error = false); ~OverwriteDialog() override; private: QVBoxLayout m_vBoxLayout; QHBoxLayout m_messageLayout; QLabel m_messageIcon; QLabel m_messageText; QListWidget m_entriesList; QDialogButtonBox m_buttonBox; }; #endif diff --git a/part/part.cpp b/part/part.cpp index e080d961..5fb869ee 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1749 +1,1749 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "part.h" #include "ark_debug.h" #include "adddialog.h" #include "overwritedialog.h" #include "archiveformat.h" #include "archivemodel.h" #include "archivesortfiltermodel.h" #include "archiveview.h" #include "arkviewer.h" #include "dnddbusinterfaceadaptor.h" #include "infopanel.h" #include "jobtracker.h" #include "generalsettingspage.h" #include "extractiondialog.h" #include "extractionsettingspage.h" #include "jobs.h" #include "settings.h" #include "previewsettingspage.h" #include "propertiesdialog.h" #include "pluginsettingspage.h" #include "pluginmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kerfuffle; namespace Ark { static quint32 s_instanceCounter = 1; Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args) : KParts::ReadWritePart(parent), m_splitter(nullptr), m_busy(false), m_jobTracker(nullptr) { Q_UNUSED(args) KAboutData aboutData(QStringLiteral("ark"), i18n("ArkPart"), QStringLiteral("3.0")); setComponentData(aboutData, false); new DndExtractAdaptor(this); const QString pathName = QStringLiteral("/DndExtract/%1").arg(s_instanceCounter++); if (!QDBusConnection::sessionBus().registerObject(pathName, this)) { qCCritical(ARK) << "Could not register a D-Bus object for drag'n'drop"; } // m_vlayout is needed for later insertion of QMessageWidget QWidget *mainWidget = new QWidget; m_vlayout = new QVBoxLayout; m_model = new ArchiveModel(pathName, this); m_filterModel = new ArchiveSortFilterModel(this); m_splitter = new QSplitter(Qt::Horizontal, parentWidget); m_view = new ArchiveView; m_infoPanel = new InfoPanel(m_model); // Add widgets for the comment field. m_commentView = new QPlainTextEdit(); m_commentView->setReadOnly(true); m_commentView->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_commentBox = new QGroupBox(i18n("Comment")); m_commentBox->hide(); QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(m_commentView); m_commentBox->setLayout(vbox); m_messageWidget = new KMessageWidget(parentWidget); m_messageWidget->setWordWrap(true); m_messageWidget->hide(); m_commentMsgWidget = new KMessageWidget(); m_commentMsgWidget->setText(i18n("Comment has been modified.")); m_commentMsgWidget->setMessageType(KMessageWidget::Information); m_commentMsgWidget->setCloseButtonVisible(false); m_commentMsgWidget->hide(); QAction *saveAction = new QAction(i18n("Save"), m_commentMsgWidget); m_commentMsgWidget->addAction(saveAction); connect(saveAction, &QAction::triggered, this, &Part::slotAddComment); m_commentBox->layout()->addWidget(m_commentMsgWidget); connect(m_commentView, &QPlainTextEdit::textChanged, this, &Part::slotCommentChanged); setWidget(mainWidget); mainWidget->setLayout(m_vlayout); // Setup search widget. m_searchWidget = new QWidget(parentWidget); m_searchWidget->setVisible(false); m_searchWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); QHBoxLayout *searchLayout = new QHBoxLayout; searchLayout->setContentsMargins(2, 2, 2, 2); m_vlayout->addWidget(m_searchWidget); m_searchWidget->setLayout(searchLayout); m_searchCloseButton = new QPushButton(QIcon::fromTheme(QStringLiteral("dialog-close")), QString(), m_searchWidget); m_searchCloseButton->setFlat(true); m_searchLineEdit = new QLineEdit(m_searchWidget); m_searchLineEdit->setClearButtonEnabled(true); m_searchLineEdit->setPlaceholderText(i18n("Type to search...")); mainWidget->installEventFilter(this); searchLayout->addWidget(m_searchCloseButton); searchLayout->addWidget(m_searchLineEdit); connect(m_searchCloseButton, &QPushButton::clicked, this, [=]() { m_searchWidget->hide(); m_searchLineEdit->clear(); }); connect(m_searchLineEdit, &QLineEdit::textChanged, this, &Part::searchEdited); // Configure the QVBoxLayout and add widgets m_vlayout->setContentsMargins(0,0,0,0); m_vlayout->addWidget(m_messageWidget); m_vlayout->addWidget(m_splitter); // Vertical QSplitter for the file view and comment field. m_commentSplitter = new QSplitter(Qt::Vertical, parentWidget); m_commentSplitter->setOpaqueResize(false); m_commentSplitter->addWidget(m_view); m_commentSplitter->addWidget(m_commentBox); m_commentSplitter->setCollapsible(0, false); // Horizontal QSplitter for the file view and infopanel. m_splitter->addWidget(m_commentSplitter); m_splitter->addWidget(m_infoPanel); // Read settings from config file and show/hide infoPanel. if (!ArkSettings::showInfoPanel()) { m_infoPanel->hide(); } else { m_splitter->setSizes(ArkSettings::splitterSizes()); } setupView(); setupActions(); connect(m_view, &ArchiveView::entryChanged, this, &Part::slotRenameFile); connect(m_model, &ArchiveModel::loadingStarted, this, &Part::slotLoadingStarted); connect(m_model, &ArchiveModel::loadingFinished, this, &Part::slotLoadingFinished); connect(m_model, &ArchiveModel::droppedFiles, this, &Part::slotDroppedFiles); connect(m_model, &ArchiveModel::error, this, &Part::slotError); connect(m_model, &ArchiveModel::messageWidget, this, &Part::displayMsgWidget); connect(this, &Part::busy, this, &Part::setBusyGui); connect(this, &Part::ready, this, &Part::setReadyGui); connect(this, &KParts::ReadOnlyPart::urlChanged, this, &Part::setFileNameFromArchive); connect(this, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &Part::setFileNameFromArchive); connect(this, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &Part::slotCompleted); connect(ArkSettings::self(), &KCoreConfigSkeleton::configChanged, this, &Part::updateActions); m_statusBarExtension = new KParts::StatusBarExtension(this); setXMLFile(QStringLiteral("ark_part.rc")); } Part::~Part() { qDeleteAll(m_tmpExtractDirList); // Only save splitterSizes if infopanel is visible, // because we don't want to store zero size for infopanel. if (m_showInfoPanelAction->isChecked()) { ArkSettings::setSplitterSizes(m_splitter->sizes()); } ArkSettings::setShowInfoPanel(m_showInfoPanelAction->isChecked()); ArkSettings::self()->save(); m_extractArchiveAction->menu()->deleteLater(); m_extractAction->menu()->deleteLater(); } void Part::slotCommentChanged() { if (!m_model->archive() || m_commentView->toPlainText().isEmpty()) { return; } if (m_commentMsgWidget->isHidden() && m_commentView->toPlainText() != m_model->archive()->comment()) { m_commentMsgWidget->animatedShow(); } else if (m_commentMsgWidget->isVisible() && m_commentView->toPlainText() == m_model->archive()->comment()) { m_commentMsgWidget->hide(); } } void Part::registerJob(KJob* job) { if (!m_jobTracker) { m_jobTracker = new JobTracker(widget()); m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(nullptr), 0, true); m_jobTracker->widget(job)->show(); } KIO::getJobTracker()->registerJob(job); m_jobTracker->registerJob(job); emit busy(); connect(job, &KJob::result, this, &Part::ready); } // TODO: KIO::mostLocalHere is used here to resolve some KIO URLs to local // paths (e.g. desktop:/), but more work is needed to support extraction // to non-local destinations. See bugs #189322 and #204323. void Part::extractSelectedFilesTo(const QString& localPath) { if (!m_model) { return; } const QUrl url = QUrl::fromUserInput(localPath, QString()); KIO::StatJob* statJob = nullptr; // Try to resolve the URL to a local path. if (!url.isLocalFile() && !url.scheme().isEmpty()) { statJob = KIO::mostLocalUrl(url); if (!statJob->exec() || statJob->error() != 0) { return; } } const QString destination = statJob ? statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : localPath; delete statJob; // The URL could not be resolved to a local path. if (!url.isLocalFile() && destination.isEmpty()) { qCWarning(ARK) << "Ark cannot extract to non-local destination:" << localPath; KMessageBox::sorry(widget(), xi18nc("@info", "Ark can only extract to local destinations.")); return; } qCDebug(ARK) << "Extract to" << destination; Kerfuffle::ExtractionOptions options; options.setDragAndDropEnabled(true); // Create and start the ExtractJob. ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(getSelectedIndexes())), destination, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } void Part::guiActivateEvent(KParts::GUIActivateEvent *event) { // #357660: prevent parent's implementation from changing the window title. Q_UNUSED(event) } void Part::setupView() { m_view->setContextMenuPolicy(Qt::CustomContextMenu); m_filterModel->setSourceModel(m_model); m_view->setModel(m_filterModel); m_filterModel->setFilterKeyColumn(0); m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::updateActions); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::selectionChanged); connect(m_view, &QTreeView::activated, this, &Part::slotActivated); connect(m_view, &QWidget::customContextMenuRequested, this, &Part::slotShowContextMenu); } void Part::slotActivated(const QModelIndex &index) { Q_UNUSED(index) // The activated signal is emitted when items are selected with the mouse, // so do nothing if CTRL or SHIFT key is pressed. if (QGuiApplication::keyboardModifiers() != Qt::ShiftModifier && QGuiApplication::keyboardModifiers() != Qt::ControlModifier) { ArkSettings::defaultOpenAction() == ArkSettings::EnumDefaultOpenAction::Preview ? slotOpenEntry(Preview) : slotOpenEntry(OpenFile); } } void Part::setupActions() { m_showInfoPanelAction = new KToggleAction(i18nc("@action:inmenu", "Show Information Panel"), this); actionCollection()->addAction(QStringLiteral( "show-infopanel" ), m_showInfoPanelAction); m_showInfoPanelAction->setChecked(ArkSettings::showInfoPanel()); connect(m_showInfoPanelAction, &QAction::triggered, this, &Part::slotToggleInfoPanel); m_saveAsAction = KStandardAction::saveAs(this, &Part::slotSaveAs, nullptr); actionCollection()->addAction(QStringLiteral("ark_file_save_as"), m_saveAsAction); m_openFileAction = actionCollection()->addAction(QStringLiteral("openfile")); m_openFileAction->setText(i18nc("open a file with external program", "&Open")); m_openFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with the associated application")); connect(m_openFileAction, &QAction::triggered, this, [this]() { slotOpenEntry(OpenFile); }); m_openFileWithAction = actionCollection()->addAction(QStringLiteral("openfilewith")); m_openFileWithAction->setText(i18nc("open a file with external program", "Open &With...")); m_openFileWithAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileWithAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with an external program")); connect(m_openFileWithAction, &QAction::triggered, this, [this]() { slotOpenEntry(OpenFileWith); }); m_previewAction = actionCollection()->addAction(QStringLiteral("preview")); m_previewAction->setText(i18nc("to preview a file inside an archive", "Pre&view")); m_previewAction->setIcon(QIcon::fromTheme(QStringLiteral("document-preview-archive"))); m_previewAction->setToolTip(i18nc("@info:tooltip", "Click to preview the selected file")); actionCollection()->setDefaultShortcut(m_previewAction, Qt::CTRL + Qt::Key_P); connect(m_previewAction, &QAction::triggered, this, [this]() { slotOpenEntry(Preview); }); m_extractArchiveAction = actionCollection()->addAction(QStringLiteral("extract_all")); m_extractArchiveAction->setText(i18nc("@action:inmenu", "E&xtract All")); m_extractArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); m_extractArchiveAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose how to extract all the files in the archive")); actionCollection()->setDefaultShortcut(m_extractArchiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_E); connect(m_extractArchiveAction, &QAction::triggered, this, &Part::slotExtractArchive); m_extractAction = actionCollection()->addAction(QStringLiteral("extract")); m_extractAction->setText(i18nc("@action:inmenu", "&Extract")); m_extractAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); actionCollection()->setDefaultShortcut(m_extractAction, Qt::CTRL + Qt::Key_E); m_extractAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose to extract either all files or just the selected ones")); connect(m_extractAction, &QAction::triggered, this, &Part::slotShowExtractionDialog); m_addFilesAction = actionCollection()->addAction(QStringLiteral("add")); m_addFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert"))); m_addFilesAction->setText(i18n("Add &Files...")); m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); actionCollection()->setDefaultShortcut(m_addFilesAction, Qt::ALT + Qt::Key_A); connect(m_addFilesAction, &QAction::triggered, this, QOverload<>::of(&Part::slotAddFiles)); m_renameFileAction = KStandardAction::renameFile(m_view, &ArchiveView::renameSelectedEntry, actionCollection()); m_deleteFilesAction = KStandardAction::deleteFile(this, &Part::slotDeleteFiles, actionCollection()); m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove"))); actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete); m_cutFilesAction = KStandardAction::cut(this, &Part::slotCutFiles, actionCollection()); m_copyFilesAction = KStandardAction::copy(this, &Part::slotCopyFiles, actionCollection()); m_pasteFilesAction = KStandardAction::paste(this, QOverload<>::of(&Part::slotPasteFiles), actionCollection()); m_propertiesAction = actionCollection()->addAction(QStringLiteral("properties")); m_propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); m_propertiesAction->setText(i18nc("@action:inmenu", "&Properties")); actionCollection()->setDefaultShortcut(m_propertiesAction, Qt::ALT + Qt::Key_Return); m_propertiesAction->setToolTip(i18nc("@info:tooltip", "Click to see properties for archive")); connect(m_propertiesAction, &QAction::triggered, this, &Part::slotShowProperties); m_editCommentAction = actionCollection()->addAction(QStringLiteral("edit_comment")); m_editCommentAction->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); actionCollection()->setDefaultShortcut(m_editCommentAction, Qt::ALT + Qt::Key_C); m_editCommentAction->setToolTip(i18nc("@info:tooltip", "Click to add or edit comment")); connect(m_editCommentAction, &QAction::triggered, this, &Part::slotShowComment); m_testArchiveAction = actionCollection()->addAction(QStringLiteral("test_archive")); m_testArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("checkmark"))); m_testArchiveAction->setText(i18nc("@action:inmenu", "&Test Integrity")); actionCollection()->setDefaultShortcut(m_testArchiveAction, Qt::ALT + Qt::Key_T); m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity")); connect(m_testArchiveAction, &QAction::triggered, this, &Part::slotTestArchive); m_searchAction = KStandardAction::find(this, &Part::slotShowFind, actionCollection()); updateActions(); updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); } void Part::updateActions() { const bool isWritable = isArchiveWritable(); const Archive::Entry *entry = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())); int selectedEntriesCount = m_view->selectionModel()->selectedRows().count(); // We disable adding files if the archive is encrypted but the password is // unknown (this happens when opening existing non-he password-protected // archives). If we added files they would not get encrypted resulting in an // archive with a mixture of encrypted and unencrypted files. const bool isEncryptedButUnknownPassword = m_model->archive() && m_model->archive()->encryptionType() != Archive::Unencrypted && m_model->archive()->password().isEmpty(); if (isEncryptedButUnknownPassword) { m_addFilesAction->setToolTip(xi18nc("@info:tooltip", "Adding files to existing password-protected archives with no header-encryption is currently not supported." "Extract the files and create a new archive if you want to add files.")); m_testArchiveAction->setToolTip(xi18nc("@info:tooltip", "Testing password-protected archives with no header-encryption is currently not supported.")); } else { m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity")); } // Figure out if entry size is larger than preview size limit. const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024; const bool limit = ArkSettings::limitPreviewFileSize(); bool isPreviewable = (!limit || (limit && entry != nullptr && entry->property("size").toLongLong() < maxPreviewSize)); const bool isDir = (entry == nullptr) ? false : entry->isDir(); m_previewAction->setEnabled(!isBusy() && isPreviewable && !isDir && (selectedEntriesCount == 1)); m_extractArchiveAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_extractAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_saveAsAction->setEnabled(!isBusy() && m_model->rowCount() > 0); m_addFilesAction->setEnabled(!isBusy() && isWritable && !isEncryptedButUnknownPassword); m_deleteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_openFileAction->setEnabled(!isBusy() && isPreviewable && !isDir && (selectedEntriesCount == 1)); m_openFileWithAction->setEnabled(!isBusy() && isPreviewable && !isDir && (selectedEntriesCount == 1)); m_propertiesAction->setEnabled(!isBusy() && m_model->archive()); m_renameFileAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount == 1)); m_cutFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_copyFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_pasteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount == 0 || (selectedEntriesCount == 1 && isDir)) && (m_model->filesToMove.count() > 0 || m_model->filesToCopy.count() > 0)); m_searchAction->setEnabled(!isBusy() && m_model->rowCount() > 0); m_commentView->setEnabled(!isBusy()); m_commentMsgWidget->setEnabled(!isBusy()); m_editCommentAction->setEnabled(false); m_testArchiveAction->setEnabled(false); if (m_model->archive()) { const KPluginMetaData metadata = PluginManager().preferredPluginFor(m_model->archive()->mimeType())->metaData(); bool supportsWriteComment = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsWriteComment(); m_editCommentAction->setEnabled(!isBusy() && supportsWriteComment); m_commentView->setReadOnly(!supportsWriteComment); m_editCommentAction->setText(m_model->archive()->hasComment() ? i18nc("@action:inmenu mutually exclusive with Add &Comment", "Edit &Comment") : i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); bool supportsTesting = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsTesting(); m_testArchiveAction->setEnabled(!isBusy() && supportsTesting && !isEncryptedButUnknownPassword); } else { m_commentView->setReadOnly(true); m_editCommentAction->setText(i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); } } void Part::slotShowComment() { if (!m_commentBox->isVisible()) { m_commentBox->show(); m_commentSplitter->setSizes(QList() << static_cast(m_view->height() * 0.6) << 1); } m_commentView->setFocus(); } void Part::slotAddComment() { CommentJob *job = m_model->archive()->addComment(m_commentView->toPlainText()); if (!job) { return; } registerJob(job); job->start(); m_commentMsgWidget->hide(); if (m_commentView->toPlainText().isEmpty()) { m_commentBox->hide(); } } void Part::slotTestArchive() { TestJob *job = m_model->archive()->testArchive(); if (!job) { return; } registerJob(job); connect(job, &KJob::result, this, &Part::slotTestingDone); job->start(); } bool Part::isArchiveWritable() const { return isReadWrite() && m_model->archive() && !m_model->archive()->isReadOnly(); } bool Part::isCreatingNewArchive() const { return arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); } void Part::createArchive() { const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; m_model->createEmptyArchive(localFilePath(), fixedMimeType, m_model); if (arguments().metaData().contains(QLatin1String("volumeSize"))) { m_model->archive()->setMultiVolume(true); } const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")]; if (!password.isEmpty()) { m_model->encryptArchive(password, arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true")); } } void Part::loadArchive() { const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; auto job = m_model->loadArchive(localFilePath(), fixedMimeType, m_model); if (job) { registerJob(job); job->start(); } else { updateActions(); } } void Part::resetArchive() { m_view->setDropsEnabled(false); m_model->reset(); closeUrl(); setFileNameFromArchive(); updateActions(); } void Part::resetGui() { m_messageWidget->hide(); m_commentView->clear(); m_commentBox->hide(); m_infoPanel->updateWithDefaults(); // Also reset format-specific compression options. m_compressionOptions = CompressionOptions(); } void Part::slotTestingDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else if (static_cast(job)->testSucceeded()) { KMessageBox::information(widget(), i18n("The archive passed the integrity test."), i18n("Test Results")); } else { KMessageBox::error(widget(), i18n("The archive failed the integrity test."), i18n("Test Results")); } } void Part::updateQuickExtractMenu(QAction *extractAction) { if (!extractAction) { return; } QMenu *menu = extractAction->menu(); if (!menu) { menu = new QMenu(); extractAction->setMenu(menu); connect(menu, &QMenu::triggered, this, &Part::slotQuickExtractFiles); // Remember to keep this action's properties as similar to // extractAction's as possible (except where it does not make // sense, such as the text or the shortcut). QAction *extractTo = menu->addAction(i18n("Extract To...")); extractTo->setIcon(extractAction->icon()); extractTo->setToolTip(extractAction->toolTip()); if (extractAction == m_extractArchiveAction) { connect(extractTo, &QAction::triggered, this, &Part::slotExtractArchive); } else { connect(extractTo, &QAction::triggered, this, &Part::slotShowExtractionDialog); } menu->addSeparator(); QAction *header = menu->addAction(i18n("Quick Extract To...")); header->setEnabled(false); header->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); } while (menu->actions().size() > 3) { menu->removeAction(menu->actions().constLast()); } const KConfigGroup conf(KSharedConfig::openConfig(), "ExtractDialog"); const QStringList dirHistory = conf.readPathEntry("DirHistory", QStringList()); for (int i = 0; i < qMin(10, dirHistory.size()); ++i) { const QString dir = QUrl(dirHistory.value(i)).toString(QUrl::RemoveScheme | QUrl::NormalizePathSegments | QUrl::PreferLocalFile); if (QDir(dir).exists()) { QAction *newAction = menu->addAction(dir); newAction->setData(dir); } } } void Part::slotQuickExtractFiles(QAction *triggeredAction) { // #190507: triggeredAction->data.isNull() means it's the "Extract to..." // action, and we do not want it to run here if (!triggeredAction->data().isNull()) { QString userDestination = triggeredAction->data().toString(); QString finalDestinationDirectory; const QString detectedSubfolder = detectSubfolder(); qCDebug(ARK) << "Detected subfolder" << detectedSubfolder; if (m_model->archive()->hasMultipleTopLevelEntries()) { if (!userDestination.endsWith(QDir::separator())) { userDestination.append(QDir::separator()); } finalDestinationDirectory = userDestination + detectedSubfolder; QDir(userDestination).mkdir(detectedSubfolder); } else { finalDestinationDirectory = userDestination; } qCDebug(ARK) << "Extracting to:" << finalDestinationDirectory; ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(getSelectedIndexes())), finalDestinationDirectory, ExtractionOptions()); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } } void Part::selectionChanged() { m_infoPanel->setIndexes(getSelectedIndexes()); } QModelIndexList Part::getSelectedIndexes() { QModelIndexList list; const auto selectedRows = m_view->selectionModel()->selectedRows(); for (const QModelIndex &i : selectedRows) { list.append(m_filterModel->mapToSource(i)); } return list; } void Part::readCompressionOptions() { // Store options from CreateDialog if they are set. if (!m_compressionOptions.isCompressionLevelSet() && arguments().metaData().contains(QLatin1String("compressionLevel"))) { m_compressionOptions.setCompressionLevel(arguments().metaData()[QStringLiteral("compressionLevel")].toInt()); } if (m_compressionOptions.compressionMethod().isEmpty() && arguments().metaData().contains(QLatin1String("compressionMethod"))) { m_compressionOptions.setCompressionMethod(arguments().metaData()[QStringLiteral("compressionMethod")]); } if (m_compressionOptions.encryptionMethod().isEmpty() && arguments().metaData().contains(QLatin1String("encryptionMethod"))) { m_compressionOptions.setEncryptionMethod(arguments().metaData()[QStringLiteral("encryptionMethod")]); } if (!m_compressionOptions.isVolumeSizeSet() && arguments().metaData().contains(QLatin1String("volumeSize"))) { m_compressionOptions.setVolumeSize(arguments().metaData()[QStringLiteral("volumeSize")].toULong()); } const auto compressionMethods = m_model->archive()->property("compressionMethods").toStringList(); qCDebug(ARK) << "compmethods:" << compressionMethods; if (compressionMethods.size() == 1) { m_compressionOptions.setCompressionMethod(compressionMethods.first()); } } bool Part::openFile() { qCDebug(ARK) << "Attempting to open archive" << localFilePath(); resetGui(); if (!isLocalFileValid()) { return false; } if (isCreatingNewArchive()) { createArchive(); return true; } loadArchive(); // Loading is async, we don't know yet whether we got a valid archive. return false; } bool Part::saveFile() { return true; } bool Part::isBusy() const { return m_busy; } KConfigSkeleton *Part::config() const { return ArkSettings::self(); } QList Part::settingsPages(QWidget *parent) const { QList pages; pages.append(new GeneralSettingsPage(parent, i18nc("@title:tab", "General"), QStringLiteral("utilities-file-archiver"))); pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction"), QStringLiteral("preferences-desktop-icons"))); pages.append(new PluginSettingsPage(parent, i18nc("@title:tab", "Plugins"), QStringLiteral("preferences-plugin"))); pages.append(new PreviewSettingsPage(parent, i18nc("@title:tab", "Previews"), QStringLiteral("image-jpeg"))); return pages; } bool Part::isLocalFileValid() { const QString localFile = localFilePath(); const QFileInfo localFileInfo(localFile); if (localFileInfo.isDir()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "%1 is a directory.", localFile)); return false; } if (isCreatingNewArchive()) { if (localFileInfo.exists()) { if (!confirmAndDelete(localFile)) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Could not overwrite %1. Check whether you have write permission.", localFile)); return false; } } displayMsgWidget(KMessageWidget::Information, xi18nc("@info", "The archive %1 will be created as soon as you add a file.", localFile)); } else { if (!localFileInfo.exists()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 was not found.", localFile)); return false; } if (!localFileInfo.isReadable()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 could not be loaded, as it was not possible to read from it.", localFile)); return false; } } return true; } bool Part::confirmAndDelete(const QString &targetFile) { QFileInfo targetInfo(targetFile); const auto buttonCode = KMessageBox::warningYesNo(widget(), xi18nc("@info", "The archive %1 already exists. Do you wish to overwrite it?", targetInfo.fileName()), i18nc("@title:window", "File Exists"), KStandardGuiItem::overwrite(), KStandardGuiItem::cancel()); if (buttonCode != KMessageBox::Yes || !targetInfo.isWritable()) { return false; } qCDebug(ARK) << "Removing file" << targetFile; return QFile(targetFile).remove(); } void Part::slotCompleted() { if (isCreatingNewArchive()) { m_view->setDropsEnabled(true); updateActions(); return; } // Existing archive, setup the view for it. m_view->sortByColumn(0, Qt::AscendingOrder); m_view->expandIfSingleFolder(); m_view->header()->resizeSections(QHeaderView::ResizeToContents); m_view->setDropsEnabled(isArchiveWritable()); if (!m_model->archive()->comment().isEmpty()) { m_commentView->setPlainText(m_model->archive()->comment()); slotShowComment(); } else { m_commentView->clear(); m_commentBox->hide(); } if (m_model->rowCount() == 0) { qCWarning(ARK) << "No entry listed by the plugin"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "The archive is empty or Ark could not open its content.")); } else if (m_model->rowCount() == 1) { if (m_model->archive()->mimeType().inherits(QStringLiteral("application/x-cd-image")) && m_model->entryForIndex(m_model->index(0, 0))->fullPath() == QLatin1String("README.TXT")) { qCWarning(ARK) << "Detected ISO image with UDF filesystem"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "Ark does not currently support ISO files with UDF filesystem.")); } } if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) { QTimer::singleShot(0, this, &Part::slotShowExtractionDialog); } updateActions(); } void Part::slotLoadingStarted() { m_model->filesToMove.clear(); m_model->filesToCopy.clear(); } void Part::slotLoadingFinished(KJob *job) { if (!job->error()) { emit completed(); return; } // Loading failed or was canceled by the user (e.g. password dialog rejected). emit canceled(job->errorString()); resetArchive(); if (job->error() != KJob::KilledJobError) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive %1 failed with the following error:%2", localFilePath(), job->errorString())); } } void Part::setReadyGui() { QApplication::restoreOverrideCursor(); m_busy = false; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->hide(); } m_view->setEnabled(true); updateActions(); } void Part::setBusyGui() { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); m_busy = true; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->show(); } m_view->setEnabled(false); updateActions(); } void Part::setFileNameFromArchive() { const QString prettyName = url().fileName(); m_infoPanel->setPrettyFileName(prettyName); m_infoPanel->updateWithDefaults(); emit setWindowCaption(prettyName); } void Part::slotOpenEntry(int mode) { QModelIndex index = m_filterModel->mapToSource(m_view->selectionModel()->currentIndex()); Archive::Entry *entry = m_model->entryForIndex(index); // Don't open directories. if (entry->isDir()) { return; } // Don't open files bigger than the size limit. const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024; if (ArkSettings::limitPreviewFileSize() && entry->property("size").toLongLong() >= maxPreviewSize) { return; } // We don't support opening symlinks. if (!entry->property("link").toString().isEmpty()) { displayMsgWidget(KMessageWidget::Information, i18n("Ark cannot open symlinks.")); return; } // Extract the entry. if (!entry->fullPath().isEmpty()) { qCDebug(ARK) << "Opening with mode" << mode; m_openFileMode = static_cast(mode); KJob *job = nullptr; if (m_openFileMode == Preview) { job = m_model->preview(entry); connect(job, &KJob::result, this, &Part::slotPreviewExtractedEntry); } else { job = (m_openFileMode == OpenFile) ? m_model->open(entry) : m_model->openWith(entry); connect(job, &KJob::result, this, &Part::slotOpenExtractedEntry); } registerJob(job); job->start(); } } void Part::slotOpenExtractedEntry(KJob *job) { if (!job->error()) { OpenJob *openJob = qobject_cast(job); Q_ASSERT(openJob); // Since the user could modify the file (unlike the Preview case), // we'll need to manually delete the temp dir in the Part destructor. m_tmpExtractDirList << openJob->tempDir(); const QString fullName = openJob->validatedFilePath(); if (isArchiveWritable()) { m_fileWatcher = new QFileSystemWatcher; connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &Part::slotWatchedFileModified); m_fileWatcher->addPath(fullName); } else { // If archive is readonly set temporarily extracted file to readonly as // well so user will be notified if trying to modify and save the file. QFile::setPermissions(fullName, QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther); } if (qobject_cast(job)) { const QList urls = {QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile)}; KRun::displayOpenWithDialog(urls, widget()); } else { KRun::runUrl(QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile), QMimeDatabase().mimeTypeForFile(fullName).name(), widget(), KRun::RunFlags()); } } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotPreviewExtractedEntry(KJob *job) { if (!job->error()) { PreviewJob *previewJob = qobject_cast(job); Q_ASSERT(previewJob); m_tmpExtractDirList << previewJob->tempDir(); ArkViewer::view(previewJob->validatedFilePath()); } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotWatchedFileModified(const QString& file) { qCDebug(ARK) << "Watched file modified:" << file; // Find the relative path of the file within the archive. QString relPath = file; for (QTemporaryDir *tmpDir : qAsConst(m_tmpExtractDirList)) { relPath.remove(tmpDir->path()); //Remove tmpDir. } relPath.remove(0, 1); //Remove leading slash. if (relPath.contains(QLatin1Char('/'))) { relPath = relPath.section(QLatin1Char('/'), 0, -2); //Remove filename. } else { // File is in the root of the archive, no path. relPath = QString(); } // Set up a string for display in KMessageBox. QString prettyFilename; if (relPath.isEmpty()) { prettyFilename = file.section(QLatin1Char('/'), -1); } else { prettyFilename = relPath + QLatin1Char('/') + file.section(QLatin1Char('/'), -1); } if (KMessageBox::questionYesNo(widget(), xi18n("The file %1 was modified. Do you want to update the archive?", prettyFilename), i18nc("@title:window", "File Modified")) == KMessageBox::Yes) { QStringList list = QStringList() << file; qCDebug(ARK) << "Updating file" << file << "with path" << relPath; slotAddFiles(list, nullptr, relPath); } // This is needed because some apps, such as Kate, delete and recreate // files when saving. m_fileWatcher->addPath(file); } void Part::slotError(const QString& errorMessage, const QString& details) { if (details.isEmpty()) { KMessageBox::error(widget(), errorMessage); } else { KMessageBox::detailedError(widget(), errorMessage, details); } } QString Part::detectSubfolder() const { if (!m_model) { return QString(); } return m_model->archive()->subfolderName(); } void Part::slotExtractArchive() { if (m_view->selectionModel()->selectedRows().count() > 0) { m_view->selectionModel()->clear(); } slotShowExtractionDialog(); } void Part::slotShowExtractionDialog() { if (!m_model) { return; } QPointer dialog(new Kerfuffle::ExtractionDialog(widget())); dialog.data()->setModal(true); if (m_view->selectionModel()->selectedRows().count() > 0) { dialog.data()->setShowSelectedFiles(true); } dialog.data()->setExtractToSubfolder(m_model->archive()->hasMultipleTopLevelEntries()); dialog.data()->setSubfolder(detectSubfolder()); dialog.data()->setCurrentUrl(QUrl::fromLocalFile(QFileInfo(m_model->archive()->fileName()).absolutePath())); dialog.data()->show(); dialog.data()->restoreWindowSize(); if (dialog.data()->exec()) { updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); QVector files; // If the user has chosen to extract only selected entries, fetch these // from the QTreeView. if (!dialog.data()->extractAllFiles()) { files = filesAndRootNodesForIndexes(addChildren(getSelectedIndexes())); } qCDebug(ARK) << "Selected " << files; Kerfuffle::ExtractionOptions options; options.setPreservePaths(dialog->preservePaths()); const QString destinationDirectory = dialog.data()->destinationDirectory().toLocalFile(); ExtractJob *job = m_model->extractFiles(files, destinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } delete dialog.data(); } QModelIndexList Part::addChildren(const QModelIndexList &list) const { Q_ASSERT(m_model); QModelIndexList ret = list; // Iterate over indexes in list and add all children. for (int i = 0; i < ret.size(); ++i) { QModelIndex index = ret.at(i); for (int j = 0; j < m_model->rowCount(index); ++j) { QModelIndex child = m_model->index(j, 0, index); if (!ret.contains(child)) { ret << child; } } } return ret; } QVector Part::filesForIndexes(const QModelIndexList& list) const { QVector ret; for (const QModelIndex& index : list) { ret << m_model->entryForIndex(index); } return ret; } QVector Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { QVector fileList; QStringList fullPathsList; for (const QModelIndex& index : list) { // Find the topmost unselected parent. This is done by iterating up // through the directory hierarchy and see if each parent is included // in the selection OR if the parent is already part of list. // The latter is needed for unselected folders which are subfolders of // a selected parent folder. QModelIndex selectionRoot = index.parent(); while (m_view->selectionModel()->isSelected(selectionRoot) || list.contains(selectionRoot)) { selectionRoot = selectionRoot.parent(); } // Fetch the root node for the unselected parent. const QString rootFileName = selectionRoot.isValid() ? m_model->entryForIndex(selectionRoot)->fullPath() : QString(); // Append index with root node to fileList. QModelIndexList alist = QModelIndexList() << index; const auto filesIndexes = filesForIndexes(alist); for (Archive::Entry *entry : filesIndexes) { const QString fullPath = entry->fullPath(); if (!fullPathsList.contains(fullPath)) { entry->rootNode = rootFileName; fileList.append(entry); fullPathsList.append(fullPath); } } } return fileList; } void Part::slotExtractionDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { ExtractJob *extractJob = qobject_cast(job); Q_ASSERT(extractJob); if (ArkSettings::openDestinationFolderAfterExtraction()) { qCDebug(ARK) << "Shall open" << extractJob->destinationDirectory(); QUrl destinationDirectory = QUrl::fromLocalFile(extractJob->destinationDirectory()).adjusted(QUrl::NormalizePathSegments); qCDebug(ARK) << "Shall open URL" << destinationDirectory; KRun::runUrl(destinationDirectory, QStringLiteral("inode/directory"), widget(), KRun::RunExecutables, QString(), QByteArray()); } if (ArkSettings::closeAfterExtraction()) { emit quit(); } } } void Part::slotAddFiles(const QStringList& filesToAdd, const Archive::Entry *destination, const QString &relPath) { if (!m_model->archive() || filesToAdd.isEmpty()) { return; } QStringList withChildPaths; for (const QString& file : filesToAdd) { m_jobTempEntries.push_back(new Archive::Entry(nullptr, file)); if (QFileInfo(file).isDir()) { withChildPaths << file + QLatin1Char('/'); QDirIterator it(file, QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (it.hasNext()) { QString path = it.next(); if (it.fileInfo().isDir()) { path += QLatin1Char('/'); } withChildPaths << path; } } else { withChildPaths << file; } } withChildPaths = ReadOnlyArchiveInterface::entryPathsFromDestination(withChildPaths, destination, 0); QList conflictingEntries; bool error = m_model->conflictingEntries(conflictingEntries, withChildPaths, true); if (conflictingEntries.count() > 0) { - QPointer overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, m_model->entryIcons(), error); + QPointer overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, error); int ret = overwriteDialog->exec(); delete overwriteDialog; if (ret == QDialog::Rejected) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); return; } } // GlobalWorkDir is used by AddJob and should contain the part of the // absolute path of files to be added that should NOT be included in the // directory structure within the archive. // Example: We add file "/home/user/somedir/somefile.txt" and want the file // to have the relative path within the archive "somedir/somefile.txt". // GlobalWorkDir is then: "/home/user" QString globalWorkDir = filesToAdd.first(); // path represents the path of the file within the archive. This needs to // be removed from globalWorkDir, otherwise the files will be added to the // root of the archive. In the example above, path would be "somedir/". if (!relPath.isEmpty()) { globalWorkDir.remove(relPath); qCDebug(ARK) << "Adding" << filesToAdd << "to" << relPath; } else { qCDebug(ARK) << "Adding " << filesToAdd << ((destination == nullptr) ? QString() : QLatin1String("to ") + destination->fullPath()); } // Remove trailing slash (needed when adding dirs). if (globalWorkDir.right(1) == QLatin1String("/")) { globalWorkDir.chop(1); } // We need to override the global options with a working directory. CompressionOptions compOptions = m_compressionOptions; // Now take the absolute path of the parent directory. globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath(); qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir; compOptions.setGlobalWorkDir(globalWorkDir); AddJob *job = m_model->addFiles(m_jobTempEntries, destination, compOptions); if (!job) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); return; } connect(job, &KJob::result, this, &Part::slotAddFilesDone); registerJob(job); job->start(); } void Part::slotDroppedFiles(const QStringList &files, const Archive::Entry *destination) { readCompressionOptions(); slotAddFiles(files, destination, QString()); } void Part::slotAddFiles() { readCompressionOptions(); QString dialogTitle = i18nc("@title:window", "Add Files"); const Archive::Entry *destination = nullptr; if (m_view->selectionModel()->selectedRows().count() == 1) { destination = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())); if (destination->isDir()) { dialogTitle = i18nc("@title:window", "Add Files to %1", destination->fullPath()); } else { destination = nullptr; } } qCDebug(ARK) << "Opening AddDialog with opts:" << m_compressionOptions; // #264819: passing widget() as the parent will not work as expected. // KFileDialog will create a KFileWidget, which runs an internal // event loop to stat the given directory. This, in turn, leads to // events being delivered to widget(), which is a QSplitter, which // in turn reimplements childEvent() and will end up calling // QWidget::show() on the KFileDialog (thus showing it in a // non-modal state). // When KFileDialog::exec() is called, the widget is already shown // and nothing happens. QPointer dlg = new AddDialog(widget(), dialogTitle, m_lastUsedAddPath, m_model->archive()->mimeType(), m_compressionOptions); if (dlg->exec() == QDialog::Accepted) { qCDebug(ARK) << "Selected files:" << dlg->selectedFiles(); qCDebug(ARK) << "Options:" << dlg->compressionOptions(); m_compressionOptions = dlg->compressionOptions(); slotAddFiles(dlg->selectedFiles(), destination, QString()); } delete dlg; } void Part::slotCutFiles() { QModelIndexList selectedRows = addChildren(getSelectedIndexes()); m_model->filesToMove = ArchiveModel::entryMap(filesForIndexes(selectedRows)); qCDebug(ARK) << "Entries marked to cut:" << m_model->filesToMove.values(); m_model->filesToCopy.clear(); for (const QModelIndex &row : qAsConst(m_cutIndexes)) { m_view->dataChanged(row, row); } m_cutIndexes = selectedRows; for (const QModelIndex &row : qAsConst(m_cutIndexes)) { m_view->dataChanged(row, row); } updateActions(); } void Part::slotCopyFiles() { m_model->filesToCopy = ArchiveModel::entryMap(filesForIndexes(addChildren(getSelectedIndexes()))); qCDebug(ARK) << "Entries marked to copy:" << m_model->filesToCopy.values(); for (const QModelIndex &row : qAsConst(m_cutIndexes)) { m_view->dataChanged(row, row); } m_cutIndexes.clear(); m_model->filesToMove.clear(); updateActions(); } void Part::slotRenameFile(const QString &name) { if (name == QLatin1Char('.') || name == QLatin1String("..") || name.contains(QLatin1Char('/'))) { displayMsgWidget(KMessageWidget::Error, i18n("Filename can't contain slashes and can't be equal to \".\" or \"..\"")); return; } const Archive::Entry *entry = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())); QVector entriesToMove = filesForIndexes(addChildren(getSelectedIndexes())); m_destination = new Archive::Entry(); const QString &entryPath = entry->fullPath(NoTrailingSlash); const QString rootPath = entryPath.left(entryPath.count() - entry->name().count()); QString path = rootPath + name; if (entry->isDir()) { path += QLatin1Char('/'); } m_destination->setFullPath(path); slotPasteFiles(entriesToMove, m_destination, 1); } void Part::slotPasteFiles() { m_destination = (m_view->selectionModel()->selectedRows().count() > 0) ? m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())) : nullptr; if (m_destination == nullptr) { m_destination = new Archive::Entry(nullptr, QString()); } else { m_destination = new Archive::Entry(nullptr, m_destination->fullPath()); } if (m_model->filesToMove.count() > 0) { // Changing destination to include new entry path if pasting only 1 entry. QVector entriesWithoutChildren = ReadOnlyArchiveInterface::entriesWithoutChildren(QVector::fromList(m_model->filesToMove.values())); if (entriesWithoutChildren.count() == 1) { const Archive::Entry *entry = entriesWithoutChildren.first(); auto entryName = entry->name(); if (entry->isDir()) { entryName += QLatin1Char('/'); } m_destination->setFullPath(m_destination->fullPath() + entryName); } for (const Archive::Entry *entry : qAsConst(entriesWithoutChildren)) { if (entry->isDir() && m_destination->fullPath().startsWith(entry->fullPath())) { KMessageBox::error(widget(), i18n("Folders can't be moved into themselves."), i18n("Moving a folder into itself")); delete m_destination; return; } } auto entryList = QVector::fromList(m_model->filesToMove.values()); slotPasteFiles(entryList, m_destination, entriesWithoutChildren.count()); m_model->filesToMove.clear(); } else { auto entryList = QVector::fromList(m_model->filesToCopy.values()); slotPasteFiles(entryList, m_destination, 0); m_model->filesToCopy.clear(); } m_cutIndexes.clear(); updateActions(); } void Part::slotPasteFiles(QVector &files, Kerfuffle::Archive::Entry *destination, int entriesWithoutChildren) { if (files.isEmpty()) { delete m_destination; return; } QStringList filesPaths = ReadOnlyArchiveInterface::entryFullPaths(files); QStringList newPaths = ReadOnlyArchiveInterface::entryPathsFromDestination(filesPaths, destination, entriesWithoutChildren); if (ArchiveModel::hasDuplicatedEntries(newPaths)) { displayMsgWidget(KMessageWidget::Error, i18n("Entries with the same names can't be pasted to the same destination.")); delete m_destination; return; } QList conflictingEntries; bool error = m_model->conflictingEntries(conflictingEntries, newPaths, false); if (conflictingEntries.count() != 0) { - QPointer overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, m_model->entryIcons(), error); + QPointer overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, error); int ret = overwriteDialog->exec(); delete overwriteDialog; if (ret == QDialog::Rejected) { delete m_destination; return; } } if (entriesWithoutChildren > 0) { qCDebug(ARK) << "Moving" << files << "to" << destination; } else { qCDebug(ARK) << "Copying " << files << "to" << destination; } KJob *job; if (entriesWithoutChildren != 0) { job = m_model->moveFiles(files, destination, CompressionOptions()); } else { job = m_model->copyFiles(files, destination, CompressionOptions()); } if (job) { connect(job, &KJob::result, this, &Part::slotPasteFilesDone); registerJob(job); job->start(); } else { delete m_destination; } } void Part::slotAddFilesDone(KJob* job) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); m_messageWidget->hide(); if (job->error()) { if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else if (isCreatingNewArchive()) { resetArchive(); } } else { // For multi-volume archive, we need to re-open the archive after adding files // because the name changes from e.g name.rar to name.part1.rar. if (m_model->archive()->isMultiVolume()) { qCDebug(ARK) << "Multi-volume archive detected, re-opening..."; KParts::OpenUrlArguments args = arguments(); args.metaData()[QStringLiteral("createNewArchive")] = QStringLiteral("false"); setArguments(args); openUrl(QUrl::fromLocalFile(m_model->archive()->multiVolumeName())); } } m_cutIndexes.clear(); m_model->filesToMove.clear(); m_model->filesToCopy.clear(); } void Part::slotPasteFilesDone(KJob *job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } m_cutIndexes.clear(); m_model->filesToMove.clear(); m_model->filesToCopy.clear(); } void Part::slotDeleteFilesDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } m_cutIndexes.clear(); m_model->filesToMove.clear(); m_model->filesToCopy.clear(); } void Part::slotDeleteFiles() { const int selectionsCount = m_view->selectionModel()->selectedRows().count(); const auto reallyDelete = KMessageBox::questionYesNo(widget(), i18ncp("@info", "Deleting this file is not undoable. Are you sure you want to do this?", "Deleting these files is not undoable. Are you sure you want to do this?", selectionsCount), i18ncp("@title:window", "Delete File", "Delete Files", selectionsCount), KStandardGuiItem::del(), KStandardGuiItem::no(), QString(), KMessageBox::Dangerous | KMessageBox::Notify); if (reallyDelete == KMessageBox::No) { return; } DeleteJob *job = m_model->deleteFiles(filesForIndexes(addChildren(getSelectedIndexes()))); connect(job, &KJob::result, this, &Part::slotDeleteFilesDone); registerJob(job); job->start(); } void Part::slotShowProperties() { m_model->countEntriesAndSize(); QPointer dialog(new Kerfuffle::PropertiesDialog(nullptr, m_model->archive(), m_model->numberOfFiles(), m_model->numberOfFolders(), m_model->uncompressedSize())); dialog.data()->show(); } void Part::slotToggleInfoPanel(bool visible) { if (visible) { m_splitter->setSizes(ArkSettings::splitterSizes()); m_infoPanel->show(); } else { // We need to save the splitterSizes before hiding, otherwise // Ark won't remember resizing done by the user. ArkSettings::setSplitterSizes(m_splitter->sizes()); m_infoPanel->hide(); } } void Part::slotSaveAs() { QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18nc("@title:window", "Save Archive As"), url()); if ((saveUrl.isValid()) && (!saveUrl.isEmpty())) { auto statJob = KIO::stat(saveUrl, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(statJob, widget()); if (statJob->exec()) { int overwrite = KMessageBox::warningContinueCancel(widget(), xi18nc("@info", "An archive named %1 already exists. Are you sure you want to overwrite it?", saveUrl.fileName()), QString(), KStandardGuiItem::overwrite()); if (overwrite != KMessageBox::Continue) { return; } } QUrl srcUrl = QUrl::fromLocalFile(localFilePath()); if (!QFile::exists(localFilePath())) { if (url().isLocalFile()) { KMessageBox::error(widget(), xi18nc("@info", "The archive %1 cannot be copied to the specified location. The archive does not exist anymore.", localFilePath())); return; } else { srcUrl = url(); } } KIO::Job *copyJob = KIO::file_copy(srcUrl, saveUrl, -1, KIO::Overwrite); KJobWidgets::setWindow(copyJob, widget()); copyJob->exec(); if (copyJob->error()) { KMessageBox::error(widget(), xi18nc("@info", "The archive could not be saved as %1. Try saving it to another location.", saveUrl.path())); } } } void Part::slotShowContextMenu() { if (!factory()) { return; } QMenu *popup = static_cast(factory()->container(QStringLiteral("context_menu"), this)); popup->popup(QCursor::pos()); } bool Part::eventFilter(QObject *target, QEvent *event) { Q_UNUSED(target) if (event->type() == QEvent::KeyPress) { QKeyEvent *e = static_cast(event); if (e->key() == Qt::Key_Escape) { m_searchWidget->hide(); m_searchLineEdit->clear(); return true; } } return false; } void Part::slotShowFind() { if (m_searchWidget->isVisible()) { m_searchLineEdit->selectAll(); } else { m_searchWidget->show(); } m_searchLineEdit->setFocus(); } void Part::searchEdited(const QString &text) { m_view->collapseAll(); m_filterModel->setFilterFixedString(text); if(text.isEmpty()) { m_view->collapseAll(); m_view->expandIfSingleFolder(); } else { m_view->expandAll(); } } void Part::displayMsgWidget(KMessageWidget::MessageType type, const QString& msg) { // The widget could be already visible, so hide it. m_messageWidget->hide(); m_messageWidget->setText(msg); m_messageWidget->setMessageType(type); m_messageWidget->animatedShow(); } } // namespace Ark