diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index 7909a9dc..f473cd15 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,638 +1,637 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * 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 "jobs.h" #include "archiveentry.h" #include "ark_debug.h" #include #include #include #include #include #include //#define DEBUG_RACECONDITION namespace Kerfuffle { class Job::Private : public QThread { public: Private(Job *job, QObject *parent = 0) : QThread(parent) , q(job) { connect(q, &KJob::result, this, &QThread::quit); } virtual void run() Q_DECL_OVERRIDE; private: Job *q; }; void Job::Private::run() { q->doWork(); if (q->isRunning()) { exec(); } #ifdef DEBUG_RACECONDITION QThread::sleep(2); #endif } Job::Job(ReadOnlyArchiveInterface *interface) : KJob() , m_archiveInterface(interface) , m_isRunning(false) , d(new Private(this)) { static bool onlyOnce = false; if (!onlyOnce) { qRegisterMetaType >("QPair"); onlyOnce = true; } setCapabilities(KJob::Killable); } Job::~Job() { qDeleteAll(m_archiveEntries); m_archiveEntries.clear(); if (d->isRunning()) { d->wait(); } delete d; } ReadOnlyArchiveInterface *Job::archiveInterface() { return m_archiveInterface; } bool Job::isRunning() const { return m_isRunning; } void Job::start() { jobTimer.start(); m_isRunning = true; if (archiveInterface()->waitForFinishedSignal()) { // CLI-based interfaces run a QProcess, no need to use threads. QTimer::singleShot(0, this, &Job::doWork); } else { // Run the job in another thread. d->start(); } } void Job::emitResult() { m_isRunning = false; KJob::emitResult(); } void Job::connectToArchiveInterfaceSignals() { connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled); connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError); connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry); connect(archiveInterface(), &ReadOnlyArchiveInterface::entryRemoved, this, &Job::onEntryRemoved); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo); connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished, Qt::DirectConnection); connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery); } void Job::onCancelled() { qCDebug(ARK) << "Cancelled emitted"; setError(KJob::KilledJobError); } void Job::onError(const QString & message, const QString & details) { Q_UNUSED(details) qCDebug(ARK) << "Error emitted:" << message; setError(KJob::UserDefinedError); setErrorText(message); } void Job::onEntry(Archive::Entry *entry) { emit newEntry(entry); } void Job::onProgress(double value) { setPercent(static_cast(100.0*value)); } void Job::onInfo(const QString& info) { emit infoMessage(this, info); } void Job::onEntryRemoved(const QString & path) { emit entryRemoved(path); } void Job::onFinished(bool result) { qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms"; emitResult(); } void Job::onUserQuery(Query *query) { emit userQuery(query); } bool Job::doKill() { bool ret = archiveInterface()->doKill(); if (!ret) { qCWarning(ARK) << "Killing does not seem to be supported here."; } return ret; } ListJob::ListJob(ReadOnlyArchiveInterface *interface) : Job(interface) , m_isSingleFolderArchive(true) , m_isPasswordProtected(false) , m_extractedFilesSize(0) , m_dirCount(0) , m_filesCount(0) { qCDebug(ARK) << "ListJob started"; connect(this, &ListJob::newEntry, this, &ListJob::onNewEntry); } void ListJob::doWork() { emit description(this, i18n("Loading archive...")); connectToArchiveInterfaceSignals(); bool ret = archiveInterface()->list(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } qlonglong ListJob::extractedFilesSize() const { return m_extractedFilesSize; } bool ListJob::isPasswordProtected() const { return m_isPasswordProtected; } bool ListJob::isSingleFolderArchive() const { if (m_filesCount == 1 && m_dirCount == 0) { return false; } return m_isSingleFolderArchive; } void ListJob::onNewEntry(const Archive::Entry *entry) { m_extractedFilesSize += entry->property("size").toLongLong(); m_isPasswordProtected |= entry->property("isPasswordProtected").toBool(); if (entry->isDir()) { m_dirCount++; } else { m_filesCount++; } if (m_isSingleFolderArchive) { // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it. const QString fullPath = entry->fullPath().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); const QString basePath = fullPath.split(QLatin1Char('/')).at(0); if (m_basePath.isEmpty()) { m_basePath = basePath; m_subfolderName = basePath; } else { if (m_basePath != basePath) { m_isSingleFolderArchive = false; m_subfolderName.clear(); } } } } QString ListJob::subfolderName() const { if (!isSingleFolderArchive()) { return QString(); } return m_subfolderName; } ExtractJob::ExtractJob(const QList &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destinationDir(destinationDir) , m_options(options) { qCDebug(ARK) << "ExtractJob created"; setDefaultOptions(); } void ExtractJob::doWork() { QString desc; if (m_entries.count() == 0) { desc = i18n("Extracting all files"); } else { desc = i18np("Extracting one file", "Extracting %1 files", m_entries.count()); } emit description(this, desc); QFileInfo destDirInfo(m_destinationDir); if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) { onError(xi18n("Could not write to destination %1.Check whether you have sufficient permissions.", m_destinationDir), QString()); onFinished(false); return; } connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Starting extraction with selected files:" << m_entries << "Destination dir:" << m_destinationDir << "Options:" << m_options; bool ret = archiveInterface()->extractFiles(m_entries, m_destinationDir, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void ExtractJob::setDefaultOptions() { ExtractionOptions defaultOptions; defaultOptions[QStringLiteral("PreservePaths")] = false; ExtractionOptions::const_iterator it = defaultOptions.constBegin(); for (; it != defaultOptions.constEnd(); ++it) { if (!m_options.contains(it.key())) { m_options[it.key()] = it.value(); } } } QString ExtractJob::destinationDirectory() const { return m_destinationDir; } ExtractionOptions ExtractJob::extractionOptions() const { return m_options; } TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entry(entry) , m_passwordProtectedHint(passwordProtectedHint) { } QString TempExtractJob::validatedFilePath() const { QString path = extractionDir() + QLatin1Char('/') + m_entry->fullPath(); // Make sure a maliciously crafted archive with parent folders named ".." do // not cause the previewed file path to be located outside the temporary // directory, resulting in a directory traversal issue. path.remove(QStringLiteral("../")); return path; } ExtractionOptions TempExtractJob::extractionOptions() const { ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; if (m_passwordProtectedHint) { options[QStringLiteral("PasswordProtectedHint")] = true; } return options; } void TempExtractJob::doWork() { emit description(this, i18n("Extracting one file")); connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Extracting:" << m_entry; bool ret = archiveInterface()->extractFiles({m_entry}, extractionDir(), extractionOptions()); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "PreviewJob started"; } QString PreviewJob::extractionDir() const { return m_tmpExtractDir.path(); } OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenJob started"; m_tmpExtractDir = new QTemporaryDir(); } QTemporaryDir *OpenJob::tempDir() const { return m_tmpExtractDir; } QString OpenJob::extractionDir() const { return m_tmpExtractDir->path(); } OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : OpenJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenWithJob started"; } AddJob::AddJob(const QList &entries, const Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "AddJob started"; } void AddJob::doWork() { qCDebug(ARK) << "AddJob: going to add" << m_entries.count() << "file(s)"; emit description(this, i18np("Adding a file", "Adding %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); const QString globalWorkDir = m_options.value(QStringLiteral("GlobalWorkDir")).toString(); const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); if (!globalWorkDir.isEmpty()) { qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir; m_oldWorkingDir = QDir::currentPath(); QDir::setCurrent(globalWorkDir); } // The file paths must be relative to GlobalWorkDir. foreach (Archive::Entry *entry, m_entries) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically const QString &fullPath = entry->fullPath(); QString relativePath = workDir.relativeFilePath(fullPath); if (fullPath.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } - qCDebug(ARK) << entry->property("fullPath") << entry->isDir() << relativePath; entry->setFullPath(relativePath); } connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void AddJob::onFinished(bool result) { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } Job::onFinished(result); } MoveJob::MoveJob(const QList &entries, Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) , m_finishedSignalsCount(0) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "MoveJob started"; } void MoveJob::doWork() { qCDebug(ARK) << "MoveJob: going to move" << m_entries.count() << "file(s)"; emit description(this, i18np("Moving a file", "Moving %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->moveFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void MoveJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->moveRequiredSignals()) { Job::onFinished(result); } } CopyJob::CopyJob(const QList &entries, Archive::Entry *destination, const CompressionOptions &options, ReadWriteArchiveInterface *interface) : Job(interface) , m_finishedSignalsCount(0) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "CopyJob started"; } void CopyJob::doWork() { qCDebug(ARK) << "CopyJob: going to copy" << m_entries.count() << "file(s)"; emit description(this, i18np("Copying a file", "Copying %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->copyFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void CopyJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->copyRequiredSignals()) { Job::onFinished(result); } } DeleteJob::DeleteJob(const QList &entries, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) { } void DeleteJob::doWork() { emit description(this, i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->deleteFiles(m_entries); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface) : Job(interface) , m_comment(comment) { } void CommentJob::doWork() { emit description(this, i18n("Adding comment")); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addComment(m_comment); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } TestJob::TestJob(ReadOnlyArchiveInterface *interface) : Job(interface) { m_testSuccess = false; } void TestJob::doWork() { qCDebug(ARK) << "TestJob started"; emit description(this, i18n("Testing archive")); connectToArchiveInterfaceSignals(); connect(archiveInterface(), &ReadOnlyArchiveInterface::testSuccess, this, &TestJob::onTestSuccess); bool ret = archiveInterface()->testArchive(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void TestJob::onTestSuccess() { m_testSuccess = true; } bool TestJob::testSucceeded() { return m_testSuccess; } } // namespace Kerfuffle diff --git a/part/part.cpp b/part/part.cpp index 9606c53c..a803c7d5 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1659 +1,1664 @@ /* * 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 * * 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 "archiveview.h" #include "arkviewer.h" #include "dnddbusinterfaceadaptor.h" #include "infopanel.h" #include "jobtracker.h" #include "kerfuffle/extractiondialog.h" #include "kerfuffle/extractionsettingspage.h" #include "kerfuffle/jobs.h" #include "kerfuffle/settings.h" #include "kerfuffle/previewsettingspage.h" #include "kerfuffle/propertiesdialog.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 using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(Factory, "ark_part.json", registerPlugin();) namespace Ark { static quint32 s_instanceCounter = 1; Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args) : KParts::ReadWritePart(parent), m_splitter(Q_NULLPTR), m_busy(false), + m_archiveIsLoaded(false), m_jobTracker(Q_NULLPTR) { Q_UNUSED(args) setComponentData(*createAboutData(), 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_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->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); // 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)); connect(m_model, &ArchiveModel::error, this, &Part::slotError); connect(this, &Part::busy, this, &Part::setBusyGui); connect(this, &Part::ready, this, &Part::setReadyGui); connect(this, static_cast(&KParts::ReadOnlyPart::completed), this, &Part::setFileNameFromArchive); m_statusBarExtension = new KParts::StatusBarExtension(this); setXMLFile(QStringLiteral("ark_part.rc")); } Part::~Part() { qDeleteAll(m_tmpOpenDirList); // 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(); } } KAboutData *Part::createAboutData() { return new KAboutData(QStringLiteral("ark"), i18n("ArkPart"), QStringLiteral("3.0")); } void Part::registerJob(KJob* job) { if (!m_jobTracker) { m_jobTracker = new JobTracker(widget()); m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(0), 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[QStringLiteral("PreservePaths")] = true; options[QStringLiteral("RemoveRootNode")] = true; options[QStringLiteral("DragAndDrop")] = true; // Create and start the ExtractJob. ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), destination, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } void Part::setupView() { m_view->setContextMenuPolicy(Qt::CustomContextMenu); m_view->setModel(m_model); m_view->setSortingEnabled(true); 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); connect(m_model, &QAbstractItemModel::columnsInserted, this, &Part::adjustColumns); } void Part::slotActivated(QModelIndex) { // 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; 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)); 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)); 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)); 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 to...")); m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); connect(m_addFilesAction, &QAction::triggered, this, static_cast(&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, this, &Part::slotEditFileName); 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)); 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); connect(m_signalMapper, static_cast(&QSignalMapper::mapped), this, &Part::slotOpenEntry); updateActions(); updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); } void Part::updateActions() { bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); int selectedEntriesCount = m_view->selectionModel()->selectedRows().count(); // 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 != Q_NULLPTR && entry->property("size").toLongLong() < maxPreviewSize)); const bool isDir = (entry == Q_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); 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_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); } 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() << 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(); } void Part::resetGui() { m_messageWidget->hide(); m_commentView->clear(); m_commentBox->hide(); } 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().last()); } 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()) { const QString userDestination = triggeredAction->data().toString(); qCDebug(ARK) << "Extract to user dest" << userDestination; QString finalDestinationDirectory; const QString detectedSubfolder = detectSubfolder(); qCDebug(ARK) << "Detected subfolder" << detectedSubfolder; if (!isSingleFolderArchive()) { finalDestinationDirectory = userDestination + QDir::separator() + detectedSubfolder; QDir(userDestination).mkdir(detectedSubfolder); } else { finalDestinationDirectory = userDestination; } qCDebug(ARK) << "Extract to final dest" << finalDestinationDirectory; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; QList files = filesAndRootNodesForIndexes(m_view->selectionModel()->selectedRows()); ExtractJob *job = m_model->extractFiles(files, finalDestinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } } void Part::selectionChanged() { m_infoPanel->setIndexes(m_view->selectionModel()->selectedRows()); } bool Part::openFile() { qCDebug(ARK) << "Attempting to open archive" << localFilePath(); resetGui(); if (!isLocalFileValid()) { return false; } const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; QScopedPointer archive(Kerfuffle::Archive::create(localFilePath(), fixedMimeType, m_model)); Q_ASSERT(archive); if (archive->error() == NoPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. No suitable plugin found." "Ark does not seem to support this file type.", QFileInfo(localFilePath()).fileName())); return false; } if (archive->error() == FailedPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. Failed to load a suitable plugin." "Make sure any executables needed to handle the archive type are installed.", QFileInfo(localFilePath()).fileName())); return false; } Q_ASSERT(archive->isValid()); // Plugin loaded successfully. KJob *job = m_model->setArchive(archive.take()); if (job) { registerJob(job); job->start(); } else { updateActions(); } m_infoPanel->setIndex(QModelIndex()); if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) { QTimer::singleShot(0, this, &Part::slotShowExtractionDialog); } const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")]; if (!password.isEmpty()) { m_model->encryptArchive(password, arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true")); } return true; } 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 ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction Settings"), QStringLiteral("archive-extract"))); 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"), KGuiItem(i18nc("@action:button", "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->errorText())); } // The file failed to open, so reset the open archive, info panel and caption. m_model->setArchive(Q_NULLPTR); + m_archiveIsLoaded = false; m_infoPanel->setPrettyFileName(QString()); m_infoPanel->updateWithDefaults(); emit setWindowCaption(QString()); } } + else { + m_archiveIsLoaded = true; + } m_view->sortByColumn(0, Qt::AscendingOrder); // #303708: expand the first level only when there is just one root folder. // Typical use case: an archive with source files. if (m_view->model()->rowCount() == 1) { m_view->expandToDepth(0); } // After loading all files, resize the columns to fit all fields m_view->header()->resizeSections(QHeaderView::ResizeToContents); 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.")); } } } 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) { qCDebug(ARK) << "Opening with mode" << mode; QModelIndex index = m_view->selectionModel()->currentIndex(); Archive::Entry *entry = m_model->entryForIndex(index); // Don't open directories. if (entry->isDir()) { 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()) { m_openFileMode = static_cast(mode); KJob *job = Q_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_tmpOpenDirList << openJob->tempDir(); const QString fullName = openJob->validatedFilePath(); bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); // 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. if (!isWritable) { QFile::setPermissions(fullName, QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther); } if (isWritable) { m_fileWatcher = new QFileSystemWatcher; connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &Part::slotWatchedFileModified); m_fileWatcher->addPath(fullName); } 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()); } } 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); 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_tmpOpenDirList) { 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, Q_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); } } bool Part::isSingleFolderArchive() const { return m_model->archive()->isSingleFolderArchive(); } 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()->setSingleFolderArchive(isSingleFolderArchive()); 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); QList files; // If the user has chosen to extract only selected entries, fetch these // from the QTreeView. if (!dialog.data()->extractAllFiles()) { files = filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); } qCDebug(ARK) << "Selected " << files; Kerfuffle::ExtractionOptions options; if (dialog.data()->preservePaths()) { options[QStringLiteral("PreservePaths")] = true; } options[QStringLiteral("FollowExtractionDialogSettings")] = true; const QString destinationDirectory = dialog.data()->destinationDirectory().toDisplayString(QUrl::PreferLocalFile); 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; } QList Part::filesForIndexes(const QModelIndexList& list) const { QList ret; foreach(const QModelIndex& index, list) { ret << m_model->entryForIndex(index); } return ret; } QList Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { QList 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); const bool followExtractionDialogSettings = extractJob->extractionOptions().value(QStringLiteral("FollowExtractionDialogSettings"), false).toBool(); if (!followExtractionDialogSettings) { return; } 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()); } if (ArkSettings::closeAfterExtraction()) { emit quit(); } } } void Part::adjustColumns() { m_view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); } void Part::slotAddFiles(const QStringList& filesToAdd, const Archive::Entry *destination, const QString &relPath) { - if (filesToAdd.isEmpty()) { + if (!m_archiveIsLoaded || filesToAdd.isEmpty()) { return; } QStringList withChildPaths; foreach (const QString& file, filesToAdd) { m_jobTempEntries.push_back(new Archive::Entry(Q_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 == Q_NULLPTR) ? QString() : QStringLiteral("to ") + destination->fullPath()); } // Remove trailing slash (needed when adding dirs). if (globalWorkDir.right(1) == QLatin1String("/")) { globalWorkDir.chop(1); } CompressionOptions options(m_model->archive()->compressionOptions()); // Now take the absolute path of the parent directory. globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath(); qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir; options[QStringLiteral("GlobalWorkDir")] = globalWorkDir; AddJob *job = m_model->addFiles(m_jobTempEntries, destination, options); if (!job) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); return; } connect(job, &KJob::result, this, &Part::slotAddFilesDone); registerJob(job); job->start(); } void Part::slotAddFiles() { // If compression options are already set, we don't use the values from CreateDialog. CompressionOptions opts; if (m_model->archive()->compressionOptions().isEmpty()) { if (arguments().metaData().contains(QStringLiteral("compressionLevel"))) { opts[QStringLiteral("CompressionLevel")] = arguments().metaData()[QStringLiteral("compressionLevel")]; } m_model->archive()->setCompressionOptions(opts); } else { opts = m_model->archive()->compressionOptions(); } QString dialogTitle = i18nc("@title:window", "Add Files"); const Archive::Entry *destination = Q_NULLPTR; if (m_view->selectionModel()->selectedRows().count() == 1) { destination = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); if (destination->isDir()) { dialogTitle = i18nc("@title:window", "Add Files to %1", destination->fullPath());; } else { destination = Q_NULLPTR; } } qCDebug(ARK) << "Opening AddDialog with opts:" << opts; // #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(), opts); if (dlg->exec() == QDialog::Accepted) { qCDebug(ARK) << "Selected files:" << dlg->selectedFiles(); qCDebug(ARK) << "Options:" << dlg->compressionOptions(); m_model->archive()->setCompressionOptions(dlg->compressionOptions()); slotAddFiles(dlg->selectedFiles(), destination, QString()); } delete dlg; } void Part::slotEditFileName() { QModelIndex currentIndex = m_view->selectionModel()->currentIndex(); currentIndex = (currentIndex.parent().isValid()) ? currentIndex.parent().child(currentIndex.row(), 0) : m_model->index(currentIndex.row(), 0); m_view->openEntryEditor(currentIndex); } void Part::slotCutFiles() { QModelIndexList selectedRows = addChildren(m_view->selectionModel()->selectedRows()); 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(m_view->selectionModel()->selectedRows()))); 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(QString name) { if (name == QStringLiteral(".") || name == QStringLiteral("..") || name.contains(QLatin1Char('/'))) { QMessageBox messageBox(QMessageBox::Warning, i18n("Invalid filename"), i18n("Filename can't contain slashes and can't be equal to \".\" or \"..\""), QMessageBox::Ok); messageBox.exec(); return; } const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); QList entriesToMove = filesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); m_destination = new Archive::Entry(); const QString &entryPath = entry->fullPath(true); const QString rootPath = entryPath.left(entryPath.count() - entry->name().count()); m_destination->setFullPath(rootPath + name + ((entry->isDir()) ? QLatin1Char('/') : QChar())); slotPasteFiles(entriesToMove, m_destination, 1); } void Part::slotPasteFiles() { m_destination = (m_view->selectionModel()->selectedRows().count() > 0) ? m_model->entryForIndex(m_view->selectionModel()->currentIndex()) : Q_NULLPTR; if (m_destination == Q_NULLPTR) { m_destination = new Archive::Entry(Q_NULLPTR, QString()); } else { m_destination = new Archive::Entry(Q_NULLPTR, m_destination->fullPath()); } if (m_model->filesToMove.count() > 0) { // Changing destination to include new entry path if pasting only 1 entry. QList entriesWithoutChildren = ReadOnlyArchiveInterface::entriesWithoutChildren(m_model->filesToMove.values()); if (entriesWithoutChildren.count() == 1) { const Archive::Entry *entry = entriesWithoutChildren.first(); const QString nameWithSlash = entry->name() + ((entry->isDir()) ? QLatin1String("/") : QString()); m_destination->setFullPath(m_destination->fullPath() + nameWithSlash); } foreach (const Archive::Entry *entry, entriesWithoutChildren) { if (entry->isDir() && m_destination->fullPath().startsWith(entry->fullPath())) { QMessageBox messageBox(QMessageBox::Warning, i18n("Moving a folder into itself"), i18n("Folders can't be moved into themselves."), QMessageBox::Ok); messageBox.exec(); delete m_destination; return; } } QList entryList = m_model->filesToMove.values(); slotPasteFiles(entryList, m_destination, entriesWithoutChildren.count()); m_model->filesToMove.clear(); } else { QList entryList = m_model->filesToCopy.values(); slotPasteFiles(entryList, m_destination, 0); m_model->filesToCopy.clear(); } m_cutIndexes.clear(); updateActions(); } void Part::slotPasteFiles(QList &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)) { QMessageBox messageBox(QMessageBox::Warning, i18n("Pasting entries with the same name"), i18n("Entries with the same names can't be pasted to the same destination."), QMessageBox::Ok); messageBox.exec(); 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; } CompressionOptions options(m_model->archive()->compressionOptions()); KJob *job; if (entriesWithoutChildren != 0) { job = m_model->moveFiles(files, destination, options); } else { job = m_model->copyFiles(files, destination, options); } 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(); } 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(m_view->selectionModel()->selectedRows()))); connect(job, &KJob::result, this, &Part::slotDeleteFilesDone); registerJob(job); job->start(); } void Part::slotShowProperties() { QPointer dialog(new Kerfuffle::PropertiesDialog(0, m_model->archive())); 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()); } 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" diff --git a/part/part.h b/part/part.h index 42367763..b428388c 100644 --- a/part/part.h +++ b/part/part.h @@ -1,218 +1,219 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef PART_H #define PART_H #include "interface.h" #include "kerfuffle/archiveentry.h" #include #include #include #include #include class ArchiveModel; class ArchiveView; class InfoPanel; class KAboutData; class KAbstractWidgetJobTracker; class KJob; class KToggleAction; class QAction; class QSplitter; class QTreeView; class QTemporaryDir; class QVBoxLayout; class QSignalMapper; class QFileSystemWatcher; class QGroupBox; class QPlainTextEdit; namespace Ark { class Part: public KParts::ReadWritePart, public Interface { Q_OBJECT Q_INTERFACES(Interface) public: enum OpenFileMode { Preview, OpenFile, OpenFileWith }; Part(QWidget *parentWidget, QObject *parent, const QVariantList &); ~Part(); static KAboutData *createAboutData(); bool openFile() Q_DECL_OVERRIDE; bool saveFile() Q_DECL_OVERRIDE; bool isBusy() const Q_DECL_OVERRIDE; KConfigSkeleton *config() const Q_DECL_OVERRIDE; QList settingsPages(QWidget *parent) const Q_DECL_OVERRIDE; /** * Validate the localFilePath() associated to this part. * If the file is not valid, an error message is displayed to the user. * @return Whether the localFilePath() can be loaded by the part. */ bool isLocalFileValid(); /** * Ask the user whether to overwrite @p targetFile, when creating a new archive with the same path. * @return True if the file has been successfully removed upon user's will. False otherwise. */ bool confirmAndDelete(const QString& targetFile); public slots: void extractSelectedFilesTo(const QString& localPath); private slots: void slotLoadingStarted(); void slotLoadingFinished(KJob *job); void slotOpenExtractedEntry(KJob*); void slotPreviewExtractedEntry(KJob* job); void slotOpenEntry(int mode); void slotError(const QString& errorMessage, const QString& details); void slotExtractArchive(); void slotShowExtractionDialog(); void slotExtractionDone(KJob*); void slotQuickExtractFiles(QAction*); /** * Creates and starts AddJob. * * @param files Files to add. * @param destination Destination path within the archive to which entries have to be added. Is used on addto action * or drag'n'drop event, for adding a watched file it has empty. * @param relPath Relative path of watched entry inside the archive. Is used only for adding temporarily extracted * watched file. */ void slotAddFiles(const QStringList &files, const Kerfuffle::Archive::Entry *destination, const QString &relPath); /** * Creates and starts MoveJob or CopyJob. * * @param files Files to paste. * @param destination Destination path within the archive to which entries have to be added. For renaming an entry * the path has to contain a new filename too. * @param entriesWithoutChildren Entries count, excluding their children. For CopyJob 0 MUST be passed. */ void slotPasteFiles(QList &files, Kerfuffle::Archive::Entry *destination, int entriesWithoutChildren); void slotAddFiles(); void slotEditFileName(); void slotCutFiles(); void slotCopyFiles(); void slotRenameFile(QString name); void slotPasteFiles(); void slotAddFilesDone(KJob*); void slotPasteFilesDone(KJob*); void slotTestingDone(KJob*); void slotDeleteFiles(); void slotDeleteFilesDone(KJob*); void slotShowProperties(); void slotShowContextMenu(); void slotActivated(QModelIndex); void slotToggleInfoPanel(bool); void slotSaveAs(); void updateActions(); void updateQuickExtractMenu(QAction *extractAction); void selectionChanged(); void adjustColumns(); void setBusyGui(); void setReadyGui(); void setFileNameFromArchive(); void slotWatchedFileModified(const QString& file); void slotShowComment(); void slotAddComment(); void slotCommentChanged(); void slotTestArchive(); signals: void busy(); void ready(); void quit(); private: void resetGui(); void setupView(); void setupActions(); bool isSingleFolderArchive() const; QString detectSubfolder() const; QList filesForIndexes(const QModelIndexList& list) const; QList filesAndRootNodesForIndexes(const QModelIndexList& list) const; QModelIndexList addChildren(const QModelIndexList &list) const; void registerJob(KJob *job); void displayMsgWidget(KMessageWidget::MessageType type, const QString& msg); ArchiveModel *m_model; ArchiveView *m_view; QAction *m_previewAction; QAction *m_openFileAction; QAction *m_openFileWithAction; QAction *m_extractArchiveAction; QAction *m_extractAction; QAction *m_addFilesAction; QAction *m_renameFileAction; QAction *m_deleteFilesAction; QAction *m_cutFilesAction; QAction *m_copyFilesAction; QAction *m_pasteFilesAction; QAction *m_saveAsAction; QAction *m_propertiesAction; QAction *m_editCommentAction; QAction *m_testArchiveAction; KToggleAction *m_showInfoPanelAction; InfoPanel *m_infoPanel; QSplitter *m_splitter; QList m_tmpOpenDirList; bool m_busy; + bool m_archiveIsLoaded; OpenFileMode m_openFileMode; QUrl m_lastUsedAddPath; QList m_jobTempEntries; Kerfuffle::Archive::Entry *m_destination; QModelIndexList m_cutIndexes; KAbstractWidgetJobTracker *m_jobTracker; KParts::StatusBarExtension *m_statusBarExtension; QVBoxLayout *m_vlayout; QSignalMapper *m_signalMapper; QFileSystemWatcher *m_fileWatcher; QSplitter *m_commentSplitter; QGroupBox *m_commentBox; QPlainTextEdit *m_commentView; KMessageWidget *m_commentMsgWidget; KMessageWidget *m_messageWidget; }; } // namespace Ark #endif // PART_H