part.cpp 49.9 KB
Newer Older
1 2 3 4
/*
 * ark -- archiver for the KDE project
 *
 * Copyright (C) 2007 Henrique Pinto <henrique.pinto@kdemail.net>
5
 * Copyright (C) 2008-2009 Harald Hvaal <haraldhv@stud.ntnu.no>
6
 * Copyright (C) 2009-2012 Raphael Kubo da Costa <rakuco@FreeBSD.org>
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */
23

Henrique Pinto's avatar
Henrique Pinto committed
24
#include "part.h"
25
#include "ark_debug.h"
26
#include "archiveformat.h"
Henrique Pinto's avatar
Henrique Pinto committed
27
#include "archivemodel.h"
28
#include "archiveview.h"
Henrique Pinto's avatar
Henrique Pinto committed
29
#include "arkviewer.h"
30
#include "dnddbusinterfaceadaptor.h"
31 32
#include "infopanel.h"
#include "jobtracker.h"
33
#include "kerfuffle/archive_kerfuffle.h"
34
#include "kerfuffle/extractiondialog.h"
35
#include "kerfuffle/extractionsettingspage.h"
36
#include "kerfuffle/jobs.h"
37
#include "kerfuffle/settings.h"
38
#include "kerfuffle/previewsettingspage.h"
Ragnar Thomsen's avatar
Ragnar Thomsen committed
39
#include "kerfuffle/propertiesdialog.h"
40
#include "pluginmanager.h"
Henrique Pinto's avatar
Henrique Pinto committed
41

42
#include <KAboutData>
Henrique Pinto's avatar
Henrique Pinto committed
43
#include <KActionCollection>
44
#include <KConfigGroup>
45
#include <KGuiItem>
46
#include <KIO/Job>
47 48
#include <KJobWidgets>
#include <KIO/StatJob>
49
#include <KMessageBox>
50
#include <KPluginFactory>
51
#include <KRun>
52
#include <KSelectAction>
53
#include <KStandardGuiItem>
54
#include <KToggleAction>
Bhushan Shah's avatar
Bhushan Shah committed
55
#include <KLocalizedString>
56
#include <KXMLGUIFactory>
57

Henrique Pinto's avatar
Henrique Pinto committed
58
#include <QAction>
59 60
#include <QCursor>
#include <QHeaderView>
61
#include <QMenu>
62
#include <QMimeData>
63
#include <QMouseEvent>
64
#include <QScopedPointer>
Elvis Angelaccio's avatar
Elvis Angelaccio committed
65
#include <QStatusBar>
66
#include <QPointer>
67 68
#include <QSplitter>
#include <QTimer>
Lukáš Tinkl's avatar
Lukáš Tinkl committed
69
#include <QFileDialog>
Arnold Dumas's avatar
Arnold Dumas committed
70
#include <QIcon>
71
#include <QInputDialog>
Ragnar Thomsen's avatar
Ragnar Thomsen committed
72
#include <QFileSystemWatcher>
73 74
#include <QGroupBox>
#include <QPlainTextEdit>
75

76 77
using namespace Kerfuffle;

78
K_PLUGIN_FACTORY(Factory, registerPlugin<Ark::Part>();)
Henrique Pinto's avatar
Henrique Pinto committed
79

80 81 82
namespace Ark
{

83 84
static quint32 s_instanceCounter = 1;

85
Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args)
86
        : KParts::ReadWritePart(parent),
Lukáš Tinkl's avatar
Lukáš Tinkl committed
87
          m_splitter(Q_NULLPTR),
88
          m_busy(false),
Lukáš Tinkl's avatar
Lukáš Tinkl committed
89
          m_jobTracker(Q_NULLPTR)
Henrique Pinto's avatar
Henrique Pinto committed
90
{
91
    Q_UNUSED(args)
92
    setComponentData(*createAboutData(), false);
Henrique Pinto's avatar
Henrique Pinto committed
93

94 95
    new DndExtractAdaptor(this);

Lukáš Tinkl's avatar
Lukáš Tinkl committed
96
    const QString pathName = QStringLiteral("/DndExtract/%1").arg(s_instanceCounter++);
97
    if (!QDBusConnection::sessionBus().registerObject(pathName, this)) {
98
        qCCritical(ARK) << "Could not register a D-Bus object for drag'n'drop";
99 100
    }

101 102 103
    // m_vlayout is needed for later insertion of QMessageWidget
    QWidget *mainWidget = new QWidget;
    m_vlayout = new QVBoxLayout;
104
    m_model = new ArchiveModel(pathName, this);
105
    m_splitter = new QSplitter(Qt::Horizontal, parentWidget);
106 107 108
    m_view = new ArchiveView;
    m_infoPanel = new InfoPanel(m_model);

109 110 111 112 113 114 115 116 117 118
    // Add widgets for the comment field.
    m_commentView = new QPlainTextEdit();
    m_commentView->setReadOnly(true);
    m_commentView->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
    m_commentBox = new QGroupBox(i18n("Comment"));
    m_commentBox->hide();
    QVBoxLayout *vbox = new QVBoxLayout;
    vbox->addWidget(m_commentView);
    m_commentBox->setLayout(vbox);

119 120 121 122 123 124 125 126 127 128 129 130 131 132
    m_commentMsgWidget = new KMessageWidget();
    m_commentMsgWidget->setText(i18n("Comment has been modified."));
    m_commentMsgWidget->setMessageType(KMessageWidget::Information);
    m_commentMsgWidget->setCloseButtonVisible(false);
    m_commentMsgWidget->hide();

    QAction *saveAction = new QAction(i18n("Save"), m_commentMsgWidget);
    m_commentMsgWidget->addAction(saveAction);
    connect(saveAction, &QAction::triggered, this, &Part::slotAddComment);

    m_commentBox->layout()->addWidget(m_commentMsgWidget);

    connect(m_commentView, &QPlainTextEdit::textChanged, this, &Part::slotCommentChanged);

133 134
    setWidget(mainWidget);
    mainWidget->setLayout(m_vlayout);
135

136 137 138 139
    // Configure the QVBoxLayout and add widgets
    m_vlayout->setContentsMargins(0,0,0,0);
    m_vlayout->addWidget(m_splitter);

140 141 142 143 144 145 146 147 148
    // Vertical QSplitter for the file view and comment field.
    m_commentSplitter = new QSplitter(Qt::Vertical, parentWidget);
    m_commentSplitter->setOpaqueResize(false);
    m_commentSplitter->addWidget(m_view);
    m_commentSplitter->addWidget(m_commentBox);
    m_commentSplitter->setCollapsible(0, false);

    // Horizontal QSplitter for the file view and infopanel.
    m_splitter->addWidget(m_commentSplitter);
149
    m_splitter->addWidget(m_infoPanel);
150 151 152 153 154 155 156

    // Read settings from config file and show/hide infoPanel.
    if (!ArkSettings::showInfoPanel()) {
        m_infoPanel->hide();
    } else {
        m_splitter->setSizes(ArkSettings::splitterSizes());
    }
157

158 159
    setupView();
    setupActions();
Henrique Pinto's avatar
Henrique Pinto committed
160

Laurent Montel's avatar
Laurent Montel committed
161 162 163 164
    connect(m_model, &ArchiveModel::loadingStarted,
            this, &Part::slotLoadingStarted);
    connect(m_model, &ArchiveModel::loadingFinished,
            this, &Part::slotLoadingFinished);
Laurent Montel's avatar
Laurent Montel committed
165 166
    connect(m_model, SIGNAL(droppedFiles(QStringList,QString)),
            this, SLOT(slotAddFiles(QStringList,QString)));
Laurent Montel's avatar
Laurent Montel committed
167 168
    connect(m_model, &ArchiveModel::error,
            this, &Part::slotError);
169

Laurent Montel's avatar
Laurent Montel committed
170 171 172 173
    connect(this, &Part::busy,
            this, &Part::setBusyGui);
    connect(this, &Part::ready,
            this, &Part::setReadyGui);
174
    connect(this, SIGNAL(completed()),
175
            this, SLOT(setFileNameFromArchive()));
176

177
    m_statusBarExtension = new KParts::StatusBarExtension(this);
178

Lukáš Tinkl's avatar
Lukáš Tinkl committed
179
    setXMLFile(QStringLiteral("ark_part.rc"));
Henrique Pinto's avatar
Henrique Pinto committed
180 181 182 183
}

Part::~Part()
{
Ragnar Thomsen's avatar
Ragnar Thomsen committed
184
    qDeleteAll(m_tmpOpenDirList);
185

186
    // Only save splitterSizes if infopanel is visible,
187
    // because we don't want to store zero size for infopanel.
188 189 190 191 192
    if (m_showInfoPanelAction->isChecked()) {
        ArkSettings::setSplitterSizes(m_splitter->sizes());
    }
    ArkSettings::setShowInfoPanel(m_showInfoPanelAction->isChecked());
    ArkSettings::self()->save();
193

194
    m_extractArchiveAction->menu()->deleteLater();
195
    m_extractAction->menu()->deleteLater();
Henrique Pinto's avatar
Henrique Pinto committed
196 197
}

198 199 200 201 202 203 204 205 206
void Part::slotCommentChanged()
{
    if (m_commentMsgWidget->isHidden() && m_commentView->toPlainText() != m_model->archive()->comment()) {
        m_commentMsgWidget->animatedShow();
    } else if (m_commentMsgWidget->isVisible() && m_commentView->toPlainText() == m_model->archive()->comment()) {
        m_commentMsgWidget->hide();
    }
}

207 208
KAboutData *Part::createAboutData()
{
209 210 211
    return new KAboutData(QStringLiteral("ark"),
                          i18n("ArkPart"),
                          QStringLiteral("3.0"));
212 213
}

214
void Part::registerJob(KJob* job)
215
{
216
    if (!m_jobTracker) {
217
        m_jobTracker = new JobTracker(widget());
218 219 220 221
        m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(0), 0, true);
        m_jobTracker->widget(job)->show();
    }
    m_jobTracker->registerJob(job);
222

223
    emit busy();
224
    connect(job, &KJob::result, this, &Part::ready);
225 226
}

227 228 229
// TODO: KIO::mostLocalHere is used here to resolve some KIO URLs to local
// paths (e.g. desktop:/), but more work is needed to support extraction
// to non-local destinations. See bugs #189322 and #204323.
230
void Part::extractSelectedFilesTo(const QString& localPath)
231
{
Raphael Kubo da Costa's avatar
Raphael Kubo da Costa committed
232 233 234
    if (!m_model) {
        return;
    }
235

236
    const QUrl url = QUrl::fromUserInput(localPath, QString());
237 238
    KIO::StatJob* statJob = nullptr;

239 240
    // Try to resolve the URL to a local path.
    if (!url.isLocalFile() && !url.scheme().isEmpty()) {
241 242 243 244 245 246 247 248 249 250
        statJob = KIO::mostLocalUrl(url);

        if (!statJob->exec() || statJob->error() != 0) {
            return;
        }
    }

    const QString destination = statJob ? statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : localPath;
    delete statJob;

251 252 253 254 255 256 257
    // The URL could not be resolved to a local path.
    if (!url.isLocalFile() && destination.isEmpty()) {
        qCWarning(ARK) << "Ark cannot extract to non-local destination:" << localPath;
        KMessageBox::sorry(widget(), xi18nc("@info", "Ark can only extract to local destinations."));
        return;
    }

258 259
    qCDebug(ARK) << "Extract to" << destination;

260
    Kerfuffle::ExtractionOptions options;
261 262
    options[QStringLiteral("PreservePaths")] = true;
    options[QStringLiteral("RemoveRootNode")] = true;
263
    options[QStringLiteral("DragAndDrop")] = true;
264

265
    // Create and start the ExtractJob.
266
    ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), destination, options);
267
    registerJob(job);
268 269
    connect(job, &KJob::result,
            this, &Part::slotExtractionDone);
270
    job->start();
271 272
}

273 274
void Part::setupView()
{
275 276
    m_view->setContextMenuPolicy(Qt::CustomContextMenu);

277
    m_view->setModel(m_model);
278

279
    m_view->setSortingEnabled(true);
Harald Hvaal's avatar
Harald Hvaal committed
280

Laurent Montel's avatar
Laurent Montel committed
281 282 283 284
    connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged,
            this, &Part::updateActions);
    connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged,
            this, &Part::selectionChanged);
Harald Hvaal's avatar
Harald Hvaal committed
285

286
    connect(m_view, &QTreeView::activated, this, &Part::slotActivated);
287

Laurent Montel's avatar
Laurent Montel committed
288
    connect(m_view, &QWidget::customContextMenuRequested, this, &Part::slotShowContextMenu);
Harald Hvaal's avatar
Harald Hvaal committed
289

Laurent Montel's avatar
Laurent Montel committed
290 291
    connect(m_model, &QAbstractItemModel::columnsInserted,
            this, &Part::adjustColumns);
292 293
}

294
void Part::slotActivated(QModelIndex)
295
{
296 297
    // The activated signal is emitted when items are selected with the mouse,
    // so do nothing if CTRL or SHIFT key is pressed.
298 299
    if (QGuiApplication::keyboardModifiers() != Qt::ShiftModifier &&
        QGuiApplication::keyboardModifiers() != Qt::ControlModifier) {
300
        ArkSettings::defaultOpenAction() == ArkSettings::EnumDefaultOpenAction::Preview ? slotOpenEntry(Preview) : slotOpenEntry(OpenFile);
301 302 303
    }
}

Henrique Pinto's avatar
Henrique Pinto committed
304 305
void Part::setupActions()
{
Ragnar Thomsen's avatar
Ragnar Thomsen committed
306 307 308 309 310
    // We use a QSignalMapper for the preview, open and openwith actions. This
    // way we can connect all three actions to the same slot slotOpenEntry and
    // pass the OpenFileMode as argument to the slot.
    m_signalMapper = new QSignalMapper;

311
    m_showInfoPanelAction = new KToggleAction(i18nc("@action:inmenu", "Show information panel"), this);
312
    actionCollection()->addAction(QStringLiteral( "show-infopanel" ), m_showInfoPanelAction);
313
    m_showInfoPanelAction->setChecked(ArkSettings::showInfoPanel());
Laurent Montel's avatar
Laurent Montel committed
314 315
    connect(m_showInfoPanelAction, &QAction::triggered,
            this, &Part::slotToggleInfoPanel);
316

317
    m_saveAsAction = actionCollection()->addAction(KStandardAction::SaveAs, QStringLiteral("ark_file_save_as"), this, SLOT(slotSaveAs()));
Raphael Kubo da Costa's avatar
Raphael Kubo da Costa committed
318

Ragnar Thomsen's avatar
Ragnar Thomsen committed
319
    m_openFileAction = actionCollection()->addAction(QStringLiteral("openfile"));
320
    m_openFileAction->setText(i18nc("open a file with external program", "&Open"));
Ragnar Thomsen's avatar
Ragnar Thomsen committed
321
    m_openFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
322
    m_openFileAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with the associated application"));
Ragnar Thomsen's avatar
Ragnar Thomsen committed
323 324 325 326
    connect(m_openFileAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map()));
    m_signalMapper->setMapping(m_openFileAction, OpenFile);

    m_openFileWithAction = actionCollection()->addAction(QStringLiteral("openfilewith"));
327
    m_openFileWithAction->setText(i18nc("open a file with external program", "Open &With..."));
Ragnar Thomsen's avatar
Ragnar Thomsen committed
328
    m_openFileWithAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
329
    m_openFileWithAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with an external program"));
Ragnar Thomsen's avatar
Ragnar Thomsen committed
330 331
    connect(m_openFileWithAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map()));
    m_signalMapper->setMapping(m_openFileWithAction, OpenFileWith);
332

333
    m_previewAction = actionCollection()->addAction(QStringLiteral("preview"));
334
    m_previewAction->setText(i18nc("to preview a file inside an archive", "Pre&view"));
335
    m_previewAction->setIcon(QIcon::fromTheme(QStringLiteral("document-preview-archive")));
336
    m_previewAction->setToolTip(i18nc("@info:tooltip", "Click to preview the selected file"));
337
    actionCollection()->setDefaultShortcut(m_previewAction, Qt::CTRL + Qt::Key_P);
Ragnar Thomsen's avatar
Ragnar Thomsen committed
338 339
    connect(m_previewAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map()));
    m_signalMapper->setMapping(m_previewAction, Preview);
340

341 342
    m_extractArchiveAction = actionCollection()->addAction(QStringLiteral("extract_all"));
    m_extractArchiveAction->setText(i18nc("@action:inmenu", "E&xtract All"));
343 344
    m_extractArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract")));
    m_extractArchiveAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose how to extract all the files in the archive"));
345
    actionCollection()->setDefaultShortcut(m_extractArchiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_E);
346 347 348
    connect(m_extractArchiveAction, &QAction::triggered,
            this, &Part::slotExtractArchive);

349 350 351 352 353 354
    m_extractAction = actionCollection()->addAction(QStringLiteral("extract"));
    m_extractAction->setText(i18nc("@action:inmenu", "&Extract"));
    m_extractAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract")));
    actionCollection()->setDefaultShortcut(m_extractAction, Qt::CTRL + Qt::Key_E);
    m_extractAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose to extract either all files or just the selected ones"));
    connect(m_extractAction, &QAction::triggered,
355
            this, &Part::slotShowExtractionDialog);
356

357 358
    m_addFilesAction = actionCollection()->addAction(QStringLiteral("add"));
    m_addFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert")));
359
    m_addFilesAction->setText(i18n("Add &File..."));
360
    m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive"));
361 362 363
    connect(m_addFilesAction, SIGNAL(triggered(bool)),
            this, SLOT(slotAddFiles()));

364 365
    m_addDirAction = actionCollection()->addAction(QStringLiteral("add-dir"));
    m_addDirAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert-directory")));
366
    m_addDirAction->setText(i18n("Add Fo&lder..."));
367
    m_addDirAction->setToolTip(i18nc("@info:tooltip", "Click to add a folder to the archive"));
Laurent Montel's avatar
Laurent Montel committed
368 369
    connect(m_addDirAction, &QAction::triggered,
            this, &Part::slotAddDir);
370

371 372
    m_deleteFilesAction = actionCollection()->addAction(QStringLiteral("delete"));
    m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove")));
373
    m_deleteFilesAction->setText(i18n("De&lete"));
374
    actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete);
375
    m_deleteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to delete the selected files"));
Laurent Montel's avatar
Laurent Montel committed
376 377
    connect(m_deleteFilesAction, &QAction::triggered,
            this, &Part::slotDeleteFiles);
378

Ragnar Thomsen's avatar
Ragnar Thomsen committed
379 380 381 382 383 384 385 386
    m_propertiesAction = actionCollection()->addAction(QStringLiteral("properties"));
    m_propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
    m_propertiesAction->setText(i18nc("@action:inmenu", "&Properties"));
    actionCollection()->setDefaultShortcut(m_propertiesAction, Qt::ALT + Qt::Key_Return);
    m_propertiesAction->setToolTip(i18nc("@info:tooltip", "Click to see properties for archive"));
    connect(m_propertiesAction, &QAction::triggered,
            this, &Part::slotShowProperties);

387 388 389 390 391 392
    m_editCommentAction = actionCollection()->addAction(QStringLiteral("edit_comment"));
    m_editCommentAction->setIcon(QIcon::fromTheme(QStringLiteral("document-edit")));
    actionCollection()->setDefaultShortcut(m_editCommentAction, Qt::ALT + Qt::Key_C);
    m_editCommentAction->setToolTip(i18nc("@info:tooltip", "Click to add or edit comment"));
    connect(m_editCommentAction, &QAction::triggered, this, &Part::slotShowComment);

Ragnar Thomsen's avatar
Ragnar Thomsen committed
393 394 395 396 397 398 399
    m_testArchiveAction = actionCollection()->addAction(QStringLiteral("test_archive"));
    m_testArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("checkmark")));
    m_testArchiveAction->setText(i18nc("@action:inmenu", "&Test Integrity"));
    actionCollection()->setDefaultShortcut(m_testArchiveAction, Qt::ALT + Qt::Key_T);
    m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity"));
    connect(m_testArchiveAction, &QAction::triggered, this, &Part::slotTestArchive);

Ragnar Thomsen's avatar
Ragnar Thomsen committed
400 401
    connect(m_signalMapper, SIGNAL(mapped(int)), this, SLOT(slotOpenEntry(int)));

402
    updateActions();
403
    updateQuickExtractMenu(m_extractArchiveAction);
404
    updateQuickExtractMenu(m_extractAction);
Henrique Pinto's avatar
Henrique Pinto committed
405 406 407 408
}

void Part::updateActions()
{
Ragnar Thomsen's avatar
Ragnar Thomsen committed
409 410 411
    bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly();
    bool isDirectory = m_model->entryForIndex(m_view->selectionModel()->currentIndex())[IsDirectory].toBool();
    int selectedEntriesCount = m_view->selectionModel()->selectedRows().count();
412

Ragnar Thomsen's avatar
Ragnar Thomsen committed
413 414 415 416 417 418 419 420 421 422
    // Figure out if entry size is larger than preview size limit.
    const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024;
    const bool limit = ArkSettings::limitPreviewFileSize();
    const qlonglong size = m_model->entryForIndex(m_view->selectionModel()->currentIndex())[Size].toLongLong();
    bool isPreviewable = (!limit || (limit && size < maxPreviewSize));

    m_previewAction->setEnabled(!isBusy() &&
                                isPreviewable &&
                                !isDirectory &&
                                (selectedEntriesCount == 1));
423 424
    m_extractArchiveAction->setEnabled(!isBusy() &&
                                       (m_model->rowCount() > 0));
425 426
    m_extractAction->setEnabled(!isBusy() &&
                                (m_model->rowCount() > 0));
427 428
    m_saveAsAction->setEnabled(!isBusy() &&
                               m_model->rowCount() > 0);
Ragnar Thomsen's avatar
Ragnar Thomsen committed
429 430 431 432 433 434 435 436 437 438
    m_addFilesAction->setEnabled(!isBusy() &&
                                 isWritable);
    m_addDirAction->setEnabled(!isBusy() &&
                               isWritable);
    m_deleteFilesAction->setEnabled(!isBusy() &&
                                    isWritable &&
                                    (selectedEntriesCount > 0));
    m_openFileAction->setEnabled(!isBusy() &&
                                 isPreviewable &&
                                 !isDirectory &&
439
                                 (selectedEntriesCount == 1));
Ragnar Thomsen's avatar
Ragnar Thomsen committed
440 441 442
    m_openFileWithAction->setEnabled(!isBusy() &&
                                     isPreviewable &&
                                     !isDirectory &&
443
                                     (selectedEntriesCount == 1));
Ragnar Thomsen's avatar
Ragnar Thomsen committed
444 445
    m_propertiesAction->setEnabled(!isBusy() &&
                                   m_model->archive());
446 447 448 449

    m_commentView->setEnabled(!isBusy());
    m_commentMsgWidget->setEnabled(!isBusy());

Ragnar Thomsen's avatar
Ragnar Thomsen committed
450 451 452
    m_editCommentAction->setEnabled(false);
    m_testArchiveAction->setEnabled(false);

453 454 455 456 457 458
    if (m_model->archive()) {
        const KPluginMetaData metadata = PluginManager().preferredPluginFor(m_model->archive()->mimeType())->metaData();
        bool supportsWriteComment = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsWriteComment();
        m_editCommentAction->setEnabled(!isBusy() &&
                                        supportsWriteComment);
        m_commentView->setReadOnly(!supportsWriteComment);
459 460
        m_editCommentAction->setText(m_model->archive()->hasComment() ? i18nc("@action:inmenu mutually exclusive with Add &Comment", "Edit &Comment") :
                                                                        i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment"));
Ragnar Thomsen's avatar
Ragnar Thomsen committed
461 462 463 464

        bool supportsTesting = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsTesting();
        m_testArchiveAction->setEnabled(!isBusy() &&
                                        supportsTesting);
465 466
    } else {
        m_commentView->setReadOnly(true);
467
        m_editCommentAction->setText(i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment"));
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
    }
}

void Part::slotShowComment()
{
    if (!m_commentBox->isVisible()) {
        m_commentBox->show();
        m_commentSplitter->setSizes(QList<int>() << m_view->height() * 0.6 << 1);
    }
    m_commentView->setFocus();
}

void Part::slotAddComment()
{
    CommentJob *job = m_model->archive()->addComment(m_commentView->toPlainText());
    if (!job) {
        return;
    }
    registerJob(job);
    job->start();
    m_commentMsgWidget->hide();
    if (m_commentView->toPlainText().isEmpty()) {
        m_commentBox->hide();
    }
492 493
}

Ragnar Thomsen's avatar
Ragnar Thomsen committed
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
void Part::slotTestArchive()
{
    TestJob *job = m_model->archive()->testArchive();
    if (!job) {
        return;
    }
    registerJob(job);
    connect(job, &KJob::result, this, &Part::slotTestingDone);
    job->start();
}

void Part::slotTestingDone(KJob* job)
{
    if (job->error() && job->error() != KJob::KilledJobError) {
        KMessageBox::error(widget(), job->errorString());
    } else if (static_cast<TestJob*>(job)->testSucceeded()) {
        KMessageBox::information(widget(), i18n("The archive passed the integrity test."), i18n("Test Results"));
    } else {
        KMessageBox::error(widget(), i18n("The archive failed the integrity test."), i18n("Test Results"));
    }
}

516 517 518 519 520 521 522 523
void Part::updateQuickExtractMenu(QAction *extractAction)
{
    if (!extractAction) {
        return;
    }

    QMenu *menu = extractAction->menu();

524
    if (!menu) {
525 526
        menu = new QMenu();
        extractAction->setMenu(menu);
Laurent Montel's avatar
Laurent Montel committed
527 528
        connect(menu, &QMenu::triggered,
                this, &Part::slotQuickExtractFiles);
529 530

        // Remember to keep this action's properties as similar to
531
        // extractAction's as possible (except where it does not make
532
        // sense, such as the text or the shortcut).
533
        QAction *extractTo = menu->addAction(i18n("Extract To..."));
534 535 536 537 538 539 540 541 542 543
        extractTo->setIcon(extractAction->icon());
        extractTo->setToolTip(extractAction->toolTip());

        if (extractAction == m_extractArchiveAction) {
            connect(extractTo, &QAction::triggered,
                    this, &Part::slotExtractArchive);
        } else {
            connect(extractTo, &QAction::triggered,
                    this, &Part::slotShowExtractionDialog);
        }
544

545
        menu->addSeparator();
546

547
        QAction *header = menu->addAction(i18n("Quick Extract To..."));
548
        header->setEnabled(false);
549
        header->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract")));
550 551
    }

552 553
    while (menu->actions().size() > 3) {
        menu->removeAction(menu->actions().last());
554 555
    }

556 557
    const KConfigGroup conf(KSharedConfig::openConfig(), "ExtractDialog");
    const QStringList dirHistory = conf.readPathEntry("DirHistory", QStringList());
558 559

    for (int i = 0; i < qMin(10, dirHistory.size()); ++i) {
560
        const QString dir = QUrl(dirHistory.value(i)).toString(QUrl::RemoveScheme | QUrl::NormalizePathSegments | QUrl::PreferLocalFile);
561 562 563 564
        if (QDir(dir).exists()) {
            QAction *newAction = menu->addAction(dir);
            newAction->setData(dir);
        }
565
    }
566 567 568 569
}

void Part::slotQuickExtractFiles(QAction *triggeredAction)
{
570 571 572 573
    // #190507: triggeredAction->data.isNull() means it's the "Extract to..."
    //          action, and we do not want it to run here
    if (!triggeredAction->data().isNull()) {
        const QString userDestination = triggeredAction->data().toString();
574
        qCDebug(ARK) << "Extract to user dest" << userDestination;
575 576
        QString finalDestinationDirectory;
        const QString detectedSubfolder = detectSubfolder();
577
        qCDebug(ARK) << "Detected subfolder" << detectedSubfolder;
578 579 580 581 582

        if (!isSingleFolderArchive()) {
            finalDestinationDirectory = userDestination +
                                        QDir::separator() + detectedSubfolder;
            QDir(userDestination).mkdir(detectedSubfolder);
Raphael Kubo da Costa's avatar
Raphael Kubo da Costa committed
583 584 585
        } else {
            finalDestinationDirectory = userDestination;
        }
586

587
        qCDebug(ARK) << "Extract to final dest" << finalDestinationDirectory;
588

589
        Kerfuffle::ExtractionOptions options;
590
        options[QStringLiteral("PreservePaths")] = true;
591
        QList<QVariant> files = filesAndRootNodesForIndexes(m_view->selectionModel()->selectedRows());
592 593 594
        ExtractJob *job = m_model->extractFiles(files, finalDestinationDirectory, options);
        registerJob(job);

Laurent Montel's avatar
Laurent Montel committed
595 596
        connect(job, &KJob::result,
                this, &Part::slotExtractionDone);
597 598 599

        job->start();
    }
Henrique Pinto's avatar
Henrique Pinto committed
600
}
601

602 603
void Part::selectionChanged()
{
604
    m_infoPanel->setIndexes(m_view->selectionModel()->selectedRows());
605 606
}

Henrique Pinto's avatar
Henrique Pinto committed
607 608
bool Part::openFile()
{
609
    qCDebug(ARK) << "Attempting to open archive" << localFilePath();
610

611
    if (!isLocalFileValid()) {
612 613 614
        return false;
    }

615 616
    const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")];
    QScopedPointer<Kerfuffle::Archive> archive(Kerfuffle::Archive::create(localFilePath(), fixedMimeType, m_model));
617 618 619
    Q_ASSERT(archive);

    if (archive->error() == NoPlugin) {
620 621 622
        displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open <filename>%1</filename>. No suitable plugin found.<nl/>"
                                                                "Ark does not seem to support this file type.",
                                                                QFileInfo(localFilePath()).fileName()));
623
        return false;
624 625 626 627 628 629
    }

    if (archive->error() == FailedPlugin) {
        displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open <filename>%1</filename>. Failed to load a suitable plugin.<nl/>"
                                                                "Make sure any executables needed to handle the archive type are installed.",
                                                                QFileInfo(localFilePath()).fileName()));
630 631 632
        return false;
    }

633 634 635
    Q_ASSERT(archive->isValid());

    // Plugin loaded successfully.
636
    KJob *job = m_model->setArchive(archive.take());
637 638 639 640 641 642 643
    if (job) {
        registerJob(job);
        job->start();
    } else {
        updateActions();
    }

644 645
    m_infoPanel->setIndex(QModelIndex());

646
    if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) {
647
        QTimer::singleShot(0, this, &Part::slotShowExtractionDialog);
648 649
    }

650
    const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")];
651
    if (!password.isEmpty()) {
652 653
        m_model->encryptArchive(password,
                                arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true"));
654 655
    }

656
    return true;
Henrique Pinto's avatar
Henrique Pinto committed
657 658 659 660
}

bool Part::saveFile()
{
661
    return true;
Henrique Pinto's avatar
Henrique Pinto committed
662
}
663

664 665 666 667 668
bool Part::isBusy() const
{
    return m_busy;
}

669 670 671 672 673 674 675
KConfigSkeleton *Part::config() const
{
    return ArkSettings::self();
}

QList<Kerfuffle::SettingsPage*> Part::settingsPages(QWidget *parent) const
{
676
    QList<SettingsPage*> pages;
677 678
    pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction Settings"), QStringLiteral("archive-extract")));
    pages.append(new PreviewSettingsPage(parent, i18nc("@title:tab", "Preview Settings"), QStringLiteral("document-preview-archive")));
679 680 681 682

    return pages;
}

683 684 685 686 687 688 689 690 691 692 693 694 695 696 697
bool Part::isLocalFileValid()
{
    const QString localFile = localFilePath();
    const QFileInfo localFileInfo(localFile);
    const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true");

    if (localFileInfo.isDir()) {
        displayMsgWidget(KMessageWidget::Error, xi18nc("@info",
                                                       "<filename>%1</filename> is a directory.",
                                                       localFile));
        return false;
    }

    if (creatingNewArchive) {
        if (localFileInfo.exists()) {
698 699 700 701
            if (!confirmAndDelete(localFile)) {
                displayMsgWidget(KMessageWidget::Error, xi18nc("@info",
                                                               "Could not overwrite <filename>%1</filename>. Check whether you have write permission.",
                                                               localFile));
702 703 704
                return false;
            }
        }
705

706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721
        displayMsgWidget(KMessageWidget::Information, xi18nc("@info", "The archive <filename>%1</filename> will be created as soon as you add a file.", localFile));
    } else {
        if (!localFileInfo.exists()) {
            displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive <filename>%1</filename> was not found.", localFile));
            return false;
        }

        if (!localFileInfo.isReadable()) {
            displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive <filename>%1</filename> could not be loaded, as it was not possible to read from it.", localFile));
            return false;
        }
    }

    return true;
}

722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741
bool Part::confirmAndDelete(const QString &targetFile)
{
    QFileInfo targetInfo(targetFile);
    const auto buttonCode = KMessageBox::warningYesNo(widget(),
                                                      xi18nc("@info",
                                                             "The archive <filename>%1</filename> already exists. Do you wish to overwrite it?",
                                                             targetInfo.fileName()),
                                                      i18nc("@title:window", "File Exists"),
                                                      KGuiItem(i18nc("@action:button", "Overwrite")),
                                                      KStandardGuiItem::cancel());

    if (buttonCode != KMessageBox::Yes || !targetInfo.isWritable()) {
        return false;
    }

    qCDebug(ARK) << "Removing file" << targetFile;

    return QFile(targetFile).remove();
}

742 743 744 745
void Part::slotLoadingStarted()
{
}

746
void Part::slotLoadingFinished(KJob *job)
747
{
Raphael Kubo da Costa's avatar
Raphael Kubo da Costa committed
748
    if (job->error()) {
749
        if (arguments().metaData()[QStringLiteral("createNewArchive")] != QLatin1String("true")) {
750
            if (job->error() != KJob::KilledJobError) {
751 752 753
                displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive <filename>%1</filename> failed with the following error:<nl/><message>%2</message>",
                                                               localFilePath(),
                                                               job->errorText()));
754
            }
755 756

            // The file failed to open, so reset the open archive, info panel and caption.
Lukáš Tinkl's avatar
Lukáš Tinkl committed
757
            m_model->setArchive(Q_NULLPTR);
758 759 760 761 762

            m_infoPanel->setPrettyFileName(QString());
            m_infoPanel->updateWithDefaults();

            emit setWindowCaption(QString());
Raphael Kubo da Costa's avatar
Raphael Kubo da Costa committed
763 764
        }
    }
765

766
    m_view->sortByColumn(0, Qt::AscendingOrder);
767

768 769 770 771 772
    // #303708: expand the first level only when there is just one root folder.
    // Typical use case: an archive with source files.
    if (m_view->model()->rowCount() == 1) {
        m_view->expandToDepth(0);
    }
773

774 775
    // After loading all files, resize the columns to fit all fields
    m_view->header()->resizeSections(QHeaderView::ResizeToContents);
776

777
    updateActions();
778

779 780 781 782 783
    if (!m_model->archive()) {
        return;
    }

    if (!m_model->archive()->comment().isEmpty()) {
784
        m_commentView->setPlainText(m_model->archive()->comment());
785
        slotShowComment();
786 787 788 789
    } else {
        m_commentView->clear();
        m_commentBox->hide();
    }
790

791 792 793 794
    if (m_model->rowCount() == 0) {
        qCWarning(ARK) << "No entry listed by the plugin";
        displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "The archive is empty or Ark could not open its content."));
    } else if (m_model->rowCount() == 1) {
Elvis Angelaccio's avatar
Elvis Angelaccio committed
795
        if (m_model->archive()->mimeType().inherits(QStringLiteral("application/x-cd-image")) &&
796 797 798 799 800
            m_model->entryForIndex(m_model->index(0, 0))[FileName].toString() == QLatin1String("README.TXT")) {
            qCWarning(ARK) << "Detected ISO image with UDF filesystem";
            displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "Ark does not currently support ISO files with UDF filesystem."));
        }
    }
801 802 803 804
}

void Part::setReadyGui()
{
805 806
    QApplication::restoreOverrideCursor();
    m_busy = false;
807 808 809 810 811

    if (m_statusBarExtension->statusBar()) {
        m_statusBarExtension->statusBar()->hide();
    }

812 813
    m_view->setEnabled(true);
    updateActions();
814 815 816 817
}

void Part::setBusyGui()
{
818 819
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
    m_busy = true;
820 821 822 823 824

    if (m_statusBarExtension->statusBar()) {
        m_statusBarExtension->statusBar()->show();
    }

825 826
    m_view->setEnabled(false);
    updateActions();
827
}
Henrique Pinto's avatar
Henrique Pinto committed
828

829
void Part::setFileNameFromArchive()
830
{
831
    const QString prettyName = url().fileName();
832 833 834 835 836

    m_infoPanel->setPrettyFileName(prettyName);
    m_infoPanel->updateWithDefaults();

    emit setWindowCaption(prettyName);
837 838
}

Ragnar Thomsen's avatar
Ragnar Thomsen committed
839
void Part::slotOpenEntry(int mode)
Henrique Pinto's avatar
Henrique Pinto committed
840
{
841
    qCDebug(ARK) << "Opening with mode" << mode;
842

Ragnar Thomsen's avatar
Ragnar Thomsen committed
843
    QModelIndex index = m_view->selectionModel()->currentIndex();
844
    const ArchiveEntry& entry =  m_model->entryForIndex(index);
845

846 847 848 849 850
    // Don't open directories.
    if (entry[IsDirectory].toBool()) {
        return;
    }

Ragnar Thomsen's avatar
Ragnar Thomsen committed
851
    // We don't support opening symlinks.
852
    if (entry[Link].toBool()) {
Ragnar Thomsen's avatar
Ragnar Thomsen committed
853
        displayMsgWidget(KMessageWidget::Information, i18n("Ark cannot open symlinks."));
854 855 856
        return;
    }

Ragnar Thomsen's avatar
Ragnar Thomsen committed
857
    // Extract the entry.
858
    if (!entry.isEmpty()) {
859

Ragnar Thomsen's avatar
Ragnar Thomsen committed
860
        m_openFileMode = static_cast<OpenFileMode>(mode);
861 862 863 864 865 866 867 868 869 870
        KJob *job = Q_NULLPTR;

        if (m_openFileMode == Preview) {
            job = m_model->preview(entry[InternalID].toString());
            connect(job, &KJob::result, this, &Part::slotPreviewExtractedEntry);
        } else {
            const QString file = entry[InternalID].toString();
            job = (m_openFileMode == OpenFile) ? m_model->open(file) : m_model->openWith(file);
            connect(job, &KJob::result, this, &Part::slotOpenExtractedEntry);
        }
871

872 873 874
        registerJob(job);
        job->start();
    }
Henrique Pinto's avatar
Henrique Pinto committed
875 876
}

Ragnar Thomsen's avatar
Ragnar Thomsen committed
877
void Part::slotOpenExtractedEntry(KJob *job)
Henrique Pinto's avatar
Henrique Pinto committed
878
{
879
    if (!job->error()) {
880

881 882 883 884 885 886
        OpenJob *openJob = qobject_cast<OpenJob*>(job);
        Q_ASSERT(openJob);

        // Since the user could modify the file (unlike the Preview case),
        // we'll need to manually delete the temp dir in the Part destructor.
        m_tmpOpenDirList << openJob->tempDir();
887

888
        const QString fullName = openJob->validatedFilePath();
889

Ragnar Thomsen's avatar
Ragnar Thomsen committed
890 891 892 893 894 895 896 897
        bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly();

        // If archive is readonly set temporarily extracted file to readonly as
        // well so user will be notified if trying to modify and save the file.
        if (!isWritable) {
            QFile::setPermissions(fullName, QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther);
        }

898
        if (isWritable) {
Ragnar Thomsen's avatar
Ragnar Thomsen committed
899
            m_fileWatcher = new QFileSystemWatcher;
Laurent Montel's avatar
Laurent Montel committed
900
            connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &Part::slotWatchedFileModified);
901
            m_fileWatcher->addPath(fullName);
Ragnar Thomsen's avatar
Ragnar Thomsen committed
902 903
        }

904 905 906 907 908 909
        if (qobject_cast<OpenWithJob*>(job)) {
            const QList<QUrl> urls = {QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile)};
            KRun::displayOpenWithDialog(urls, widget());
        } else {
            KRun::runUrl(QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile),
                         QMimeDatabase().mimeTypeForFile(fullName).name(),
Ragnar Thomsen's avatar
Ragnar Thomsen committed
910 911
                         widget());
        }
912 913 914 915 916 917 918 919 920 921 922 923 924
    } else if (job->error() != KJob::KilledJobError) {
        KMessageBox::error(widget(), job->errorString());
    }
    setReadyGui();
}

void Part::slotPreviewExtractedEntry(KJob *job)
{
    if (!job->error()) {
        PreviewJob *previewJob = qobject_cast<PreviewJob*>(job);
        Q_ASSERT(previewJob);

        ArkViewer::view(previewJob->validatedFilePath());
Ragnar Thomsen's avatar
Ragnar Thomsen committed
925