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"