diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp index ee7554cb..8a77c6b7 100644 --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -1,347 +1,347 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2002-2003: Georg Robbers * Copyright (C) 2003: Helio Chissini de Castro * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008 Harald Hvaal * * 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 "mainwindow.h" #include "ark_debug.h" #include "archive_kerfuffle.h" #include "createdialog.h" #include "settingsdialog.h" #include "settingspage.h" #include "pluginmanager.h" #include "interface.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool isValidArchiveDrag(const QMimeData *data) { return ((data->hasUrls()) && (data->urls().count() == 1)); } MainWindow::MainWindow(QWidget *) : KParts::MainWindow() { setupActions(); setAcceptDrops(true); // Ark doesn't provide a fullscreen mode; remove the corresponding window button setWindowFlags(windowFlags() & ~Qt::WindowFullscreenButtonHint); } MainWindow::~MainWindow() { if (m_recentFilesAction) { m_recentFilesAction->saveEntries(KSharedConfig::openConfig()->group("Recent Files")); } guiFactory()->removeClient(m_part); delete m_part; m_part = nullptr; } void MainWindow::dragEnterEvent(QDragEnterEvent * event) { qCDebug(ARK) << "dragEnterEvent" << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } const bool partAcceptsDrops = !m_part->url().isEmpty() && m_part->isReadWrite(); if (!event->source() && isValidArchiveDrag(event->mimeData()) && !partAcceptsDrops) { event->acceptProposedAction(); } return; } void MainWindow::dropEvent(QDropEvent * event) { qCDebug(ARK) << "dropEvent" << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } if ((event->source() == nullptr) && (isValidArchiveDrag(event->mimeData()))) { event->acceptProposedAction(); } //TODO: if this call provokes a message box the drag will still be going //while the box is onscreen. looks buggy, do something about it openUrl(event->mimeData()->urls().at(0)); } void MainWindow::dragMoveEvent(QDragMoveEvent * event) { qCDebug(ARK) << "dragMoveEvent" << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } if ((event->source() == nullptr) && (isValidArchiveDrag(event->mimeData()))) { event->acceptProposedAction(); } } bool MainWindow::loadPart() { KPluginFactory *factory = nullptr; const auto plugins = KPluginLoader::findPlugins(QString(), [](const KPluginMetaData& metaData) { return metaData.pluginId() == QStringLiteral("arkpart") && metaData.serviceTypes().contains(QStringLiteral("KParts/ReadOnlyPart")) && metaData.serviceTypes().contains(QStringLiteral("Browser/View")); }); if (!plugins.isEmpty()) { factory = KPluginLoader(plugins.first().fileName()).factory(); } m_part = factory ? static_cast(factory->create(this)) : nullptr; if (!m_part) { KMessageBox::error(this, i18n("Unable to find Ark's KPart component, please check your installation.")); qCWarning(ARK) << "Error loading Ark KPart."; return false; } m_part->setObjectName(QStringLiteral("ArkPart")); setCentralWidget(m_part->widget()); setXMLFile(QStringLiteral("arkui.rc")); setupGUI(ToolBar | Keys | Save); createGUI(m_part); statusBar()->hide(); connect(m_part, SIGNAL(ready()), this, SLOT(updateActions())); connect(m_part, SIGNAL(quit()), this, SLOT(quit())); // #365200: this will disable m_recentFilesAction, while openUrl() will enable it. // So updateActions() needs to be called after openUrl() returns. connect(m_part, SIGNAL(busy()), this, SLOT(updateActions()), Qt::QueuedConnection); - connect(m_part, static_cast(&KParts::ReadOnlyPart::completed), this, &MainWindow::addPartUrl); + connect(m_part, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &MainWindow::addPartUrl); return true; } void MainWindow::setupActions() { m_newAction = actionCollection()->addAction(KStandardAction::New, QStringLiteral("ark_file_new"), this, SLOT(newArchive())); m_openAction = actionCollection()->addAction(KStandardAction::Open, QStringLiteral("ark_file_open"), this, SLOT(openArchive())); actionCollection()->addAction(KStandardAction::Quit, QStringLiteral("ark_quit"), this, SLOT(quit())); m_recentFilesAction = KStandardAction::openRecent(this, SLOT(openUrl(QUrl)), nullptr); actionCollection()->addAction(QStringLiteral("ark_file_open_recent"), m_recentFilesAction); m_recentFilesAction->setToolBarMode(KRecentFilesAction::MenuMode); m_recentFilesAction->setToolButtonPopupMode(QToolButton::DelayedPopup); m_recentFilesAction->setIconText(i18nc("action, to open an archive", "Open")); m_recentFilesAction->setToolTip(i18n("Open an archive")); m_recentFilesAction->loadEntries(KSharedConfig::openConfig()->group("Recent Files")); KStandardAction::preferences(this, SLOT(showSettings()), actionCollection()); } void MainWindow::updateActions() { Interface *iface = qobject_cast(m_part); m_newAction->setEnabled(!iface->isBusy()); m_openAction->setEnabled(!iface->isBusy()); m_recentFilesAction->setEnabled(!iface->isBusy()); } void MainWindow::openArchive() { Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); Q_UNUSED(iface); Kerfuffle::PluginManager pluginManager; auto dlg = new QFileDialog(this, i18nc("to open an archive", "Open Archive")); dlg->setMimeTypeFilters(pluginManager.supportedMimeTypes(Kerfuffle::PluginManager::SortByComment)); dlg->setFileMode(QFileDialog::ExistingFile); dlg->setAcceptMode(QFileDialog::AcceptOpen); connect(dlg, &QDialog::finished, this, [this, dlg](int result) { if (result == QDialog::Accepted) { openUrl(dlg->selectedUrls().at(0)); } dlg->deleteLater(); }); dlg->open(); } void MainWindow::openUrl(const QUrl& url) { if (url.isEmpty()) { return; } m_part->setArguments(m_openArgs); m_part->openUrl(url); } void MainWindow::setShowExtractDialog(bool option) { if (option) { m_openArgs.metaData()[QStringLiteral("showExtractDialog")] = QStringLiteral("true"); } else { m_openArgs.metaData().remove(QStringLiteral("showExtractDialog")); } } void MainWindow::closeEvent(QCloseEvent *event) { // Preview windows don't have a parent, so we need to manually close them. foreach (QWidget *widget, qApp->topLevelWidgets()) { if (widget->isVisible()) { widget->close(); } } KParts::MainWindow::closeEvent(event); } void MainWindow::quit() { close(); } void MainWindow::showSettings() { if (KConfigDialog::showDialog(QStringLiteral("settings"))) { return; } Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); auto dialog = new Kerfuffle::SettingsDialog(this, QStringLiteral("settings"), iface->config()); foreach (Kerfuffle::SettingsPage *page, iface->settingsPages(this)) { dialog->addPage(page, page->name(), page->iconName()); connect(dialog, &KConfigDialog::settingsChanged, page, &Kerfuffle::SettingsPage::slotSettingsChanged); connect(dialog, &Kerfuffle::SettingsDialog::defaultsButtonClicked, page, &Kerfuffle::SettingsPage::slotDefaultsButtonClicked); } // Hide the icons list if only one page has been added. dialog->setFaceType(KPageDialog::Auto); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::writeSettings); dialog->show(); } void MainWindow::writeSettings() { Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); iface->config()->save(); } void MainWindow::addPartUrl() { m_recentFilesAction->addUrl(m_part->url()); } void MainWindow::newArchive() { qCDebug(ARK) << "Creating new archive"; Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); Q_UNUSED(iface); QPointer dialog = new Kerfuffle::CreateDialog( nullptr, // parent i18n("Create New Archive"), // caption QUrl()); // startDir if (dialog.data()->exec()) { const QUrl saveFileUrl = dialog.data()->selectedUrl(); const QString password = dialog.data()->password(); const QString fixedMimeType = dialog.data()->currentMimeType().name(); qCDebug(ARK) << "CreateDialog returned URL:" << saveFileUrl.toString(); qCDebug(ARK) << "CreateDialog returned mime:" << fixedMimeType; m_openArgs.metaData()[QStringLiteral("createNewArchive")] = QStringLiteral("true"); m_openArgs.metaData()[QStringLiteral("fixedMimeType")] = fixedMimeType; if (dialog.data()->compressionLevel() > -1) { m_openArgs.metaData()[QStringLiteral("compressionLevel")] = QString::number(dialog.data()->compressionLevel()); } if (dialog.data()->volumeSize() > 0) { qCDebug(ARK) << "Setting volume size:" << QString::number(dialog.data()->volumeSize()); m_openArgs.metaData()[QStringLiteral("volumeSize")] = QString::number(dialog.data()->volumeSize()); } if (!dialog.data()->compressionMethod().isEmpty()) { m_openArgs.metaData()[QStringLiteral("compressionMethod")] = dialog.data()->compressionMethod(); } if (!dialog.data()->encryptionMethod().isEmpty()) { m_openArgs.metaData()[QStringLiteral("encryptionMethod")] = dialog.data()->encryptionMethod(); } m_openArgs.metaData()[QStringLiteral("encryptionPassword")] = password; if (dialog.data()->isHeaderEncryptionEnabled()) { m_openArgs.metaData()[QStringLiteral("encryptHeader")] = QStringLiteral("true"); } openUrl(saveFileUrl); m_openArgs.metaData().remove(QStringLiteral("showExtractDialog")); m_openArgs.metaData().remove(QStringLiteral("createNewArchive")); m_openArgs.metaData().remove(QStringLiteral("fixedMimeType")); m_openArgs.metaData().remove(QStringLiteral("compressionLevel")); m_openArgs.metaData().remove(QStringLiteral("encryptionPassword")); m_openArgs.metaData().remove(QStringLiteral("encryptHeader")); } delete dialog.data(); } diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index a5bb8dc1..4a05b84c 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1106 +1,1106 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * 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 "cliinterface.h" #include "ark_debug.h" #include "queries.h" #ifdef Q_OS_WIN # include #else # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args) { //because this interface uses the event loop setWaitForFinishedSignal(true); if (QMetaType::type("QProcess::ExitStatus") == 0) { qRegisterMetaType("QProcess::ExitStatus"); } m_cliProps = new CliProperties(this, m_metaData, mimetype()); } CliInterface::~CliInterface() { Q_ASSERT(!m_process); } void CliInterface::setListEmptyLines(bool emptyLines) { m_listEmptyLines = emptyLines; } int CliInterface::copyRequiredSignals() const { return 2; } bool CliInterface::list() { resetParsing(); m_operationMode = List; m_numberOfEntries = 0; // To compute progress. m_archiveSizeOnDisk = static_cast(QFileInfo(filename()).size()); connect(this, &ReadOnlyArchiveInterface::entry, this, &CliInterface::onEntry); return runProcess(m_cliProps->property("listProgram").toString(), m_cliProps->listArgs(filename(), password())); } bool CliInterface::extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << Q_FUNC_INFO << "to" << destinationDirectory; m_operationMode = Extract; m_extractionOptions = options; m_extractedFiles = files; m_extractDestDir = destinationDirectory; if (!m_cliProps->property("passwordSwitch").toStringList().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } QUrl destDir = QUrl(destinationDirectory); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); const bool useTmpExtractDir = options.isDragAndDropEnabled() || options.alwaysUseTempDir(); if (useTmpExtractDir) { // Create an hidden temp folder in the current directory. m_extractTempDir.reset(new QTemporaryDir(QStringLiteral(".%1-").arg(QCoreApplication::applicationName()))); qCDebug(ARK) << "Using temporary extraction dir:" << m_extractTempDir->path(); if (!m_extractTempDir->isValid()) { qCDebug(ARK) << "Creation of temporary directory failed."; emit finished(false); return false; } m_oldWorkingDir = QDir::currentPath(); destDir = QUrl(m_extractTempDir->path()); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); } return runProcess(m_cliProps->property("extractProgram").toString(), m_cliProps->extractArgs(filename(), extractFilesList(files), options.preservePaths(), password())); } bool CliInterface::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd) { Q_UNUSED(numberOfEntriesToAdd) m_operationMode = Add; QVector filesToPass = QVector(); // If destination path is specified, we have recreate its structure inside the temp directory // and then place symlinks of targeted files there. const QString destinationPath = (destination == nullptr) ? QString() : destination->fullPath(); qCDebug(ARK) << "Adding" << files.count() << "file(s) to destination:" << destinationPath; if (!destinationPath.isEmpty()) { m_extractTempDir.reset(new QTemporaryDir()); const QString absoluteDestinationPath = m_extractTempDir->path() + QLatin1Char('/') + destinationPath; QDir qDir; qDir.mkpath(absoluteDestinationPath); QObject *preservedParent = nullptr; foreach (Archive::Entry *file, files) { // The entries may have parent. We have to save and apply it to our new entry in order to prevent memory // leaks. if (preservedParent == nullptr) { preservedParent = file->parent(); } const QString filePath = QDir::currentPath() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newFilePath = absoluteDestinationPath + file->fullPath(NoTrailingSlash); if (QFile::link(filePath, newFilePath)) { qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; } else { qCDebug(ARK) << "Can't create symlink" << filePath << newFilePath; emit finished(false); return false; } } qCDebug(ARK) << "Changing working dir again to " << m_extractTempDir->path(); QDir::setCurrent(m_extractTempDir->path()); filesToPass.push_back(new Archive::Entry(preservedParent, destinationPath.split(QLatin1Char('/'), QString::SkipEmptyParts).at(0))); } else { filesToPass = files; } if (!m_cliProps->property("passwordSwitch").toString().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } return runProcess(m_cliProps->property("addProgram").toString(), m_cliProps->addArgs(filename(), entryFullPaths(filesToPass, NoTrailingSlash), password(), isHeaderEncryptionEnabled(), options.compressionLevel(), options.compressionMethod(), options.encryptionMethod(), options.volumeSize())); } bool CliInterface::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options); m_operationMode = Move; m_removedFiles = files; QVector withoutChildren = entriesWithoutChildren(files); setNewMovedFiles(files, destination, withoutChildren.count()); return runProcess(m_cliProps->property("moveProgram").toString(), m_cliProps->moveArgs(filename(), withoutChildren, destination, password())); } bool CliInterface::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { m_oldWorkingDir = QDir::currentPath(); m_tempWorkingDir.reset(new QTemporaryDir()); m_tempAddDir.reset(new QTemporaryDir()); QDir::setCurrent(m_tempWorkingDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_numberOfEntries = 0; m_subOperation = Extract; connect(this, &CliInterface::finished, this, &CliInterface::continueCopying); return extractFiles(files, QDir::currentPath(), ExtractionOptions()); } bool CliInterface::deleteFiles(const QVector &files) { m_operationMode = Delete; m_removedFiles = files; return runProcess(m_cliProps->property("deleteProgram").toString(), m_cliProps->deleteArgs(filename(), files, password())); } bool CliInterface::testArchive() { resetParsing(); m_operationMode = Test; return runProcess(m_cliProps->property("testProgram").toString(), m_cliProps->testArgs(filename(), password())); } bool CliInterface::runProcess(const QString& programName, const QStringList& arguments) { Q_ASSERT(!m_process); QString programPath = QStandardPaths::findExecutable(programName); if (programPath.isEmpty()) { emit error(xi18nc("@info", "Failed to locate program %1 on disk.", programName)); emit finished(false); return false; } qCDebug(ARK) << "Executing" << programPath << arguments << "within directory" << QDir::currentPath(); #ifdef Q_OS_WIN m_process = new KProcess; #else m_process = new KPtyProcess; m_process->setPtyChannels(KPtyProcess::StdinChannel); #endif m_process->setOutputChannelMode(KProcess::MergedChannels); m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text); m_process->setProgram(programPath, arguments); connect(m_process, &QProcess::readyReadStandardOutput, this, [=]() { readStdout(); }); if (m_operationMode == Extract) { // Extraction jobs need a dedicated post-processing function. - connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::extractProcessFinished); + connect(m_process, QOverload::of(&QProcess::finished), this, &CliInterface::extractProcessFinished); } else { - connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::processFinished); + connect(m_process, QOverload::of(&QProcess::finished), this, &CliInterface::processFinished); } m_stdOutData.clear(); m_process->start(); return true; } void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { m_exitCode = exitCode; qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { //handle all the remaining data in the process readStdout(true); delete m_process; m_process = nullptr; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_operationMode == Delete || m_operationMode == Move) { QStringList removedFullPaths = entryFullPaths(m_removedFiles); foreach (const QString &fullPath, removedFullPaths) { emit entryRemoved(fullPath); } foreach (Archive::Entry *e, m_newMovedFiles) { emit entry(e); } m_newMovedFiles.clear(); } if (m_operationMode == Add && !isMultiVolume()) { list(); } else if (m_operationMode == List && isCorrupt()) { Kerfuffle::LoadCorruptQuery query(filename()); query.execute(); if (!query.responseYes()) { emit cancelled(); emit finished(false); } else { emit progress(1.0); emit finished(true); } } else { emit progress(1.0); emit finished(true); } } void CliInterface::extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_ASSERT(m_operationMode == Extract); m_exitCode = exitCode; qCDebug(ARK) << "Extraction process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { // Handle all the remaining data in the process. readStdout(true); delete m_process; m_process = nullptr; } // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_extractionOptions.alwaysUseTempDir()) { // unar exits with code 1 if extraction fails. // This happens at least with wrong passwords or not enough space in the destination folder. if (m_exitCode == 1) { if (password().isEmpty()) { qCWarning(ARK) << "Extraction aborted, destination folder might not have enough space."; emit error(i18n("Extraction failed. Make sure that enough space is available.")); } else { qCWarning(ARK) << "Extraction aborted, either the password is wrong or the destination folder doesn't have enough space."; emit error(i18n("Extraction failed. Make sure you provided the correct password and that enough space is available.")); setPassword(QString()); } cleanUpExtracting(); emit finished(false); return; } if (!m_extractionOptions.isDragAndDropEnabled()) { if (!moveToDestination(QDir::current(), QDir(m_extractDestDir), m_extractionOptions.preservePaths())) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } } if (m_extractionOptions.isDragAndDropEnabled()) { if (!moveDroppedFilesToDest(m_extractedFiles, m_extractDestDir)) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } emit progress(1.0); emit finished(true); } void CliInterface::continueCopying(bool result) { if (!result) { finishCopying(false); return; } switch (m_subOperation) { case Extract: m_subOperation = Add; m_passedFiles = entriesWithoutChildren(m_passedFiles); if (!setAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { finishCopying(false); } break; case Add: finishCopying(true); break; default: Q_ASSERT(false); } } bool CliInterface::moveDroppedFilesToDest(const QVector &files, const QString &finalDest) { // Move extracted files from a QTemporaryDir to the final destination. QDir finalDestDir(finalDest); qCDebug(ARK) << "Setting final dir to" << finalDest; bool overwriteAll = false; bool skipAll = false; foreach (const Archive::Entry *file, files) { QFileInfo relEntry(file->fullPath().remove(file->rootNode)); QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file->fullPath()); QFileInfo absDestEntry(finalDestDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absSourceEntry.isDir()) { // For directories, just create the path. if (!finalDestDir.mkpath(relEntry.filePath())) { qCWarning(ARK) << "Failed to create directory" << relEntry.filePath() << "in final destination."; } } else { // If destination file exists, prompt the user. if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; if (!skipAll && !overwriteAll) { Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); query.execute(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } } // Create any parent directories. if (!finalDestDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } // Move files to the final destination. if (!QFile(absSourceEntry.absoluteFilePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << absSourceEntry.filePath() << "to final destination."; return false; } } } return true; } bool CliInterface::isEmptyDir(const QDir &dir) { QDir d = dir; d.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); return d.count() == 0; } void CliInterface::cleanUpExtracting() { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } m_extractTempDir.reset(); } void CliInterface::finishCopying(bool result) { disconnect(this, &CliInterface::finished, this, &CliInterface::continueCopying); emit progress(1.0); emit finished(result); cleanUp(); } bool CliInterface::moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths) { qCDebug(ARK) << "Moving extracted files from temp dir" << tempDir.path() << "to final destination" << destDir.path(); bool overwriteAll = false; bool skipAll = false; QDirIterator dirIt(tempDir.path(), QDir::AllEntries | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { dirIt.next(); // We skip directories if: // 1. We are not preserving paths // 2. The dir is not empty. Only empty directories need to be explicitly moved. // The non-empty ones are created by QDir::mkpath() below. if (dirIt.fileInfo().isDir()) { if (!preservePaths || !isEmptyDir(QDir(dirIt.filePath()))) { continue; } } QFileInfo relEntry; if (preservePaths) { relEntry = QFileInfo(dirIt.filePath().remove(tempDir.path() + QLatin1Char('/'))); } else { relEntry = QFileInfo(dirIt.fileName()); } QFileInfo absDestEntry(destDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); query.execute(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } if (preservePaths) { // Create any parent directories. if (!destDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } } // Move file to the final destination. if (!QFile(dirIt.filePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << dirIt.filePath() << "to final destination."; return false; } } return true; } void CliInterface::setNewMovedFiles(const QVector &entries, const Archive::Entry *destination, int entriesWithoutChildren) { m_newMovedFiles.clear(); QMap entryMap; foreach (const Archive::Entry* entry, entries) { entryMap.insert(entry->fullPath(), entry); } QString lastFolder; QString newPath; int nameLength = 0; foreach (const Archive::Entry* entry, entryMap) { if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) { // Replace last moved or copied folder path with destination path. int charsCount = entry->fullPath().count() - lastFolder.count(); if (entriesWithoutChildren > 1) { charsCount += nameLength; } newPath = destination->fullPath() + entry->fullPath().right(charsCount); } else { if (entriesWithoutChildren > 1) { newPath = destination->fullPath() + entry->name(); } else { // If there is only one passed file in the list, // we have to use destination as newPath. newPath = destination->fullPath(NoTrailingSlash); } if (entry->isDir()) { newPath += QLatin1Char('/'); nameLength = entry->name().count() + 1; // plus slash lastFolder = entry->fullPath(); } else { nameLength = 0; lastFolder = QString(); } } Archive::Entry *newEntry = new Archive::Entry(nullptr); newEntry->copyMetaData(entry); newEntry->setFullPath(newPath); m_newMovedFiles << newEntry; } } QStringList CliInterface::extractFilesList(const QVector &entries) const { QStringList filesList; foreach (const Archive::Entry *e, entries) { filesList << escapeFileName(e->fullPath(NoTrailingSlash)); } return filesList; } void CliInterface::killProcess(bool emitFinished) { // TODO: Would be good to unit test #304764/#304178. if (!m_process) { return; } m_abortingOperation = !emitFinished; // Give some time for the application to finish gracefully if (!m_process->waitForFinished(5)) { m_process->kill(); // It takes a few hundred ms for the process to be killed. m_process->waitForFinished(1000); } m_abortingOperation = false; } bool CliInterface::passwordQuery() { Kerfuffle::PasswordNeededQuery query(filename()); query.execute(); if (query.responseCancelled()) { emit cancelled(); // There is no process running, so finished() must be emitted manually. emit finished(false); return false; } setPassword(query.password()); return true; } void CliInterface::cleanUp() { qDeleteAll(m_tempAddedFiles); m_tempAddedFiles.clear(); QDir::setCurrent(m_oldWorkingDir); m_tempWorkingDir.reset(); m_tempAddDir.reset(); } void CliInterface::readStdout(bool handleAll) { //when hacking this function, please remember the following: //- standard output comes in unpredictable chunks, this is why //you can never know if the last part of the output is a complete line or not //- console applications are not really consistent about what //characters they send out (newline, backspace, carriage return, //etc), so keep in mind that this function is supposed to handle //all those special cases and be the lowest common denominator if (m_abortingOperation) return; Q_ASSERT(m_process); if (!m_process->bytesAvailable()) { //if process has no more data, we can just bail out return; } QByteArray dd = m_process->readAllStandardOutput(); m_stdOutData += dd; QList lines = m_stdOutData.split('\n'); //The reason for this check is that archivers often do not end //queries (such as file exists, wrong password) on a new line, but //freeze waiting for input. So we check for errors on the last line in //all cases. // TODO: QLatin1String() might not be the best choice here. // The call to handleLine() at the end of the method uses // QString::fromLocal8Bit(), for example. // TODO: The same check methods are called in handleLine(), this // is suboptimal. bool wrongPasswordMessage = m_cliProps->isWrongPasswordMsg(QLatin1String(lines.last())); bool foundErrorMessage = (wrongPasswordMessage || m_cliProps->isDiskFullMsg(QLatin1String(lines.last())) || m_cliProps->isfileExistsMsg(QLatin1String(lines.last()))) || m_cliProps->isPasswordPrompt(QLatin1String(lines.last())); if (foundErrorMessage) { handleAll = true; } if (wrongPasswordMessage) { setPassword(QString()); } //this is complex, here's an explanation: //if there is no newline, then there is no guaranteed full line to //handle in the output. The exception is that it is supposed to handle //all the data, OR if there's been an error message found in the //partial data. if (lines.size() == 1 && !handleAll) { return; } if (handleAll) { m_stdOutData.clear(); } else { //because the last line might be incomplete we leave it for now //note, this last line may be an empty string if the stdoutdata ends //with a newline m_stdOutData = lines.takeLast(); } foreach(const QByteArray& line, lines) { if (!line.isEmpty() || (m_listEmptyLines && m_operationMode == List)) { if (!handleLine(QString::fromLocal8Bit(line))) { killProcess(); return; } } } } bool CliInterface::setAddedFiles() { QDir::setCurrent(m_tempAddDir->path()); foreach (const Archive::Entry *file, m_passedFiles) { const QString oldPath = m_tempWorkingDir->path() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + file->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(nullptr, file->name()); } return true; } bool CliInterface::handleLine(const QString& line) { // TODO: This should be implemented by each plugin; the way progress is // shown by each CLI application is subject to a lot of variation. if ((m_operationMode == Extract || m_operationMode == Add) && m_cliProps->property("captureProgress").toBool()) { //read the percentage int pos = line.indexOf(QLatin1Char( '%' )); if (pos > 1) { int percentage = line.midRef(pos - 2, 2).toInt(); emit progress(float(percentage) / 100); return true; } } if (m_operationMode == Extract) { if (m_cliProps->isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); query.execute(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } if (m_cliProps->isDiskFullMsg(line)) { qCWarning(ARK) << "Found disk full message:" << line; emit error(i18nc("@info", "Extraction failed because the disk is full.")); return false; } if (m_cliProps->isWrongPasswordMsg(line)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18nc("@info", "Extraction failed: Incorrect password")); return false; } if (handleFileExistsMessage(line)) { return true; } return readExtractLine(line); } if (m_operationMode == List) { if (m_cliProps->isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); query.execute(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } if (m_cliProps->isWrongPasswordMsg(line)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18n("Incorrect password.")); return false; } if (m_cliProps->isCorruptArchiveMsg(line)) { qCWarning(ARK) << "Archive corrupt"; setCorrupt(true); // Special case: corrupt is not a "fatal" error so we return true here. return true; } if (handleFileExistsMessage(line)) { return true; } return readListLine(line); } if (m_operationMode == Delete) { return readDeleteLine(line); } if (m_operationMode == Test) { if (m_cliProps->isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; emit error(i18n("Ark does not currently support testing this archive.")); return false; } if (m_cliProps->isTestPassedMsg(line)) { qCDebug(ARK) << "Test successful"; emit testSuccess(); return true; } } return true; } bool CliInterface::readDeleteLine(const QString &line) { Q_UNUSED(line); return true; } bool CliInterface::handleFileExistsMessage(const QString& line) { // Check for a filename and store it. foreach (const QString &pattern, m_cliProps->property("fileExistsFileName").toStringList()) { const QRegularExpression rxFileNamePattern(pattern); const QRegularExpressionMatch rxMatch = rxFileNamePattern.match(line); if (rxMatch.hasMatch()) { m_storedFileName = rxMatch.captured(1); qCWarning(ARK) << "Detected existing file:" << m_storedFileName; } } if (!m_cliProps->isfileExistsMsg(line)) { return false; } Kerfuffle::OverwriteQuery query(QDir::current().path() + QLatin1Char( '/' ) + m_storedFileName); query.setNoRenameMode(true); query.execute(); QString responseToProcess; const QStringList choices = m_cliProps->property("fileExistsInput").toStringList(); if (query.responseOverwrite()) { responseToProcess = choices.at(0); } else if (query.responseSkip()) { responseToProcess = choices.at(1); } else if (query.responseOverwriteAll()) { responseToProcess = choices.at(2); } else if (query.responseAutoSkip()) { responseToProcess = choices.at(3); } else if (query.responseCancelled()) { if (choices.count() < 5) { // If the program has no way to cancel the extraction, we resort to killing it return doKill(); } responseToProcess = choices.at(4); } Q_ASSERT(!responseToProcess.isEmpty()); responseToProcess += QLatin1Char( '\n' ); writeToProcess(responseToProcess.toLocal8Bit()); return true; } bool CliInterface::doKill() { if (m_process) { killProcess(false); return true; } return false; } bool CliInterface::doSuspend() { return false; } bool CliInterface::doResume() { return false; } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } QStringList CliInterface::entryPathDestinationPairs(const QVector &entriesWithoutChildren, const Archive::Entry *destination) { QStringList pairList; if (entriesWithoutChildren.count() > 1) { foreach (const Archive::Entry *file, entriesWithoutChildren) { pairList << file->fullPath(NoTrailingSlash) << destination->fullPath() + file->name(); } } else { pairList << entriesWithoutChildren.at(0)->fullPath(NoTrailingSlash) << destination->fullPath(NoTrailingSlash); } return pairList; } void CliInterface::writeToProcess(const QByteArray& data) { Q_ASSERT(m_process); Q_ASSERT(!data.isNull()); qCDebug(ARK) << "Writing" << data << "to the process"; #ifdef Q_OS_WIN m_process->write(data); #else m_process->pty()->write(data); #endif } bool CliInterface::addComment(const QString &comment) { m_operationMode = Comment; m_commentTempFile.reset(new QTemporaryFile()); if (!m_commentTempFile->open()) { qCWarning(ARK) << "Failed to create temporary file for comment"; emit finished(false); return false; } QTextStream stream(m_commentTempFile.data()); stream << comment << endl; m_commentTempFile->close(); if (!runProcess(m_cliProps->property("addProgram").toString(), m_cliProps->commentArgs(filename(), m_commentTempFile->fileName()))) { return false; } m_comment = comment; return true; } QString CliInterface::multiVolumeName() const { QString oldSuffix = QMimeDatabase().suffixForFileName(filename()); QString name; foreach (const QString &multiSuffix, m_cliProps->property("multiVolumeSuffix").toStringList()) { QString newSuffix = multiSuffix; newSuffix.replace(QStringLiteral("$Suffix"), oldSuffix); name = filename().remove(oldSuffix).append(newSuffix); if (QFileInfo::exists(name)) { break; } } return name; } CliProperties *CliInterface::cliProperties() const { return m_cliProps; } void CliInterface::onEntry(Archive::Entry *archiveEntry) { if (archiveEntry->compressedSizeIsSet) { m_listedSize += archiveEntry->property("compressedSize").toULongLong(); if (m_listedSize <= m_archiveSizeOnDisk) { emit progress(float(m_listedSize)/float(m_archiveSizeOnDisk)); } else { // In case summed compressed size exceeds archive size on disk. emit progress(1); } } } } diff --git a/kerfuffle/createdialog.cpp b/kerfuffle/createdialog.cpp index 6a3c3c80..09bc5619 100644 --- a/kerfuffle/createdialog.cpp +++ b/kerfuffle/createdialog.cpp @@ -1,249 +1,249 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * Copyright (C) 2009,2011 Raphael Kubo da Costa * Copyright (C) 2015 Elvis Angelaccio * 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 "createdialog.h" #include "archiveformat.h" #include "ark_debug.h" #include "ui_createdialog.h" #include "mimetypes.h" #include #include #include #include #include #include namespace Kerfuffle { class CreateDialogUI: public QWidget, public Ui::CreateDialog { Q_OBJECT public: CreateDialogUI(QWidget *parent = nullptr) : QWidget(parent) { setupUi(this); } }; CreateDialog::CreateDialog(QWidget *parent, const QString &caption, const QUrl &startDir) : QDialog(parent, Qt::Dialog) { qCDebug(ARK) << "CreateDialog loaded"; setWindowTitle(caption); setModal(true); m_supportedMimeTypes = m_pluginManger.supportedWriteMimeTypes(PluginManager::SortByComment); m_vlayout = new QVBoxLayout(); setLayout(m_vlayout); m_ui = new CreateDialogUI(this); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); m_ui->destFolderUrlRequester->setMode(KFile::Directory); if (startDir.isEmpty()) { m_ui->destFolderUrlRequester->setUrl(QUrl::fromLocalFile(QDir::currentPath() + QLatin1Char('/'))); } else { m_ui->destFolderUrlRequester->setUrl(startDir); } // Populate combobox with mimetypes. foreach (const QString &type, m_supportedMimeTypes) { m_ui->mimeComboBox->addItem(QMimeDatabase().mimeTypeForName(type).comment()); } connect(m_ui->filenameLineEdit, &QLineEdit::textChanged, this, &CreateDialog::slotFileNameEdited); connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(this, &QDialog::accepted, this, &CreateDialog::slotUpdateDefaultMimeType); - connect(m_ui->mimeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &CreateDialog::slotUpdateWidgets); - connect(m_ui->mimeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &CreateDialog::slotUpdateFilenameExtension); + connect(m_ui->mimeComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &CreateDialog::slotUpdateWidgets); + connect(m_ui->mimeComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &CreateDialog::slotUpdateFilenameExtension); m_vlayout->addWidget(m_ui); m_ui->optionsWidget->setMimeType(currentMimeType()); loadConfiguration(); layout()->setSizeConstraint(QLayout::SetFixedSize); m_ui->filenameLineEdit->setFocus(); slotUpdateFilenameExtension(m_ui->mimeComboBox->currentIndex()); } void CreateDialog::slotFileNameEdited(const QString &fileName) { const QMimeType mimeFromFileName = QMimeDatabase().mimeTypeForFile(fileName, QMimeDatabase::MatchExtension); if (m_supportedMimeTypes.contains(mimeFromFileName.name())) { setMimeType(mimeFromFileName.name()); } m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!fileName.isEmpty()); } void CreateDialog::slotUpdateWidgets(int index) { m_ui->optionsWidget->setMimeType(QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(index))); } void CreateDialog::slotUpdateFilenameExtension(int index) { m_ui->chkAddExtension->setText(i18nc("the argument is a file extension (the period is not a typo)", "Automatically add .%1", QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(index)).preferredSuffix())); } QUrl CreateDialog::selectedUrl() const { QString fileName = m_ui->filenameLineEdit->text(); QString dir = m_ui->destFolderUrlRequester->url().toLocalFile(); if (m_ui->chkAddExtension->isChecked()) { QString detectedSuffix = QMimeDatabase().suffixForFileName(m_ui->filenameLineEdit->text().trimmed()); if (!currentMimeType().suffixes().contains(detectedSuffix)) { if (!fileName.endsWith(QLatin1Char('.'))) { fileName.append(QLatin1Char('.')); } fileName.append(currentMimeType().preferredSuffix()); } } if (!dir.endsWith(QLatin1Char('/'))) { dir.append(QLatin1Char('/')); } return QUrl::fromLocalFile(dir + fileName); } int CreateDialog::compressionLevel() const { return m_ui->optionsWidget->compressionLevel(); } QString CreateDialog::compressionMethod() const { return m_ui->optionsWidget->compressionMethod(); } QString CreateDialog::encryptionMethod() const { return m_ui->optionsWidget->encryptionMethod(); } ulong CreateDialog::volumeSize() const { return m_ui->optionsWidget->volumeSize(); } QString CreateDialog::password() const { return m_ui->optionsWidget->password(); } bool CreateDialog::isEncryptionAvailable() const { return m_ui->optionsWidget->isEncryptionAvailable(); } bool CreateDialog::isEncryptionEnabled() const { return m_ui->optionsWidget->isEncryptionEnabled(); } bool CreateDialog::isHeaderEncryptionAvailable() const { return m_ui->optionsWidget->isHeaderEncryptionAvailable(); } bool CreateDialog::isHeaderEncryptionEnabled() const { return m_ui->optionsWidget->isHeaderEncryptionEnabled(); } void CreateDialog::accept() { if (!isEncryptionEnabled()) { QDialog::accept(); return; } switch (m_ui->optionsWidget->passwordStatus()) { case KNewPasswordWidget::WeakPassword: case KNewPasswordWidget::StrongPassword: QDialog::accept(); break; case KNewPasswordWidget::PasswordNotVerified: KMessageBox::error(nullptr, i18n("The chosen password does not match the given verification password.")); break; default: break; } } void CreateDialog::slotUpdateDefaultMimeType() { m_config.writeEntry("LastMimeType", currentMimeType().name()); } void CreateDialog::loadConfiguration() { m_config = KConfigGroup(KSharedConfig::openConfig()->group("CreateDialog")); QMimeType lastUsedMime = QMimeDatabase().mimeTypeForName(m_config.readEntry("LastMimeType", QStringLiteral("application/x-compressed-tar"))); setMimeType(lastUsedMime.name()); } QMimeType CreateDialog::currentMimeType() const { Q_ASSERT(m_supportedMimeTypes.size() > m_ui->mimeComboBox->currentIndex()); return QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(m_ui->mimeComboBox->currentIndex())); } bool CreateDialog::setMimeType(const QString &mimeTypeName) { int index = m_supportedMimeTypes.indexOf(mimeTypeName); if (index == -1) { return false; } m_ui->mimeComboBox->setCurrentIndex(index); // This is needed to make sure widgets get updated in case the mimetype is already selected. slotUpdateWidgets(index); return true; } } #include "createdialog.moc" diff --git a/part/part.cpp b/part/part.cpp index 93c21c28..6a7aee0d 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1770 +1,1770 @@ /* * 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 #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, static_cast(&Part::slotAddFiles)); + this, QOverload::of(&Part::slotAddFiles)); 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, static_cast(&KParts::ReadOnlyPart::completed), + connect(this, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &Part::setFileNameFromArchive); 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()) { 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(); } 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() { // We use a QSignalMapper for the preview, open and openwith actions. This // way we can connect all three actions to the same slot slotOpenEntry and // pass the OpenFileMode as argument to the slot. m_signalMapper = new QSignalMapper(this); 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 = actionCollection()->addAction(KStandardAction::SaveAs, QStringLiteral("ark_file_save_as"), this, SLOT(slotSaveAs())); 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, m_signalMapper, static_cast(&QSignalMapper::map)); + connect(m_openFileAction, &QAction::triggered, m_signalMapper, QOverload<>::of(&QSignalMapper::map)); m_signalMapper->setMapping(m_openFileAction, 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, m_signalMapper, static_cast(&QSignalMapper::map)); + connect(m_openFileWithAction, &QAction::triggered, m_signalMapper, QOverload<>::of(&QSignalMapper::map)); m_signalMapper->setMapping(m_openFileWithAction, 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, m_signalMapper, static_cast(&QSignalMapper::map)); + connect(m_previewAction, &QAction::triggered, m_signalMapper, QOverload<>::of(&QSignalMapper::map)); m_signalMapper->setMapping(m_previewAction, 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, static_cast(&Part::slotAddFiles)); + connect(m_addFilesAction, &QAction::triggered, this, QOverload<>::of(&Part::slotAddFiles)); m_renameFileAction = actionCollection()->addAction(QStringLiteral("rename")); m_renameFileAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); m_renameFileAction->setText(i18n("&Rename")); actionCollection()->setDefaultShortcut(m_renameFileAction, Qt::Key_F2); m_renameFileAction->setToolTip(i18nc("@info:tooltip", "Click to rename the selected file")); connect(m_renameFileAction, &QAction::triggered, m_view, &ArchiveView::renameSelectedEntry); m_deleteFilesAction = actionCollection()->addAction(QStringLiteral("delete")); m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove"))); m_deleteFilesAction->setText(i18n("De&lete")); actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete); m_deleteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to delete the selected files")); connect(m_deleteFilesAction, &QAction::triggered, this, &Part::slotDeleteFiles); m_cutFilesAction = actionCollection()->addAction(QStringLiteral("cut")); m_cutFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-cut"))); m_cutFilesAction->setText(i18nc("@action:inmenu", "C&ut")); actionCollection()->setDefaultShortcut(m_cutFilesAction, Qt::CTRL + Qt::Key_X); m_cutFilesAction->setToolTip(i18nc("@info:tooltip", "Click to cut the selected files")); connect(m_cutFilesAction, &QAction::triggered, this, &Part::slotCutFiles); m_copyFilesAction = actionCollection()->addAction(QStringLiteral("copy")); m_copyFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); m_copyFilesAction->setText(i18nc("@action:inmenu", "C&opy")); actionCollection()->setDefaultShortcut(m_copyFilesAction, Qt::CTRL + Qt::Key_C); m_copyFilesAction->setToolTip(i18nc("@info:tooltip", "Click to copy the selected files")); connect(m_copyFilesAction, &QAction::triggered, this, &Part::slotCopyFiles); m_pasteFilesAction = actionCollection()->addAction(QStringLiteral("paste")); m_pasteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste"))); m_pasteFilesAction->setText(i18nc("@action:inmenu", "Pa&ste")); actionCollection()->setDefaultShortcut(m_pasteFilesAction, Qt::CTRL + Qt::Key_V); m_pasteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to paste the files here")); - connect(m_pasteFilesAction, &QAction::triggered, this, static_cast(&Part::slotPasteFiles)); + connect(m_pasteFilesAction, &QAction::triggered, this, QOverload<>::of(&Part::slotPasteFiles)); 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 = actionCollection()->addAction(QStringLiteral("find_in_archive")); m_searchAction->setIcon(QIcon::fromTheme(QStringLiteral("search"))); m_searchAction->setText(i18nc("@action:inmenu", "&Find Files")); actionCollection()->setDefaultShortcut(m_searchAction, Qt::CTRL + Qt::Key_F); m_searchAction->setToolTip(i18nc("@info:tooltip", "Click to search in archive")); connect(m_searchAction, &QAction::triggered, this, &Part::slotShowFind); - connect(m_signalMapper, static_cast(&QSignalMapper::mapped), + connect(m_signalMapper, QOverload::of(&QSignalMapper::mapped), this, &Part::slotOpenEntry); 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(); } void Part::createArchive() { const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; m_model->createEmptyArchive(localFilePath(), fixedMimeType, m_model); if (arguments().metaData().contains(QStringLiteral("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")); } updateActions(); m_view->setDropsEnabled(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::resetGui() { m_messageWidget->hide(); m_commentView->clear(); m_commentBox->hide(); m_infoPanel->setIndex(QModelIndex()); // 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; foreach (const QModelIndex &i, m_view->selectionModel()->selectedRows()) { list.append(m_filterModel->mapToSource(i)); } return list; } bool Part::openFile() { qCDebug(ARK) << "Attempting to open archive" << localFilePath(); resetGui(); if (!isLocalFileValid()) { return false; } const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); if (creatingNewArchive) { 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 Settings"), QStringLiteral("go-home"))); pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction Settings"), QStringLiteral("archive-extract"))); pages.append(new PluginSettingsPage(parent, i18nc("@title:tab", "Plugin Settings"), QStringLiteral("plugins"))); pages.append(new PreviewSettingsPage(parent, i18nc("@title:tab", "Preview Settings"), QStringLiteral("document-preview-archive"))); return pages; } bool Part::isLocalFileValid() { const QString localFile = localFilePath(); const QFileInfo localFileInfo(localFile); const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); if (localFileInfo.isDir()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "%1 is a directory.", localFile)); return false; } if (creatingNewArchive) { 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::slotLoadingStarted() { m_model->filesToMove.clear(); m_model->filesToCopy.clear(); } void Part::slotLoadingFinished(KJob *job) { if (job->error()) { if (arguments().metaData()[QStringLiteral("createNewArchive")] != QLatin1String("true")) { if (job->error() != KJob::KilledJobError) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive %1 failed with the following error:%2", localFilePath(), job->errorString())); } // The file failed to open, so reset the open archive, info panel and caption. m_model->reset(); m_infoPanel->setPrettyFileName(QString()); m_infoPanel->updateWithDefaults(); emit setWindowCaption(QString()); #if KPARTS_VERSION >= QT_VERSION_CHECK(5, 37, 0) // See https://phabricator.kde.org/D6856 closeUrl(); #else setUrl(QUrl()); #endif } } else { emit completed(); } m_view->sortByColumn(0, Qt::AscendingOrder); m_view->expandIfSingleFolder(); // After loading all files, resize the columns to fit all fields m_view->header()->resizeSections(QHeaderView::ResizeToContents); // Now we can start accepting drops in the archive view (if loading was successful and the archive can be modified). m_view->setDropsEnabled(!job->error() && isArchiveWritable()); updateActions(); if (!m_model->archive()) { return; } 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); } } 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; foreach (QTemporaryDir *tmpDir, m_tmpExtractDirList) { relPath.remove(tmpDir->path()); //Remove tmpDir. } relPath = relPath.mid(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); 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; foreach(const QModelIndex& index, list) { ret << m_model->entryForIndex(index); } return ret; } QVector Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { QVector fileList; QStringList fullPathsList; foreach (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; foreach (Archive::Entry *entry, filesForIndexes(alist)) { 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; foreach (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); 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() : QStringLiteral("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::slotAddFiles() { // Store options from CreateDialog if they are set. if (!m_compressionOptions.isCompressionLevelSet() && arguments().metaData().contains(QStringLiteral("compressionLevel"))) { m_compressionOptions.setCompressionLevel(arguments().metaData()[QStringLiteral("compressionLevel")].toInt()); } if (m_compressionOptions.compressionMethod().isEmpty() && arguments().metaData().contains(QStringLiteral("compressionMethod"))) { m_compressionOptions.setCompressionMethod(arguments().metaData()[QStringLiteral("compressionMethod")]); } if (m_compressionOptions.encryptionMethod().isEmpty() && arguments().metaData().contains(QStringLiteral("encryptionMethod"))) { m_compressionOptions.setEncryptionMethod(arguments().metaData()[QStringLiteral("encryptionMethod")]); } if (!m_compressionOptions.isVolumeSizeSet() && arguments().metaData().contains(QStringLiteral("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()); } 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(); foreach (const QModelIndex &row, m_cutIndexes) { m_view->dataChanged(row, row); } m_cutIndexes = selectedRows; foreach (const QModelIndex &row, 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(); foreach (const QModelIndex &row, m_cutIndexes) { m_view->dataChanged(row, row); } m_cutIndexes.clear(); m_model->filesToMove.clear(); updateActions(); } void Part::slotRenameFile(const QString &name) { if (name == QLatin1String(".") || 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()); auto 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); } foreach (const Archive::Entry *entry, 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); 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(); if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { // Hide the "archive will be created as soon as you add a file" message. m_messageWidget->hide(); // 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(0, 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 #include "part.moc"