File indexing completed on 2024-04-28 05:47:45

0001 /*
0002     SPDX-FileCopyrightText: 2007 Henrique Pinto <henrique.pinto@kdemail.net>
0003     SPDX-FileCopyrightText: 2008-2009 Harald Hvaal <haraldhv@stud.ntnu.no>
0004     SPDX-FileCopyrightText: 2009-2012 Raphael Kubo da Costa <rakuco@FreeBSD.org>
0005     SPDX-FileCopyrightText: 2016 Vladyslav Batyrenko <mvlabat@gmail.com>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "part.h"
0011 #include "adddialog.h"
0012 #include "archiveformat.h"
0013 #include "archivemodel.h"
0014 #include "archivesortfiltermodel.h"
0015 #include "archiveview.h"
0016 #include "ark_debug.h"
0017 #include "arkviewer.h"
0018 #include "dnddbusinterfaceadaptor.h"
0019 #include "extractiondialog.h"
0020 #include "extractionsettingspage.h"
0021 #include "generalsettingspage.h"
0022 #include "infopanel.h"
0023 #include "jobs.h"
0024 #include "jobtracker.h"
0025 #include "overwritedialog.h"
0026 #include "pluginmanager.h"
0027 #include "pluginsettingspage.h"
0028 #include "previewsettingspage.h"
0029 #include "propertiesdialog.h"
0030 #include "settings.h"
0031 
0032 #include <KActionCollection>
0033 #include <KConfigGroup>
0034 #include <KIO/ApplicationLauncherJob>
0035 #include <KIO/FileCopyJob>
0036 #include <KIO/Job>
0037 #include <KIO/JobTracker>
0038 #include <KIO/JobUiDelegate>
0039 #include <KIO/JobUiDelegateFactory>
0040 #include <KIO/OpenUrlJob>
0041 #include <KIO/StatJob>
0042 #include <KJobWidgets>
0043 #include <KLocalizedString>
0044 #include <KMessageBox>
0045 #include <KParts/OpenUrlArguments>
0046 #include <KPluginFactory>
0047 #include <KPluginMetaData>
0048 #include <KStandardGuiItem>
0049 #include <KToggleAction>
0050 #include <KXMLGUIFactory>
0051 
0052 #include <QAction>
0053 #include <QCursor>
0054 #include <QFileDialog>
0055 #include <QFileSystemWatcher>
0056 #include <QGroupBox>
0057 #include <QHeaderView>
0058 #include <QIcon>
0059 #include <QLineEdit>
0060 #include <QMenu>
0061 #include <QPlainTextEdit>
0062 #include <QPointer>
0063 #include <QPushButton>
0064 #include <QSplitter>
0065 #include <QStatusBar>
0066 
0067 using namespace Kerfuffle;
0068 
0069 namespace Ark
0070 {
0071 static quint32 s_instanceCounter = 1;
0072 
0073 Part::Part(QWidget *parentWidget, QObject *parent, const KPluginMetaData &metaData, const QVariantList &args)
0074     : KParts::ReadWritePart(parent, metaData)
0075     , m_splitter(nullptr)
0076     , m_busy(false)
0077     , m_jobTracker(nullptr)
0078 {
0079     Q_UNUSED(args)
0080 
0081     new DndExtractAdaptor(this);
0082 
0083     // Since QFileSystemWatcher::fileChanged is emitted for each part of the file that is flushed,
0084     // we wait a bit to ensure that the last flush will write the file completely
0085     // BUG: 382606, also see https://bugreports.qt.io/browse/QTBUG-8244
0086     // TODO: Find the most optimal flush interval
0087     m_watchedFileChangeTimer.setSingleShot(true);
0088     m_watchedFileChangeTimer.setInterval(200);
0089     connect(&m_watchedFileChangeTimer, &QTimer::timeout, this, [this]() {
0090         slotWatchedFileModified(m_lastChangedFilename);
0091     });
0092 
0093     const QString pathName = QStringLiteral("/DndExtract/%1").arg(s_instanceCounter++);
0094     if (!QDBusConnection::sessionBus().registerObject(pathName, this)) {
0095         qCCritical(ARK) << "Could not register a D-Bus object for drag'n'drop";
0096     }
0097 
0098     // m_vlayout is needed for later insertion of QMessageWidget
0099     QWidget *mainWidget = new QWidget;
0100     m_vlayout = new QVBoxLayout;
0101     m_vlayout->setSpacing(0);
0102     m_model = new ArchiveModel(pathName, this);
0103     m_filterModel = new ArchiveSortFilterModel(this);
0104     m_splitter = new QSplitter(Qt::Horizontal, parentWidget);
0105     m_view = new ArchiveView;
0106     m_infoPanel = new InfoPanel(m_model);
0107 
0108     // Add widgets for the comment field.
0109     m_commentView = new QPlainTextEdit();
0110     m_commentView->setReadOnly(true);
0111     m_commentView->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
0112     m_commentBox = new QGroupBox(i18n("Comment"));
0113     m_commentBox->setFlat(true);
0114     m_commentBox->hide();
0115     QVBoxLayout *vbox = new QVBoxLayout;
0116     vbox->addWidget(m_commentView);
0117     m_commentBox->setLayout(vbox);
0118 
0119     m_messageWidget = new KMessageWidget(parentWidget);
0120     m_messageWidget->setWordWrap(true);
0121     m_messageWidget->setPosition(KMessageWidget::Header);
0122     m_messageWidget->hide();
0123 
0124     m_commentMsgWidget = new KMessageWidget();
0125     m_commentMsgWidget->setText(i18n("Comment has been modified."));
0126     m_commentMsgWidget->setMessageType(KMessageWidget::Information);
0127     m_commentMsgWidget->setCloseButtonVisible(false);
0128     m_commentMsgWidget->hide();
0129 
0130     QAction *saveAction = new QAction(i18n("Save"), m_commentMsgWidget);
0131     m_commentMsgWidget->addAction(saveAction);
0132     connect(saveAction, &QAction::triggered, this, &Part::slotAddComment);
0133 
0134     m_commentBox->layout()->addWidget(m_commentMsgWidget);
0135 
0136     connect(m_commentView, &QPlainTextEdit::textChanged, this, &Part::slotCommentChanged);
0137 
0138     setWidget(mainWidget);
0139     mainWidget->setLayout(m_vlayout);
0140 
0141     // Setup search widget.
0142     m_searchWidget = new QWidget(parentWidget);
0143     m_searchWidget->setVisible(false);
0144     m_searchWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
0145     QHBoxLayout *searchLayout = new QHBoxLayout;
0146     searchLayout->setContentsMargins(2, 2, 2, 2);
0147     m_vlayout->addWidget(m_searchWidget);
0148     m_searchWidget->setLayout(searchLayout);
0149     m_searchCloseButton = new QPushButton(QIcon::fromTheme(QStringLiteral("dialog-close")), QString(), m_searchWidget);
0150     m_searchCloseButton->setFlat(true);
0151     m_searchLineEdit = new QLineEdit(m_searchWidget);
0152     m_searchLineEdit->setClearButtonEnabled(true);
0153     m_searchLineEdit->setPlaceholderText(i18n("Type to search..."));
0154     mainWidget->installEventFilter(this);
0155     searchLayout->addWidget(m_searchCloseButton);
0156     searchLayout->addWidget(m_searchLineEdit);
0157     connect(m_searchCloseButton, &QPushButton::clicked, this, [=]() {
0158         m_searchWidget->hide();
0159         m_searchLineEdit->clear();
0160     });
0161     connect(m_searchLineEdit, &QLineEdit::textChanged, this, &Part::searchEdited);
0162 
0163     // Configure the QVBoxLayout and add widgets
0164     m_vlayout->setContentsMargins(0, 0, 0, 0);
0165     m_vlayout->addWidget(m_messageWidget);
0166     m_vlayout->addWidget(m_splitter);
0167 
0168     // Vertical QSplitter for the file view and comment field.
0169     m_commentSplitter = new QSplitter(Qt::Vertical, parentWidget);
0170     m_commentSplitter->setOpaqueResize(false);
0171     m_commentSplitter->addWidget(m_view);
0172     m_commentSplitter->addWidget(m_commentBox);
0173     m_commentSplitter->setCollapsible(0, false);
0174 
0175     // Horizontal QSplitter for the file view and infopanel.
0176     m_splitter->addWidget(m_commentSplitter);
0177     m_splitter->addWidget(m_infoPanel);
0178 
0179     // Read settings from config file and show/hide infoPanel.
0180     if (!ArkSettings::showInfoPanel()) {
0181         m_infoPanel->hide();
0182     } else {
0183         m_splitter->setSizes(ArkSettings::splitterSizes());
0184     }
0185 
0186     setupView();
0187     setupActions();
0188 
0189     connect(m_view, &ArchiveView::entryChanged, this, &Part::slotRenameFile);
0190 
0191     connect(m_model, &ArchiveModel::loadingStarted, this, &Part::slotLoadingStarted);
0192     connect(m_model, &ArchiveModel::loadingFinished, this, &Part::slotLoadingFinished);
0193     connect(m_model, &ArchiveModel::droppedFiles, this, &Part::slotDroppedFiles);
0194     connect(m_model, &ArchiveModel::error, this, &Part::slotError);
0195     connect(m_model, &ArchiveModel::messageWidget, this, &Part::displayMsgWidget);
0196 
0197     connect(this, &Part::busy, this, &Part::setBusyGui);
0198     connect(this, &Part::ready, this, &Part::setReadyGui);
0199     connect(this, &KParts::ReadOnlyPart::urlChanged, this, &Part::setFileNameFromArchive);
0200     connect(this, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &Part::setFileNameFromArchive);
0201     connect(this, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &Part::slotCompleted);
0202     connect(ArkSettings::self(), &KCoreConfigSkeleton::configChanged, this, &Part::updateActions);
0203 
0204     m_statusBarExtension = new KParts::StatusBarExtension(this);
0205 
0206     setXMLFile(QStringLiteral("ark_part.rc"));
0207 }
0208 
0209 Part::~Part()
0210 {
0211     qDeleteAll(m_tmpExtractDirList);
0212 
0213     // save the state of m_infoPanel only if it's embedded
0214     if (m_splitter->indexOf(m_infoPanel) >= 0) {
0215         // Only save splitterSizes if infopanel is visible,
0216         // because we don't want to store zero size for infopanel.
0217         if (m_showInfoPanelAction->isChecked()) {
0218             ArkSettings::setSplitterSizes(m_splitter->sizes());
0219         }
0220         ArkSettings::setShowInfoPanel(m_showInfoPanelAction->isChecked());
0221     }
0222     ArkSettings::self()->save();
0223 
0224     m_extractArchiveAction->menu()->deleteLater();
0225     m_extractAction->menu()->deleteLater();
0226 }
0227 
0228 QString Part::componentName() const
0229 {
0230     // also the part ui.rc file is in the program folder
0231     // TODO: change the component name to "arkpart" by removing this method and
0232     // adapting the folder where the file is placed.
0233     // Needs a way to also move any potential custom user ui.rc files
0234     // from ark/ark_part.rc to arkpart/ark_part.rc
0235     return QStringLiteral("ark");
0236 }
0237 
0238 void Part::slotCommentChanged()
0239 {
0240     if (!m_model->archive() || m_commentView->toPlainText().isEmpty()) {
0241         return;
0242     }
0243 
0244     if (m_commentMsgWidget->isHidden() && m_commentView->toPlainText() != m_model->archive()->comment()) {
0245         m_commentMsgWidget->animatedShow();
0246     } else if (m_commentMsgWidget->isVisible() && m_commentView->toPlainText() == m_model->archive()->comment()) {
0247         m_commentMsgWidget->hide();
0248     }
0249 }
0250 
0251 void Part::registerJob(KJob *job)
0252 {
0253     if (!m_jobTracker) {
0254         m_jobTracker = new JobTracker(widget());
0255         m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(nullptr), 0, true);
0256         m_jobTracker->widget(job)->show();
0257     }
0258 
0259     KIO::getJobTracker()->registerJob(job);
0260     m_jobTracker->registerJob(job);
0261 
0262     Q_EMIT busy();
0263     connect(job, &KJob::result, this, &Part::ready);
0264 }
0265 
0266 // TODO: KIO::mostLocalHere is used here to resolve some KIO URLs to local
0267 // paths (e.g. desktop:/), but more work is needed to support extraction
0268 // to non-local destinations. See bugs #189322 and #204323.
0269 void Part::extractSelectedFilesTo(const QString &localPath)
0270 {
0271     if (!m_model) {
0272         return;
0273     }
0274 
0275     const QUrl url = QUrl::fromUserInput(localPath, QDir::currentPath());
0276 
0277     auto doExtract = [this](const QString &destination) {
0278         qCDebug(ARK) << "Extract to" << destination;
0279 
0280         Kerfuffle::ExtractionOptions options;
0281         options.setDragAndDropEnabled(true);
0282 
0283         // Create and start the ExtractJob.
0284         ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(getSelectedIndexes())), destination, options);
0285         registerJob(job);
0286         connect(job, &KJob::result, this, &Part::slotExtractionDone);
0287         job->start();
0288     };
0289 
0290     if (!url.isLocalFile() && !url.scheme().isEmpty()) {
0291         // Try to resolve the URL to a local path.
0292         KIO::StatJob *statJob = KIO::mostLocalUrl(url);
0293 
0294         connect(statJob, &KJob::result, this, [=]() {
0295             if (statJob->error()) {
0296                 KMessageBox::error(widget(), statJob->errorString());
0297                 return;
0298             }
0299 
0300             const QString udsLocalPath = statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH);
0301             if (udsLocalPath.isEmpty()) { // The URL could not be resolved to a local path
0302                 qCWarning(ARK) << "Ark cannot extract to non-local destination:" << localPath;
0303                 KMessageBox::error(widget(), xi18nc("@info", "Ark can extract archives to local destinations only."));
0304                 return;
0305             }
0306 
0307             doExtract(udsLocalPath);
0308         });
0309 
0310         return;
0311     }
0312 
0313     doExtract(localPath);
0314 }
0315 
0316 void Part::guiActivateEvent(KParts::GUIActivateEvent *event)
0317 {
0318     // #357660: prevent parent's implementation from changing the window title.
0319     Q_UNUSED(event)
0320 }
0321 
0322 void Part::setupView()
0323 {
0324     m_view->setContextMenuPolicy(Qt::CustomContextMenu);
0325 
0326     m_filterModel->setSourceModel(m_model);
0327     m_view->setModel(m_filterModel);
0328 
0329     m_view->setItemDelegate(new NoHighlightSelectionDelegate(this));
0330 
0331     m_filterModel->setFilterKeyColumn(0);
0332     m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0333 
0334     connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::updateActions);
0335     connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::selectionChanged);
0336 
0337     connect(m_view, &QTreeView::activated, this, &Part::slotActivated);
0338 
0339     connect(m_view, &QWidget::customContextMenuRequested, this, &Part::slotShowContextMenu);
0340 }
0341 
0342 void Part::slotActivated(const QModelIndex &index)
0343 {
0344     Q_UNUSED(index)
0345 
0346     // The activated signal is emitted when items are selected with the mouse,
0347     // so do nothing if CTRL or SHIFT key is pressed.
0348     if (QGuiApplication::keyboardModifiers() != Qt::ShiftModifier && QGuiApplication::keyboardModifiers() != Qt::ControlModifier) {
0349         ArkSettings::defaultOpenAction() == ArkSettings::EnumDefaultOpenAction::Preview ? slotOpenEntry(Preview) : slotOpenEntry(OpenFile);
0350     }
0351 }
0352 
0353 void Part::setupActions()
0354 {
0355     m_showInfoPanelAction = new KToggleAction(i18nc("@action:inmenu", "Show Information Panel"), this);
0356     actionCollection()->addAction(QStringLiteral("show-infopanel"), m_showInfoPanelAction);
0357     m_showInfoPanelAction->setChecked(ArkSettings::showInfoPanel());
0358     connect(m_showInfoPanelAction, &QAction::triggered, this, &Part::slotToggleInfoPanel);
0359 
0360     m_saveAsAction = KStandardAction::saveAs(this, &Part::slotSaveAs, this);
0361     m_saveAsAction->setText(i18nc("@action:inmenu", "Save Copy As..."));
0362     actionCollection()->addAction(QStringLiteral("ark_file_save_as"), m_saveAsAction);
0363 
0364     m_openFileAction = actionCollection()->addAction(QStringLiteral("openfile"));
0365     m_openFileAction->setText(i18nc("open a file with external program", "&Open in External Application"));
0366     m_openFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-export")));
0367     connect(m_openFileAction, &QAction::triggered, this, [this]() {
0368         slotOpenEntry(OpenFile);
0369     });
0370 
0371     m_openFileWithAction = actionCollection()->addAction(QStringLiteral("openfilewith"));
0372     m_openFileWithAction->setText(i18nc("open a file with external program", "Open &With..."));
0373     m_openFileWithAction->setIcon(QIcon::fromTheme(QStringLiteral("document-export")));
0374     m_openFileWithAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with an external program"));
0375     connect(m_openFileWithAction, &QAction::triggered, this, [this]() {
0376         slotOpenEntry(OpenFileWith);
0377     });
0378 
0379     m_previewAction = actionCollection()->addAction(QStringLiteral("preview"));
0380     m_previewAction->setText(i18nc("to preview a file inside an archive", "Pre&view"));
0381     m_previewAction->setIcon(QIcon::fromTheme(QStringLiteral("document-preview-archive")));
0382     m_previewAction->setToolTip(i18nc("@info:tooltip", "Click to preview the selected file"));
0383     actionCollection()->setDefaultShortcut(m_previewAction, Qt::CTRL | Qt::Key_P);
0384     connect(m_previewAction, &QAction::triggered, this, [this]() {
0385         slotOpenEntry(Preview);
0386     });
0387 
0388     m_extractArchiveAction = actionCollection()->addAction(QStringLiteral("extract_all"));
0389     m_extractArchiveAction->setText(i18nc("@action:inmenu", "E&xtract All"));
0390     m_extractArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract")));
0391     m_extractArchiveAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose how to extract all the files in the archive"));
0392     actionCollection()->setDefaultShortcut(m_extractArchiveAction, Qt::CTRL | Qt::SHIFT | Qt::Key_E);
0393     connect(m_extractArchiveAction, &QAction::triggered, this, &Part::slotExtractArchive);
0394 
0395     m_extractAction = actionCollection()->addAction(QStringLiteral("extract"));
0396     m_extractAction->setText(i18nc("@action:inmenu", "&Extract"));
0397     m_extractAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract")));
0398     actionCollection()->setDefaultShortcut(m_extractAction, Qt::CTRL | Qt::Key_E);
0399     m_extractAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose to extract either all files or just the selected ones"));
0400     connect(m_extractAction, &QAction::triggered, this, &Part::slotShowExtractionDialog);
0401 
0402     m_addFilesAction = actionCollection()->addAction(QStringLiteral("add"));
0403     m_addFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert")));
0404     m_addFilesAction->setText(i18n("Add &Files..."));
0405     m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive"));
0406     actionCollection()->setDefaultShortcut(m_addFilesAction, Qt::ALT | Qt::Key_A);
0407     connect(m_addFilesAction, &QAction::triggered, this, QOverload<>::of(&Part::slotAddFiles));
0408 
0409     m_renameFileAction = KStandardAction::renameFile(m_view, &ArchiveView::renameSelectedEntry, actionCollection());
0410 
0411     m_deleteFilesAction = KStandardAction::deleteFile(this, &Part::slotDeleteFiles, actionCollection());
0412     m_deleteFilesAction->setText(i18nc("@action", "Remove from Archive"));
0413     m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove")));
0414     actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete);
0415 
0416     m_cutFilesAction = KStandardAction::cut(this, &Part::slotCutFiles, actionCollection());
0417     m_copyFilesAction = KStandardAction::copy(this, &Part::slotCopyFiles, actionCollection());
0418     m_pasteFilesAction = KStandardAction::paste(this, QOverload<>::of(&Part::slotPasteFiles), actionCollection());
0419 
0420     m_propertiesAction = actionCollection()->addAction(QStringLiteral("properties"));
0421     m_propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
0422     m_propertiesAction->setText(i18nc("@action:inmenu", "&Properties"));
0423     actionCollection()->setDefaultShortcut(m_propertiesAction, Qt::ALT | Qt::Key_Return);
0424     m_propertiesAction->setToolTip(i18nc("@info:tooltip", "Click to see properties for archive"));
0425     connect(m_propertiesAction, &QAction::triggered, this, &Part::slotShowProperties);
0426 
0427     m_editCommentAction = actionCollection()->addAction(QStringLiteral("edit_comment"));
0428     m_editCommentAction->setIcon(QIcon::fromTheme(QStringLiteral("document-edit")));
0429     actionCollection()->setDefaultShortcut(m_editCommentAction, Qt::ALT | Qt::Key_C);
0430     m_editCommentAction->setToolTip(i18nc("@info:tooltip", "Click to add or edit comment"));
0431     connect(m_editCommentAction, &QAction::triggered, this, &Part::slotShowComment);
0432 
0433     m_testArchiveAction = actionCollection()->addAction(QStringLiteral("test_archive"));
0434     m_testArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("checkmark")));
0435     m_testArchiveAction->setText(i18nc("@action:inmenu", "&Test Integrity"));
0436     actionCollection()->setDefaultShortcut(m_testArchiveAction, Qt::ALT | Qt::Key_T);
0437     m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity"));
0438     connect(m_testArchiveAction, &QAction::triggered, this, &Part::slotTestArchive);
0439 
0440     m_searchAction = KStandardAction::find(this, &Part::slotShowFind, actionCollection());
0441 
0442     updateActions();
0443     updateQuickExtractMenu(m_extractArchiveAction);
0444     updateQuickExtractMenu(m_extractAction);
0445 }
0446 
0447 void Part::updateActions()
0448 {
0449     const bool isWritable = isArchiveWritable();
0450     const Archive::Entry *entry = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex()));
0451     int selectedEntriesCount = m_view->selectionModel()->selectedRows().count();
0452 
0453     // We disable adding files if the archive is encrypted but the password is
0454     // unknown (this happens when opening existing non-he password-protected
0455     // archives). If we added files they would not get encrypted resulting in an
0456     // archive with a mixture of encrypted and unencrypted files.
0457     const bool isEncryptedButUnknownPassword =
0458         m_model->archive() && m_model->archive()->encryptionType() != Archive::Unencrypted && m_model->archive()->password().isEmpty();
0459 
0460     if (isEncryptedButUnknownPassword) {
0461         m_addFilesAction->setToolTip(xi18nc("@info:tooltip",
0462                                             "Adding files to existing password-protected archives with no header-encryption is currently not supported."
0463                                             "<nl/><nl/>Extract the files and create a new archive if you want to add files."));
0464         m_testArchiveAction->setToolTip(xi18nc("@info:tooltip", "Testing password-protected archives with no header-encryption is currently not supported."));
0465     } else {
0466         m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive"));
0467         m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity"));
0468     }
0469 
0470     // Figure out if entry size is larger than preview size limit.
0471     const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024;
0472     const bool limit = ArkSettings::limitPreviewFileSize();
0473     bool isPreviewable = (!limit || (limit && entry != nullptr && entry->property("size").toLongLong() < maxPreviewSize));
0474 
0475     const bool isDir = (entry == nullptr) ? false : entry->isDir();
0476     m_previewAction->setEnabled(!isBusy() && isPreviewable && !isDir && (selectedEntriesCount == 1));
0477     m_extractArchiveAction->setEnabled(!isBusy() && (m_model->rowCount() > 0));
0478     m_extractAction->setEnabled(!isBusy() && (m_model->rowCount() > 0));
0479     m_saveAsAction->setEnabled(!isBusy() && m_model->rowCount() > 0);
0480     m_addFilesAction->setEnabled(!isBusy() && isWritable && !isEncryptedButUnknownPassword);
0481     m_deleteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0));
0482     m_openFileAction->setEnabled(!isBusy() && isPreviewable && !isDir && (selectedEntriesCount == 1));
0483     m_openFileWithAction->setEnabled(!isBusy() && isPreviewable && !isDir && (selectedEntriesCount == 1));
0484     m_propertiesAction->setEnabled(!isBusy() && m_model->archive());
0485 
0486     m_renameFileAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount == 1));
0487     m_cutFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0));
0488     m_copyFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0));
0489     m_pasteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount == 0 || (selectedEntriesCount == 1 && isDir))
0490                                    && (m_model->filesToMove.count() > 0 || m_model->filesToCopy.count() > 0));
0491 
0492     m_searchAction->setEnabled(!isBusy() && m_model->rowCount() > 0);
0493 
0494     m_commentView->setEnabled(!isBusy());
0495     m_commentMsgWidget->setEnabled(!isBusy());
0496 
0497     m_editCommentAction->setEnabled(false);
0498     m_testArchiveAction->setEnabled(false);
0499 
0500     if (m_model->archive()) {
0501         const KPluginMetaData metadata = PluginManager().preferredPluginFor(m_model->archive()->mimeType())->metaData();
0502         bool supportsWriteComment = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsWriteComment();
0503         m_editCommentAction->setEnabled(!isBusy() && supportsWriteComment);
0504         m_commentView->setReadOnly(!supportsWriteComment);
0505         m_editCommentAction->setText(m_model->archive()->hasComment() ? i18nc("@action:inmenu mutually exclusive with Add &Comment", "Edit &Comment")
0506                                                                       : i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment"));
0507 
0508         bool supportsTesting = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsTesting();
0509         m_testArchiveAction->setEnabled(!isBusy() && supportsTesting && !isEncryptedButUnknownPassword);
0510     } else {
0511         m_commentView->setReadOnly(true);
0512         m_editCommentAction->setText(i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment"));
0513     }
0514 }
0515 
0516 void Part::slotShowComment()
0517 {
0518     if (!m_commentBox->isVisible()) {
0519         m_commentBox->show();
0520         m_commentSplitter->setSizes(QList<int>() << static_cast<int>(m_view->height() * 0.6) << 1);
0521     }
0522     m_commentView->setFocus();
0523 }
0524 
0525 void Part::slotAddComment()
0526 {
0527     CommentJob *job = m_model->archive()->addComment(m_commentView->toPlainText());
0528     if (!job) {
0529         return;
0530     }
0531     registerJob(job);
0532     job->start();
0533     m_commentMsgWidget->hide();
0534     if (m_commentView->toPlainText().isEmpty()) {
0535         m_commentBox->hide();
0536     }
0537 }
0538 
0539 void Part::slotTestArchive()
0540 {
0541     TestJob *job = m_model->archive()->testArchive();
0542     if (!job) {
0543         return;
0544     }
0545     registerJob(job);
0546     connect(job, &KJob::result, this, &Part::slotTestingDone);
0547     job->start();
0548 }
0549 
0550 bool Part::isArchiveWritable() const
0551 {
0552     return isReadWrite() && m_model->archive() && !m_model->archive()->isReadOnly();
0553 }
0554 
0555 bool Part::isCreatingNewArchive() const
0556 {
0557     return arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true");
0558 }
0559 
0560 void Part::createArchive()
0561 {
0562     const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")];
0563     m_model->createEmptyArchive(localFilePath(), fixedMimeType, m_model);
0564 
0565     if (arguments().metaData().contains(QStringLiteral("volumeSize"))) {
0566         m_model->archive()->setMultiVolume(true);
0567     }
0568 
0569     const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")];
0570     if (!password.isEmpty()) {
0571         m_model->encryptArchive(password, arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true"));
0572     }
0573 }
0574 
0575 void Part::loadArchive()
0576 {
0577     const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")];
0578     auto job = m_model->loadArchive(localFilePath(), fixedMimeType, m_model);
0579 
0580     if (job) {
0581         registerJob(job);
0582         job->start();
0583     } else {
0584         updateActions();
0585     }
0586 }
0587 
0588 void Part::resetArchive()
0589 {
0590     m_view->setDropsEnabled(false);
0591     m_model->reset();
0592     closeUrl();
0593     setFileNameFromArchive();
0594     updateActions();
0595 }
0596 
0597 void Part::resetGui()
0598 {
0599     m_messageWidget->hide();
0600     m_commentView->clear();
0601     m_commentBox->hide();
0602     m_infoPanel->updateWithDefaults();
0603     // Also reset format-specific compression options.
0604     m_compressionOptions = CompressionOptions();
0605 }
0606 
0607 void Part::slotTestingDone(KJob *job)
0608 {
0609     if (job->error() && job->error() != KJob::KilledJobError) {
0610         KMessageBox::error(widget(), job->errorString());
0611     } else if (static_cast<TestJob *>(job)->testSucceeded()) {
0612         KMessageBox::information(widget(), i18n("The archive passed the integrity test."), i18n("Test Results"));
0613     } else {
0614         KMessageBox::error(widget(), i18n("The archive failed the integrity test."), i18n("Test Results"));
0615     }
0616 }
0617 
0618 void Part::updateQuickExtractMenu(QAction *extractAction)
0619 {
0620     if (!extractAction) {
0621         return;
0622     }
0623 
0624     QMenu *menu = extractAction->menu();
0625 
0626     if (!menu) {
0627         menu = new QMenu();
0628         extractAction->setMenu(menu);
0629         connect(menu, &QMenu::triggered, this, &Part::slotQuickExtractFiles);
0630 
0631         // Remember to keep this action's properties as similar to
0632         // extractAction's as possible (except where it does not make
0633         // sense, such as the text or the shortcut).
0634         QAction *extractTo = menu->addAction(i18n("Extract To..."));
0635         extractTo->setIcon(extractAction->icon());
0636         extractTo->setToolTip(extractAction->toolTip());
0637 
0638         if (extractAction == m_extractArchiveAction) {
0639             connect(extractTo, &QAction::triggered, this, &Part::slotExtractArchive);
0640         } else {
0641             connect(extractTo, &QAction::triggered, this, &Part::slotShowExtractionDialog);
0642         }
0643 
0644         menu->addSeparator();
0645 
0646         QAction *header = menu->addAction(i18n("Quick Extract To..."));
0647         header->setEnabled(false);
0648         header->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract")));
0649     }
0650 
0651     while (menu->actions().size() > 3) {
0652         menu->removeAction(menu->actions().constLast());
0653     }
0654 
0655     const KConfigGroup conf(KSharedConfig::openConfig(), QStringLiteral("ExtractDialog"));
0656     const QStringList dirHistory = conf.readPathEntry("DirHistory", QStringList());
0657 
0658     for (int i = 0; i < qMin(10, dirHistory.size()); ++i) {
0659         const QString dir = QUrl(dirHistory.value(i)).toString(QUrl::RemoveScheme | QUrl::NormalizePathSegments | QUrl::PreferLocalFile);
0660         if (QDir(dir).exists()) {
0661             QAction *newAction = menu->addAction(dir);
0662             newAction->setData(dir);
0663         }
0664     }
0665 }
0666 
0667 void Part::slotQuickExtractFiles(QAction *triggeredAction)
0668 {
0669     // #190507: triggeredAction->data.isNull() means it's the "Extract to..."
0670     //          action, and we do not want it to run here
0671     if (!triggeredAction->data().isNull()) {
0672         QString userDestination = triggeredAction->data().toString();
0673         QString finalDestinationDirectory;
0674         const QString detectedSubfolder = detectSubfolder();
0675         qCDebug(ARK) << "Detected subfolder" << detectedSubfolder;
0676 
0677         if (m_model->archive()->hasMultipleTopLevelEntries()) {
0678             if (!userDestination.endsWith(QDir::separator())) {
0679                 userDestination.append(QDir::separator());
0680             }
0681             finalDestinationDirectory = userDestination + detectedSubfolder;
0682             QDir(userDestination).mkdir(detectedSubfolder);
0683         } else {
0684             finalDestinationDirectory = userDestination;
0685         }
0686 
0687         qCDebug(ARK) << "Extracting to:" << finalDestinationDirectory;
0688 
0689         ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(getSelectedIndexes())), finalDestinationDirectory, ExtractionOptions());
0690         registerJob(job);
0691 
0692         connect(job, &KJob::result, this, &Part::slotExtractionDone);
0693 
0694         job->start();
0695     }
0696 }
0697 
0698 void Part::selectionChanged()
0699 {
0700     m_infoPanel->setIndexes(getSelectedIndexes());
0701 }
0702 
0703 QModelIndexList Part::getSelectedIndexes()
0704 {
0705     QModelIndexList list;
0706     const auto selectedRows = m_view->selectionModel()->selectedRows();
0707     for (const QModelIndex &i : selectedRows) {
0708         list.append(m_filterModel->mapToSource(i));
0709     }
0710     return list;
0711 }
0712 
0713 void Part::readCompressionOptions()
0714 {
0715     // Store options from CreateDialog if they are set.
0716     if (!m_compressionOptions.isCompressionLevelSet() && arguments().metaData().contains(QStringLiteral("compressionLevel"))) {
0717         m_compressionOptions.setCompressionLevel(arguments().metaData()[QStringLiteral("compressionLevel")].toInt());
0718     }
0719     if (m_compressionOptions.compressionMethod().isEmpty() && arguments().metaData().contains(QStringLiteral("compressionMethod"))) {
0720         m_compressionOptions.setCompressionMethod(arguments().metaData()[QStringLiteral("compressionMethod")]);
0721     }
0722     if (m_compressionOptions.encryptionMethod().isEmpty() && arguments().metaData().contains(QStringLiteral("encryptionMethod"))) {
0723         m_compressionOptions.setEncryptionMethod(arguments().metaData()[QStringLiteral("encryptionMethod")]);
0724     }
0725     if (!m_compressionOptions.isVolumeSizeSet() && arguments().metaData().contains(QStringLiteral("volumeSize"))) {
0726         m_compressionOptions.setVolumeSize(arguments().metaData()[QStringLiteral("volumeSize")].toULong());
0727     }
0728 
0729     const auto compressionMethods = m_model->archive()->property("compressionMethods").toStringList();
0730     qCDebug(ARK) << "compmethods:" << compressionMethods;
0731     if (compressionMethods.size() == 1) {
0732         m_compressionOptions.setCompressionMethod(compressionMethods.first());
0733     }
0734 }
0735 
0736 bool Part::openFile()
0737 {
0738     qCDebug(ARK) << "Attempting to open archive" << localFilePath();
0739 
0740     resetGui();
0741 
0742     if (!isLocalFileValid()) {
0743         return false;
0744     }
0745 
0746     if (isCreatingNewArchive()) {
0747         createArchive();
0748         Q_EMIT ready();
0749         return true;
0750     }
0751 
0752     loadArchive();
0753     // Loading is async, we don't know yet whether we got a valid archive.
0754     return false;
0755 }
0756 
0757 bool Part::saveFile()
0758 {
0759     return true;
0760 }
0761 
0762 bool Part::isBusy() const
0763 {
0764     return m_busy;
0765 }
0766 
0767 KConfigSkeleton *Part::config() const
0768 {
0769     return ArkSettings::self();
0770 }
0771 
0772 QList<Kerfuffle::SettingsPage *> Part::settingsPages(QWidget *parent) const
0773 {
0774     QList<SettingsPage *> pages;
0775     pages.append(new GeneralSettingsPage(parent, i18nc("@title:tab", "General"), QStringLiteral("utilities-file-archiver")));
0776     pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction"), QStringLiteral("preferences-desktop-icons")));
0777     pages.append(new PluginSettingsPage(parent, i18nc("@title:tab", "Plugins"), QStringLiteral("preferences-plugin")));
0778     pages.append(new PreviewSettingsPage(parent, i18nc("@title:tab", "Previews"), QStringLiteral("image-jpeg")));
0779 
0780     return pages;
0781 }
0782 
0783 QWidget *Part::infoPanel() const
0784 {
0785     return m_infoPanel;
0786 }
0787 
0788 bool Part::isLocalFileValid()
0789 {
0790     const QString localFile = localFilePath();
0791     const QFileInfo localFileInfo(localFile);
0792 
0793     if (localFileInfo.isDir()) {
0794         displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "<filename>%1</filename> is a directory.", localFile));
0795         return false;
0796     }
0797 
0798     if (isCreatingNewArchive()) {
0799         if (localFileInfo.exists()) {
0800             if (!confirmAndDelete(localFile)) {
0801                 displayMsgWidget(KMessageWidget::Error,
0802                                  xi18nc("@info", "Could not overwrite <filename>%1</filename>. Check whether you have write permission.", localFile));
0803                 return false;
0804             }
0805         }
0806 
0807         displayMsgWidget(KMessageWidget::Information,
0808                          xi18nc("@info", "The archive <filename>%1</filename> will be created as soon as you add a file.", localFile));
0809     } else {
0810         if (!localFileInfo.exists()) {
0811             displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive <filename>%1</filename> was not found.", localFile));
0812             return false;
0813         }
0814 
0815         if (!localFileInfo.isReadable()) {
0816             displayMsgWidget(KMessageWidget::Error,
0817                              xi18nc("@info", "The archive <filename>%1</filename> could not be loaded, as it was not possible to read from it.", localFile));
0818             return false;
0819         }
0820     }
0821 
0822     return true;
0823 }
0824 
0825 bool Part::confirmAndDelete(const QString &targetFile)
0826 {
0827     QFileInfo targetInfo(targetFile);
0828     const auto buttonCode = KMessageBox::warningTwoActions(
0829         widget(),
0830         xi18nc("@info", "The archive <filename>%1</filename> already exists. Do you wish to overwrite it?", targetInfo.fileName()),
0831         i18nc("@title:window", "File Exists"),
0832         KStandardGuiItem::overwrite(),
0833         KStandardGuiItem::cancel());
0834 
0835     if (buttonCode != KMessageBox::PrimaryAction || !targetInfo.isWritable()) {
0836         return false;
0837     }
0838 
0839     qCDebug(ARK) << "Removing file" << targetFile;
0840 
0841     return QFile(targetFile).remove();
0842 }
0843 
0844 void Part::slotCompleted()
0845 {
0846     if (isCreatingNewArchive()) {
0847         m_view->setDropsEnabled(true);
0848         updateActions();
0849         return;
0850     }
0851 
0852     // Existing archive, setup the view for it.
0853     m_view->sortByColumn(0, Qt::AscendingOrder);
0854     m_view->expandIfSingleFolder();
0855     m_view->header()->resizeSections(QHeaderView::ResizeToContents);
0856     m_view->setDropsEnabled(isArchiveWritable());
0857 
0858     if (!m_model->archive()->comment().isEmpty()) {
0859         m_commentView->setPlainText(m_model->archive()->comment());
0860         slotShowComment();
0861     } else {
0862         m_commentView->clear();
0863         m_commentBox->hide();
0864     }
0865 
0866     if (m_model->rowCount() == 0) {
0867         qCWarning(ARK) << "No entry listed by the plugin";
0868         displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "The archive is empty or Ark could not open its content."));
0869     } else if (m_model->rowCount() == 1 &&
0870                // TODO: drop application/x-cd-image once all distributions ship shared-mime-info >= 2.3
0871                (m_model->archive()->mimeType().inherits(QStringLiteral("application/x-cd-image"))
0872                 || m_model->archive()->mimeType().inherits(QStringLiteral("application/vnd.efi.img")))
0873                && m_model->entryForIndex(m_model->index(0, 0))->fullPath() == QLatin1String("README.TXT")) {
0874         qCWarning(ARK) << "Detected ISO image with UDF filesystem";
0875         displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "Ark does not currently support ISO files with UDF filesystem."));
0876     } else {
0877         m_model->countEntriesAndSize();
0878     }
0879 
0880     if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) {
0881         QTimer::singleShot(0, this, &Part::slotShowExtractionDialog);
0882     }
0883 
0884     updateActions();
0885 }
0886 
0887 void Part::slotLoadingStarted()
0888 {
0889     m_model->filesToMove.clear();
0890     m_model->filesToCopy.clear();
0891 }
0892 
0893 void Part::slotLoadingFinished(KJob *job)
0894 {
0895     if (!job->error()) {
0896         Q_EMIT completed();
0897         return;
0898     }
0899 
0900     // Loading failed or was canceled by the user (e.g. password dialog rejected).
0901     Q_EMIT canceled(job->errorString());
0902     resetArchive();
0903 
0904     if (job->error() != KJob::KilledJobError) {
0905         displayMsgWidget(KMessageWidget::Error,
0906                          xi18nc("@info",
0907                                 "Loading the archive <filename>%1</filename> failed with the following error:<nl/><message>%2</message>",
0908                                 localFilePath(),
0909                                 job->errorString()));
0910     }
0911 }
0912 
0913 void Part::setReadyGui()
0914 {
0915     QApplication::restoreOverrideCursor();
0916     m_busy = false;
0917 
0918     if (m_statusBarExtension->statusBar()) {
0919         m_statusBarExtension->statusBar()->hide();
0920     }
0921 
0922     m_view->setEnabled(true);
0923     updateActions();
0924 }
0925 
0926 void Part::setBusyGui()
0927 {
0928     QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
0929     m_busy = true;
0930 
0931     if (m_statusBarExtension->statusBar()) {
0932         m_statusBarExtension->statusBar()->show();
0933     }
0934 
0935     m_view->setEnabled(false);
0936     updateActions();
0937 }
0938 
0939 void Part::setFileNameFromArchive()
0940 {
0941     const QString prettyName = url().fileName();
0942 
0943     m_infoPanel->setPrettyFileName(prettyName);
0944     m_infoPanel->updateWithDefaults();
0945 
0946     Q_EMIT setWindowCaption(prettyName);
0947 }
0948 
0949 void Part::slotOpenEntry(int mode)
0950 {
0951     QModelIndex index = m_filterModel->mapToSource(m_view->selectionModel()->currentIndex());
0952     Archive::Entry *entry = m_model->entryForIndex(index);
0953 
0954     // Don't open directories.
0955     if (entry->isDir()) {
0956         return;
0957     }
0958 
0959     // Don't open files bigger than the size limit.
0960     const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024;
0961     if (ArkSettings::limitPreviewFileSize() && entry->property("size").toLongLong() >= maxPreviewSize) {
0962         return;
0963     }
0964 
0965     // We don't support opening symlinks.
0966     if (!entry->property("link").toString().isEmpty()) {
0967         displayMsgWidget(KMessageWidget::Information, i18n("Ark cannot open symlinks."));
0968         return;
0969     }
0970 
0971     // Extract the entry.
0972     if (!entry->fullPath().isEmpty()) {
0973         qCDebug(ARK) << "Opening with mode" << mode;
0974         m_openFileMode = static_cast<OpenFileMode>(mode);
0975         KJob *job = nullptr;
0976 
0977         if (m_openFileMode == Preview) {
0978             job = m_model->preview(entry);
0979             connect(job, &KJob::result, this, &Part::slotPreviewExtractedEntry);
0980         } else {
0981             job = (m_openFileMode == OpenFile) ? m_model->open(entry) : m_model->openWith(entry);
0982             connect(job, &KJob::result, this, &Part::slotOpenExtractedEntry);
0983         }
0984 
0985         registerJob(job);
0986         job->start();
0987     }
0988 }
0989 
0990 void Part::slotOpenExtractedEntry(KJob *job)
0991 {
0992     if (!job->error()) {
0993         OpenJob *openJob = qobject_cast<OpenJob *>(job);
0994         Q_ASSERT(openJob);
0995 
0996         // Since the user could modify the file (unlike the Preview case),
0997         // we'll need to manually delete the temp dir in the Part destructor.
0998         m_tmpExtractDirList << openJob->tempDir();
0999 
1000         const QString fullName = openJob->validatedFilePath();
1001         if (isArchiveWritable()) {
1002             m_fileWatcher.reset(new QFileSystemWatcher);
1003             connect(m_fileWatcher.get(), &QFileSystemWatcher::fileChanged, this, &Part::slotResetFileChangeTimer);
1004 
1005             m_fileWatcher->addPath(fullName);
1006         } else {
1007             // If archive is readonly set temporarily extracted file to readonly as
1008             // well so user will be notified if trying to modify and save the file.
1009             QFile::setPermissions(fullName, QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther);
1010         }
1011 
1012         const QUrl url = QUrl::fromUserInput(fullName, QDir::currentPath(), QUrl::AssumeLocalFile);
1013         if (qobject_cast<OpenWithJob *>(job)) {
1014             // Constructing an ApplicationLauncherJob without an argument will
1015             // trigger the openWith dialog
1016             KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob();
1017             job->setUrls({url});
1018             job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, widget()));
1019             job->start();
1020         } else {
1021             KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url);
1022             job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, widget()));
1023             job->start();
1024         }
1025     } else if (job->error() != KJob::KilledJobError) {
1026         KMessageBox::error(widget(), job->errorString());
1027     }
1028     setReadyGui();
1029 }
1030 
1031 void Part::slotPreviewExtractedEntry(KJob *job)
1032 {
1033     if (!job->error()) {
1034         PreviewJob *previewJob = qobject_cast<PreviewJob *>(job);
1035         Q_ASSERT(previewJob);
1036 
1037         m_tmpExtractDirList << previewJob->tempDir();
1038         // Use displayName to detect the mimetype, otherwise with single-file archives with fake 'data' entry the detected mime would be the default one.
1039         QMimeType mimeType = QMimeDatabase().mimeTypeForFile(previewJob->entry()->displayName());
1040         if (previewJob->entry()->displayName() != previewJob->entry()->name()) {
1041             ArkViewer::view(previewJob->validatedFilePath(), previewJob->entry()->displayName(), mimeType);
1042         } else {
1043             ArkViewer::view(previewJob->validatedFilePath(), previewJob->entry()->fullPath(PathFormat::NoTrailingSlash), mimeType);
1044         }
1045 
1046     } else if (job->error() != KJob::KilledJobError) {
1047         KMessageBox::error(widget(), job->errorString());
1048     }
1049     setReadyGui();
1050 }
1051 
1052 void Part::slotResetFileChangeTimer(const QString &file)
1053 {
1054     const bool timerActive = m_watchedFileChangeTimer.isActive();
1055     m_watchedFileChangeTimer.stop();
1056     // Check if a different file was changed while monitoring the previous file.
1057     if (timerActive && !m_lastChangedFilename.isEmpty() && file != m_lastChangedFilename) {
1058         const QString prevFile = m_lastChangedFilename;
1059         QTimer::singleShot(0, this, [this, prevFile]() {
1060             slotWatchedFileModified(prevFile);
1061         });
1062     }
1063 
1064     m_lastChangedFilename = file;
1065     m_watchedFileChangeTimer.start();
1066 }
1067 
1068 void Part::slotWatchedFileModified(const QString &file)
1069 {
1070     qCDebug(ARK) << "Watched file modified:" << file;
1071 
1072     // Find the relative path of the file within the archive.
1073     QString relPath = file;
1074     for (QTemporaryDir *tmpDir : std::as_const(m_tmpExtractDirList)) {
1075         relPath.remove(tmpDir->path()); // Remove tmpDir.
1076     }
1077     relPath.remove(0, 1); // Remove leading slash.
1078     if (relPath.contains(QLatin1Char('/'))) {
1079         relPath = relPath.section(QLatin1Char('/'), 0, -2); // Remove filename.
1080     } else {
1081         // File is in the root of the archive, no path.
1082         relPath = QString();
1083     }
1084 
1085     // Set up a string for display in KMessageBox.
1086     QString prettyFilename;
1087     if (relPath.isEmpty()) {
1088         prettyFilename = file.section(QLatin1Char('/'), -1);
1089     } else {
1090         prettyFilename = relPath + QLatin1Char('/') + file.section(QLatin1Char('/'), -1);
1091     }
1092 
1093     if (KMessageBox::questionTwoActions(widget(),
1094                                         xi18n("The file <filename>%1</filename> was modified. Do you want to update the archive?", prettyFilename),
1095                                         i18nc("@title:window", "File Modified"),
1096                                         KGuiItem(i18nc("@action:button", "Update"), QStringLiteral("view-refresh")),
1097                                         KGuiItem(i18nc("@action:button", "Ignore"), QStringLiteral("dialog-cancel")))
1098         == KMessageBox::PrimaryAction) {
1099         QStringList list = QStringList() << file;
1100 
1101         qCDebug(ARK) << "Updating file" << file << "with path" << relPath;
1102         slotAddFiles(list, nullptr, relPath, DoNotShowOverwriteDialog);
1103     }
1104     // This is needed because some apps, such as Kate, delete and recreate
1105     // files when saving.
1106     m_fileWatcher->addPath(file);
1107 }
1108 
1109 void Part::slotError(const QString &errorMessage, const QString &details)
1110 {
1111     if (details.isEmpty()) {
1112         KMessageBox::error(widget(), errorMessage);
1113     } else {
1114         KMessageBox::detailedError(widget(), errorMessage, details);
1115     }
1116 }
1117 
1118 QString Part::detectSubfolder() const
1119 {
1120     if (!m_model) {
1121         return QString();
1122     }
1123 
1124     return m_model->archive()->subfolderName();
1125 }
1126 
1127 void Part::slotExtractArchive()
1128 {
1129     if (m_view->selectionModel()->selectedRows().count() > 0) {
1130         m_view->selectionModel()->clear();
1131     }
1132 
1133     slotShowExtractionDialog();
1134 }
1135 
1136 void Part::slotShowExtractionDialog()
1137 {
1138     if (!m_model) {
1139         return;
1140     }
1141 
1142     QPointer<Kerfuffle::ExtractionDialog> dialog(new Kerfuffle::ExtractionDialog(widget()));
1143 
1144     dialog.data()->setModal(true);
1145 
1146     if (m_view->selectionModel()->selectedRows().count() > 0) {
1147         dialog.data()->setShowSelectedFiles(true);
1148     }
1149 
1150     dialog.data()->setExtractToSubfolder(m_model->archive()->hasMultipleTopLevelEntries());
1151     dialog.data()->setSubfolder(detectSubfolder());
1152 
1153     dialog.data()->setCurrentUrl(QUrl::fromLocalFile(QFileInfo(m_model->archive()->fileName()).absolutePath()));
1154 
1155     dialog.data()->show();
1156     dialog.data()->restoreWindowSize();
1157 
1158     if (dialog.data()->exec()) {
1159         updateQuickExtractMenu(m_extractArchiveAction);
1160         updateQuickExtractMenu(m_extractAction);
1161 
1162         QVector<Archive::Entry *> files;
1163 
1164         // If the user has chosen to extract only selected entries, fetch these
1165         // from the QTreeView.
1166         if (!dialog.data()->extractAllFiles()) {
1167             files = filesAndRootNodesForIndexes(addChildren(getSelectedIndexes()));
1168         }
1169 
1170         qCDebug(ARK) << "Selected " << files;
1171 
1172         Kerfuffle::ExtractionOptions options;
1173         options.setPreservePaths(dialog->preservePaths());
1174 
1175         const QString destinationDirectory = dialog.data()->destinationDirectory().toLocalFile();
1176         ExtractJob *job = m_model->extractFiles(files, destinationDirectory, options);
1177         registerJob(job);
1178 
1179         connect(job, &KJob::result, this, &Part::slotExtractionDone);
1180 
1181         job->start();
1182     }
1183 
1184     delete dialog.data();
1185 }
1186 
1187 QModelIndexList Part::addChildren(const QModelIndexList &list) const
1188 {
1189     Q_ASSERT(m_model);
1190 
1191     QModelIndexList ret = list;
1192 
1193     // Iterate over indexes in list and add all children.
1194     for (int i = 0; i < ret.size(); ++i) {
1195         QModelIndex index = ret.at(i);
1196 
1197         for (int j = 0; j < m_model->rowCount(index); ++j) {
1198             QModelIndex child = m_model->index(j, 0, index);
1199             if (!ret.contains(child)) {
1200                 ret << child;
1201             }
1202         }
1203     }
1204 
1205     return ret;
1206 }
1207 
1208 QVector<Archive::Entry *> Part::filesForIndexes(const QModelIndexList &list) const
1209 {
1210     QVector<Archive::Entry *> ret;
1211 
1212     for (const QModelIndex &index : list) {
1213         ret << m_model->entryForIndex(index);
1214     }
1215 
1216     return ret;
1217 }
1218 
1219 QVector<Kerfuffle::Archive::Entry *> Part::filesAndRootNodesForIndexes(const QModelIndexList &list) const
1220 {
1221     QVector<Kerfuffle::Archive::Entry *> fileList;
1222     QStringList fullPathsList;
1223 
1224     for (const QModelIndex &index : list) {
1225         // Find the topmost unselected parent. This is done by iterating up
1226         // through the directory hierarchy and see if each parent is included
1227         // in the selection OR if the parent is already part of list.
1228         // The latter is needed for unselected folders which are subfolders of
1229         // a selected parent folder.
1230         QModelIndex selectionRoot = index.parent();
1231         while (m_view->selectionModel()->isSelected(selectionRoot) || list.contains(selectionRoot)) {
1232             selectionRoot = selectionRoot.parent();
1233         }
1234 
1235         // Fetch the root node for the unselected parent.
1236         const QString rootFileName = selectionRoot.isValid() ? m_model->entryForIndex(selectionRoot)->fullPath() : QString();
1237 
1238         // Append index with root node to fileList.
1239         QModelIndexList alist = QModelIndexList() << index;
1240         const auto filesIndexes = filesForIndexes(alist);
1241         for (Archive::Entry *entry : filesIndexes) {
1242             const QString fullPath = entry->fullPath();
1243             if (!fullPathsList.contains(fullPath)) {
1244                 entry->rootNode = rootFileName;
1245                 fileList.append(entry);
1246                 fullPathsList.append(fullPath);
1247             }
1248         }
1249     }
1250     return fileList;
1251 }
1252 
1253 void Part::slotExtractionDone(KJob *job)
1254 {
1255     if (job->error() && job->error() != KJob::KilledJobError) {
1256         KMessageBox::error(widget(), job->errorString());
1257     } else {
1258         ExtractJob *extractJob = qobject_cast<ExtractJob *>(job);
1259         Q_ASSERT(extractJob);
1260 
1261         if (ArkSettings::openDestinationFolderAfterExtraction()) {
1262             qCDebug(ARK) << "Shall open" << extractJob->destinationDirectory();
1263             QUrl destinationDirectory = QUrl::fromLocalFile(extractJob->destinationDirectory()).adjusted(QUrl::NormalizePathSegments);
1264             qCDebug(ARK) << "Shall open URL" << destinationDirectory;
1265 
1266             KIO::OpenUrlJob *job = new KIO::OpenUrlJob(destinationDirectory, QStringLiteral("inode/directory"));
1267             job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, widget()));
1268             job->start();
1269         }
1270 
1271         if (ArkSettings::closeAfterExtraction()) {
1272             Q_EMIT quit();
1273         }
1274     }
1275 }
1276 
1277 void Part::slotAddFiles(const QStringList &filesToAdd, const Archive::Entry *destination, const QString &relPath, OverwriteBehaviour onOverwrite)
1278 {
1279     if (!m_model->archive() || filesToAdd.isEmpty()) {
1280         return;
1281     }
1282 
1283     QStringList withChildPaths;
1284     for (const QString &file : filesToAdd) {
1285         m_jobTempEntries.push_back(new Archive::Entry(nullptr, file));
1286         if (QFileInfo(file).isDir()) {
1287             withChildPaths << file + QLatin1Char('/');
1288             QDirIterator it(file, QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
1289             while (it.hasNext()) {
1290                 QString path = it.next();
1291                 if (it.fileInfo().isDir()) {
1292                     path += QLatin1Char('/');
1293                 }
1294                 withChildPaths << path;
1295             }
1296         } else {
1297             withChildPaths << file;
1298         }
1299     }
1300 
1301     withChildPaths = ReadOnlyArchiveInterface::entryPathsFromDestination(withChildPaths, destination, 0);
1302     QList<const Archive::Entry *> conflictingEntries;
1303     bool error = m_model->conflictingEntries(conflictingEntries, withChildPaths, true);
1304 
1305     if (onOverwrite == ShowOverwriteDialog && conflictingEntries.count() > 0) {
1306         QPointer<OverwriteDialog> overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, error);
1307         int ret = overwriteDialog->exec();
1308         delete overwriteDialog;
1309         if (ret == QDialog::Rejected) {
1310             qDeleteAll(m_jobTempEntries);
1311             m_jobTempEntries.clear();
1312             return;
1313         }
1314     }
1315 
1316     // GlobalWorkDir is used by AddJob and should contain the part of the
1317     // absolute path of files to be added that should NOT be included in the
1318     // directory structure within the archive.
1319     // Example: We add file "/home/user/somedir/somefile.txt" and want the file
1320     // to have the relative path within the archive "somedir/somefile.txt".
1321     // GlobalWorkDir is then: "/home/user"
1322     QString globalWorkDir = filesToAdd.first();
1323 
1324     // path represents the path of the file within the archive. This needs to
1325     // be removed from globalWorkDir, otherwise the files will be added to the
1326     // root of the archive. In the example above, path would be "somedir/".
1327     if (!relPath.isEmpty()) {
1328         globalWorkDir.remove(relPath);
1329         qCDebug(ARK) << "Adding" << filesToAdd << "to" << relPath;
1330     } else {
1331         qCDebug(ARK) << "Adding " << filesToAdd << ((destination == nullptr) ? QString() : QLatin1String("to ") + destination->fullPath());
1332     }
1333 
1334     // Remove trailing slash (needed when adding dirs).
1335     if (globalWorkDir.right(1) == QLatin1String("/")) {
1336         globalWorkDir.chop(1);
1337     }
1338 
1339     // We need to override the global options with a working directory.
1340     CompressionOptions compOptions = m_compressionOptions;
1341 
1342     // Now take the absolute path of the parent directory.
1343     globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath();
1344 
1345     qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir;
1346     compOptions.setGlobalWorkDir(globalWorkDir);
1347 
1348     AddJob *job = m_model->addFiles(m_jobTempEntries, destination, compOptions);
1349     if (!job) {
1350         qDeleteAll(m_jobTempEntries);
1351         m_jobTempEntries.clear();
1352         return;
1353     }
1354 
1355     connect(job, &KJob::result, this, &Part::slotAddFilesDone);
1356     registerJob(job);
1357     job->start();
1358 }
1359 
1360 void Part::slotDroppedFiles(const QStringList &files, const Archive::Entry *destination)
1361 {
1362     readCompressionOptions();
1363     slotAddFiles(files, destination, QString());
1364 }
1365 
1366 void Part::slotAddFiles()
1367 {
1368     readCompressionOptions();
1369 
1370     QString dialogTitle = i18nc("@title:window", "Add Files");
1371     const Archive::Entry *destination = nullptr;
1372     if (m_view->selectionModel()->selectedRows().count() == 1) {
1373         destination = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex()));
1374         if (destination->isDir()) {
1375             dialogTitle = i18nc("@title:window", "Add Files to %1", destination->fullPath());
1376         } else {
1377             destination = nullptr;
1378         }
1379     }
1380 
1381     qCDebug(ARK) << "Opening AddDialog with opts:" << m_compressionOptions;
1382 
1383     // #264819: passing widget() as the parent will not work as expected.
1384     //          KFileDialog will create a KFileWidget, which runs an internal
1385     //          event loop to stat the given directory. This, in turn, leads to
1386     //          events being delivered to widget(), which is a QSplitter, which
1387     //          in turn reimplements childEvent() and will end up calling
1388     //          QWidget::show() on the KFileDialog (thus showing it in a
1389     //          non-modal state).
1390     //          When KFileDialog::exec() is called, the widget is already shown
1391     //          and nothing happens.
1392 
1393     QPointer<AddDialog> dlg = new AddDialog(widget(), dialogTitle, m_lastUsedAddPath, m_model->archive()->mimeType(), m_compressionOptions);
1394 
1395     if (dlg->exec() == QDialog::Accepted) {
1396         qCDebug(ARK) << "Selected files:" << dlg->selectedFiles();
1397         qCDebug(ARK) << "Options:" << dlg->compressionOptions();
1398         m_compressionOptions = dlg->compressionOptions();
1399         slotAddFiles(dlg->selectedFiles(), destination, QString());
1400     }
1401     delete dlg;
1402 }
1403 
1404 void Part::slotCutFiles()
1405 {
1406     QModelIndexList selectedRows = addChildren(getSelectedIndexes());
1407     m_model->filesToMove = ArchiveModel::entryMap(filesForIndexes(selectedRows));
1408     qCDebug(ARK) << "Entries marked to cut:" << m_model->filesToMove.values();
1409     m_model->filesToCopy.clear();
1410     for (const QModelIndex &row : std::as_const(m_cutIndexes)) {
1411         m_view->dataChanged(row, row);
1412     }
1413     m_cutIndexes = selectedRows;
1414     for (const QModelIndex &row : std::as_const(m_cutIndexes)) {
1415         m_view->dataChanged(row, row);
1416     }
1417     updateActions();
1418 }
1419 
1420 void Part::slotCopyFiles()
1421 {
1422     m_model->filesToCopy = ArchiveModel::entryMap(filesForIndexes(addChildren(getSelectedIndexes())));
1423     qCDebug(ARK) << "Entries marked to copy:" << m_model->filesToCopy.values();
1424     for (const QModelIndex &row : std::as_const(m_cutIndexes)) {
1425         m_view->dataChanged(row, row);
1426     }
1427     m_cutIndexes.clear();
1428     m_model->filesToMove.clear();
1429     updateActions();
1430 }
1431 
1432 void Part::slotRenameFile(const QString &name)
1433 {
1434     if (name == QLatin1Char('.') || name == QLatin1String("..") || name.contains(QLatin1Char('/'))) {
1435         displayMsgWidget(KMessageWidget::Error, i18n("Filename can't contain slashes and can't be equal to \".\" or \"..\""));
1436         return;
1437     }
1438     const Archive::Entry *entry = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex()));
1439     QVector<Archive::Entry *> entriesToMove = filesForIndexes(addChildren(getSelectedIndexes()));
1440 
1441     m_destination = new Archive::Entry();
1442     const QString &entryPath = entry->fullPath(NoTrailingSlash);
1443     const QString rootPath = entryPath.left(entryPath.length() - entry->name().length());
1444     QString path = rootPath + name;
1445     if (entry->isDir()) {
1446         path += QLatin1Char('/');
1447     }
1448     m_destination->setFullPath(path);
1449 
1450     slotPasteFiles(entriesToMove, m_destination, 1);
1451 }
1452 
1453 void Part::slotPasteFiles()
1454 {
1455     m_destination = (m_view->selectionModel()->selectedRows().count() > 0)
1456         ? m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex()))
1457         : nullptr;
1458     if (m_destination == nullptr) {
1459         m_destination = new Archive::Entry(nullptr, QString());
1460     } else {
1461         m_destination = new Archive::Entry(nullptr, m_destination->fullPath());
1462     }
1463 
1464     if (m_model->filesToMove.count() > 0) {
1465         // Changing destination to include new entry path if pasting only 1 entry.
1466         QVector<Archive::Entry *> entriesWithoutChildren =
1467             ReadOnlyArchiveInterface::entriesWithoutChildren(QVector<Archive::Entry *>::fromList(m_model->filesToMove.values()));
1468         if (entriesWithoutChildren.count() == 1) {
1469             const Archive::Entry *entry = entriesWithoutChildren.first();
1470             auto entryName = entry->name();
1471             if (entry->isDir()) {
1472                 entryName += QLatin1Char('/');
1473             }
1474             m_destination->setFullPath(m_destination->fullPath() + entryName);
1475         }
1476 
1477         for (const Archive::Entry *entry : std::as_const(entriesWithoutChildren)) {
1478             if (entry->isDir() && m_destination->fullPath().startsWith(entry->fullPath())) {
1479                 KMessageBox::error(widget(), i18n("Folders can't be moved into themselves."), i18n("Moving a folder into itself"));
1480                 delete m_destination;
1481                 return;
1482             }
1483         }
1484         auto entryList = QVector<Archive::Entry *>::fromList(m_model->filesToMove.values());
1485         slotPasteFiles(entryList, m_destination, entriesWithoutChildren.count());
1486         m_model->filesToMove.clear();
1487     } else {
1488         auto entryList = QVector<Archive::Entry *>::fromList(m_model->filesToCopy.values());
1489         slotPasteFiles(entryList, m_destination, 0);
1490         m_model->filesToCopy.clear();
1491     }
1492     m_cutIndexes.clear();
1493     updateActions();
1494 }
1495 
1496 void Part::slotPasteFiles(QVector<Kerfuffle::Archive::Entry *> &files, Kerfuffle::Archive::Entry *destination, int entriesWithoutChildren)
1497 {
1498     if (files.isEmpty()) {
1499         delete m_destination;
1500         return;
1501     }
1502 
1503     QStringList filesPaths = ReadOnlyArchiveInterface::entryFullPaths(files);
1504     QStringList newPaths = ReadOnlyArchiveInterface::entryPathsFromDestination(filesPaths, destination, entriesWithoutChildren);
1505 
1506     if (ArchiveModel::hasDuplicatedEntries(newPaths)) {
1507         displayMsgWidget(KMessageWidget::Error, i18n("Entries with the same names can't be pasted to the same destination."));
1508         delete m_destination;
1509         return;
1510     }
1511 
1512     QList<const Archive::Entry *> conflictingEntries;
1513     bool error = m_model->conflictingEntries(conflictingEntries, newPaths, false);
1514 
1515     if (conflictingEntries.count() != 0) {
1516         QPointer<OverwriteDialog> overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, error);
1517         int ret = overwriteDialog->exec();
1518         delete overwriteDialog;
1519         if (ret == QDialog::Rejected) {
1520             delete m_destination;
1521             return;
1522         }
1523     }
1524 
1525     if (entriesWithoutChildren > 0) {
1526         qCDebug(ARK) << "Moving" << files << "to" << destination;
1527     } else {
1528         qCDebug(ARK) << "Copying " << files << "to" << destination;
1529     }
1530 
1531     KJob *job;
1532     if (entriesWithoutChildren != 0) {
1533         job = m_model->moveFiles(files, destination, CompressionOptions());
1534     } else {
1535         job = m_model->copyFiles(files, destination, CompressionOptions());
1536     }
1537 
1538     if (job) {
1539         connect(job, &KJob::result, this, &Part::slotPasteFilesDone);
1540         registerJob(job);
1541         job->start();
1542     } else {
1543         delete m_destination;
1544     }
1545 }
1546 
1547 void Part::slotAddFilesDone(KJob *job)
1548 {
1549     qDeleteAll(m_jobTempEntries);
1550     m_jobTempEntries.clear();
1551     m_messageWidget->hide();
1552     if (job->error()) {
1553         if (job->error() != KJob::KilledJobError) {
1554             KMessageBox::error(widget(), job->errorString());
1555         } else if (isCreatingNewArchive()) {
1556             resetArchive();
1557         }
1558     } else {
1559         // For multi-volume archive, we need to re-open the archive after adding files
1560         // because the name changes from e.g name.rar to name.part1.rar.
1561         if (m_model->archive()->isMultiVolume()) {
1562             qCDebug(ARK) << "Multi-volume archive detected, re-opening...";
1563             KParts::OpenUrlArguments args = arguments();
1564             args.metaData()[QStringLiteral("createNewArchive")] = QStringLiteral("false");
1565             setArguments(args);
1566 
1567             openUrl(QUrl::fromLocalFile(m_model->archive()->multiVolumeName()));
1568         } else {
1569             m_model->countEntriesAndSize();
1570         }
1571     }
1572     m_cutIndexes.clear();
1573     m_model->filesToMove.clear();
1574     m_model->filesToCopy.clear();
1575 }
1576 
1577 void Part::slotPasteFilesDone(KJob *job)
1578 {
1579     if (job->error() && job->error() != KJob::KilledJobError) {
1580         KMessageBox::error(widget(), job->errorString());
1581     } else {
1582         m_model->countEntriesAndSize();
1583     }
1584     m_cutIndexes.clear();
1585     m_model->filesToMove.clear();
1586     m_model->filesToCopy.clear();
1587 }
1588 
1589 void Part::slotDeleteFilesDone(KJob *job)
1590 {
1591     if (job->error() && job->error() != KJob::KilledJobError) {
1592         KMessageBox::error(widget(), job->errorString());
1593     } else {
1594         m_model->countEntriesAndSize();
1595     }
1596     m_cutIndexes.clear();
1597     m_model->filesToMove.clear();
1598     m_model->filesToCopy.clear();
1599 }
1600 
1601 void Part::slotDeleteFiles()
1602 {
1603     const int selectionsCount = m_view->selectionModel()->selectedRows().count();
1604     const auto reallyDelete = KMessageBox::questionTwoActions(widget(),
1605                                                               i18ncp("@info",
1606                                                                      "Deleting this file is not undoable. Are you sure you want to do this?",
1607                                                                      "Deleting these files is not undoable. Are you sure you want to do this?",
1608                                                                      selectionsCount),
1609                                                               i18ncp("@title:window", "Delete File", "Delete Files", selectionsCount),
1610                                                               KStandardGuiItem::del(),
1611                                                               KStandardGuiItem::cancel(),
1612                                                               QString(),
1613                                                               KMessageBox::Dangerous | KMessageBox::Notify);
1614 
1615     if (reallyDelete == KMessageBox::SecondaryAction) {
1616         return;
1617     }
1618 
1619     DeleteJob *job = m_model->deleteFiles(filesForIndexes(addChildren(getSelectedIndexes())));
1620     connect(job, &KJob::result, this, &Part::slotDeleteFilesDone);
1621     registerJob(job);
1622     job->start();
1623 }
1624 
1625 void Part::slotShowProperties()
1626 {
1627     QPointer<Kerfuffle::PropertiesDialog> dialog(
1628         new Kerfuffle::PropertiesDialog(nullptr, m_model->archive(), m_model->numberOfFiles(), m_model->numberOfFolders(), m_model->uncompressedSize()));
1629     dialog.data()->show();
1630 }
1631 
1632 void Part::slotToggleInfoPanel(bool visible)
1633 {
1634     if (visible) {
1635         m_splitter->setSizes(ArkSettings::splitterSizes());
1636         m_infoPanel->show();
1637     } else {
1638         // We need to save the splitterSizes before hiding, otherwise
1639         // Ark won't remember resizing done by the user.
1640         ArkSettings::setSplitterSizes(m_splitter->sizes());
1641         m_infoPanel->hide();
1642     }
1643 }
1644 
1645 void Part::slotSaveAs()
1646 {
1647     const QUrl srcUrl = url();
1648     const QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18nc("@title:window", "Save Copy As"), srcUrl);
1649 
1650     if (saveUrl.isEmpty()) { // If the user selected "cancel" the returned url is empty
1651         return;
1652     }
1653 
1654     KIO::Job *copyJob = KIO::file_copy(srcUrl, saveUrl, -1, KIO::Overwrite);
1655     KJobWidgets::setWindow(copyJob, widget());
1656     connect(copyJob, &KJob::result, this, [this, copyJob, srcUrl, saveUrl]() {
1657         const int err = copyJob->error();
1658         if (err) {
1659             QString msg = copyJob->errorString();
1660             // Use custom error messages for these two cases, otherwise just use KIO's
1661             if (err == KIO::ERR_WRITE_ACCESS_DENIED) {
1662                 msg = xi18nc("@info",
1663                              "The archive could not be saved as <filename>%1</filename>. Try saving"
1664                              " it to another location.",
1665                              saveUrl.toDisplayString(QUrl::PreferLocalFile));
1666             } else if (err == KIO::ERR_DOES_NOT_EXIST) {
1667                 msg = xi18nc("@info",
1668                              "The archive <filename>%1</filename> does not exist anymore, therefore it"
1669                              " cannot be copied to the specified location.",
1670                              srcUrl.toDisplayString(QUrl::PreferLocalFile));
1671             }
1672 
1673             KMessageBox::error(widget(), msg);
1674         }
1675     });
1676 }
1677 
1678 void Part::slotShowContextMenu()
1679 {
1680     if (!factory()) {
1681         return;
1682     }
1683 
1684     QMenu *popup = static_cast<QMenu *>(factory()->container(QStringLiteral("context_menu"), this));
1685     if (KHamburgerMenu *const hamburgerMenu =
1686             static_cast<KHamburgerMenu *>(actionCollection()->action(KStandardAction::name(KStandardAction::HamburgerMenu)))) {
1687         hamburgerMenu->insertIntoMenuBefore(popup, popup->actions().constFirst());
1688     }
1689     popup->popup(QCursor::pos());
1690 }
1691 
1692 bool Part::eventFilter(QObject *target, QEvent *event)
1693 {
1694     Q_UNUSED(target)
1695 
1696     if (event->type() == QEvent::KeyPress) {
1697         QKeyEvent *e = static_cast<QKeyEvent *>(event);
1698         if (e->key() == Qt::Key_Escape) {
1699             m_searchWidget->hide();
1700             m_searchLineEdit->clear();
1701             return true;
1702         }
1703     }
1704     return false;
1705 }
1706 
1707 void Part::slotShowFind()
1708 {
1709     if (m_searchWidget->isVisible()) {
1710         m_searchLineEdit->selectAll();
1711     } else {
1712         m_searchWidget->show();
1713     }
1714     m_searchLineEdit->setFocus();
1715 }
1716 
1717 void Part::searchEdited(const QString &text)
1718 {
1719     m_view->collapseAll();
1720 
1721     m_filterModel->setFilterFixedString(text);
1722 
1723     if (text.isEmpty()) {
1724         m_view->collapseAll();
1725         m_view->expandIfSingleFolder();
1726     } else {
1727         m_view->expandAll();
1728     }
1729 }
1730 
1731 void Part::displayMsgWidget(KMessageWidget::MessageType type, const QString &msg)
1732 {
1733     // The widget could be already visible, so hide it.
1734     m_messageWidget->hide();
1735     m_messageWidget->setText(msg);
1736     m_messageWidget->setMessageType(type);
1737     m_messageWidget->animatedShow();
1738 }
1739 
1740 } // namespace Ark
1741 
1742 #include "moc_part.cpp"