diff --git a/autotests/kerfuffle/CMakeLists.txt b/autotests/kerfuffle/CMakeLists.txt index 00e18eb8..233ec08c 100644 --- a/autotests/kerfuffle/CMakeLists.txt +++ b/autotests/kerfuffle/CMakeLists.txt @@ -1,30 +1,35 @@ set(RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) set(JSONINTERFACE_SOURCES jsonarchiveinterface.cpp jsonparser.cpp ) add_library(jsoninterface STATIC ${JSONINTERFACE_SOURCES}) target_link_libraries(jsoninterface kerfuffle) ecm_add_tests( addtoarchivetest.cpp extracttest.cpp addtest.cpp movetest.cpp copytest.cpp createdialogtest.cpp metadatatest.cpp mimetypetest.cpp LINK_LIBRARIES testhelper kerfuffle Qt5::Test NAME_PREFIX kerfuffle-) +ecm_add_test( + adddialogtest.cpp + LINK_LIBRARIES kerfuffle Qt5::Test KF5::KIOFileWidgets + NAME_PREFIX kerfuffle-) + ecm_add_tests( jobstest.cpp LINK_LIBRARIES jsoninterface Qt5::Test NAME_PREFIX kerfuffle-) # metadatatest needs the number of plugins actually installed by an install() command. list(REMOVE_ITEM INSTALLED_KERFUFFLE_PLUGINS "") list(LENGTH INSTALLED_KERFUFFLE_PLUGINS INSTALLED_COUNT) target_compile_definitions(metadatatest PRIVATE -DPLUGINS_COUNT=${INSTALLED_COUNT}) diff --git a/autotests/kerfuffle/adddialogtest.cpp b/autotests/kerfuffle/adddialogtest.cpp new file mode 100644 index 00000000..8b32702e --- /dev/null +++ b/autotests/kerfuffle/adddialogtest.cpp @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2016 Ragnar Thomsen + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "adddialog.h" +#include "archiveformat.h" +#include "pluginmanager.h" + +#include + +#include +#include +#include +#include +#include + +using namespace Kerfuffle; + +class AddDialogTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testBasicWidgets_data(); + void testBasicWidgets(); + +private: + PluginManager m_pluginManager; +}; + +void AddDialogTest::testBasicWidgets_data() +{ + QTest::addColumn("mimeType"); + QTest::addColumn("supportsCompLevel"); + QTest::addColumn("initialCompLevel"); + QTest::addColumn("changeToCompLevel"); + + QTest::newRow("tar") << QStringLiteral("application/x-tar") << false << -1 << -1; + QTest::newRow("targzip") << QStringLiteral("application/x-compressed-tar") << true << 3 << 7; + QTest::newRow("tarbzip") << QStringLiteral("application/x-bzip-compressed-tar") << true << 3 << 7; + QTest::newRow("tarZ") << QStringLiteral("application/x-tarz") << false << -1 << -1; + QTest::newRow("tarxz") << QStringLiteral("application/x-xz-compressed-tar") << true << 3 << 7; + QTest::newRow("tarlzma") << QStringLiteral("application/x-lzma-compressed-tar") << true << 3 << 7; + QTest::newRow("tarlzop") << QStringLiteral("application/x-tzo") << true << 3 << 7; + QTest::newRow("tarlzip") << QStringLiteral("application/x-lzip-compressed-tar") << true << 3 << 7; + + const auto writeMimeTypes = m_pluginManager.supportedWriteMimeTypes(); + + if (writeMimeTypes.contains(QStringLiteral("application/zip"))) { + QTest::newRow("zip") << QStringLiteral("application/zip") << true << 3 << 7; + } else { + qDebug() << "zip format not available, skipping test."; + } + + if (writeMimeTypes.contains(QStringLiteral("application/x-7z-compressed"))) { + QTest::newRow("7z") << QStringLiteral("application/x-7z-compressed") << true << 3 << 7; + } else { + qDebug() << "7z format not available, skipping test."; + } + + if (writeMimeTypes.contains(QStringLiteral("application/x-rar"))) { + QTest::newRow("rar") << QStringLiteral("application/x-rar") << true << 2 << 5; + } else { + qDebug() << "rar format not available, skipping test."; + } + + if (writeMimeTypes.contains(QStringLiteral("application/x-lrzip-compressed-tar"))) { + QTest::newRow("tarlrzip") << QStringLiteral("application/x-lrzip-compressed-tar") << true << 3 << 7; + } else { + qDebug() << "tar.lrzip format not available, skipping test."; + } +} + +void AddDialogTest::testBasicWidgets() +{ + QFETCH(QString, mimeType); + const QMimeType mime = QMimeDatabase().mimeTypeForName(mimeType); + QFETCH(bool, supportsCompLevel); + QFETCH(int, initialCompLevel); + QFETCH(int, changeToCompLevel); + + AddDialog *dialog = new AddDialog(Q_NULLPTR, QString(), QUrl(), mime); + + dialog->slotOpenOptions(); + + auto collapsibleCompression = dialog->optionsDialog->findChild(QStringLiteral("collapsibleCompression")); + QVERIFY(collapsibleCompression); + + if (supportsCompLevel) { + // Test that collapsiblegroupbox is enabled for mimetypes that support compression levels. + QVERIFY(collapsibleCompression->isEnabled()); + + auto compLevelSlider = dialog->optionsDialog->findChild(QStringLiteral("compLevelSlider")); + QVERIFY(compLevelSlider); + + const KPluginMetaData metadata = PluginManager().preferredPluginFor(mime)->metaData(); + const ArchiveFormat archiveFormat = ArchiveFormat::fromMetadata(mime, metadata); + QVERIFY(archiveFormat.isValid()); + + // Test that min/max of slider are correct. + QCOMPARE(compLevelSlider->minimum(), archiveFormat.minCompressionLevel()); + QCOMPARE(compLevelSlider->maximum(), archiveFormat.maxCompressionLevel()); + + // Test that the slider is set to default compression level. + QCOMPARE(compLevelSlider->value(), archiveFormat.defaultCompressionLevel()); + + // Set the compression level slider. + compLevelSlider->setValue(changeToCompLevel); + } else { + // Test that collapsiblegroupbox is disabled for mimetypes that don't support compression levels. + QVERIFY(!collapsibleCompression->isEnabled()); + } + + dialog->optionsDialog->accept(); + dialog->accept(); + + if (supportsCompLevel) { + // Test that the value set by slider is exported from AddDialog. + QCOMPARE(dialog->compressionOptions()[QStringLiteral("CompressionLevel")].toInt(), changeToCompLevel); + } + + // Test that passing a compression level in ctor works. + CompressionOptions opts; + opts[QStringLiteral("CompressionLevel")] = initialCompLevel; + + dialog = new AddDialog(Q_NULLPTR, QString(), QUrl(), mime, opts); + dialog->slotOpenOptions(); + + if (supportsCompLevel) { + + auto compLevelSlider = dialog->optionsDialog->findChild(QStringLiteral("compLevelSlider")); + QVERIFY(compLevelSlider); + + // Test that slider is set to the compression level supplied in ctor. + QCOMPARE(compLevelSlider->value(), initialCompLevel); + } + dialog->optionsDialog->accept(); + dialog->accept(); +} + +QTEST_MAIN(AddDialogTest) + +#include "adddialogtest.moc" diff --git a/kerfuffle/CMakeLists.txt b/kerfuffle/CMakeLists.txt index 938f6e89..b545f527 100644 --- a/kerfuffle/CMakeLists.txt +++ b/kerfuffle/CMakeLists.txt @@ -1,66 +1,69 @@ ########### next target ############### set(kerfuffle_SRCS archiveformat.cpp archive_kerfuffle.cpp archiveinterface.cpp extractionsettingspage.cpp previewsettingspage.cpp settingspage.cpp jobs.cpp + adddialog.cpp + compressionoptionswidget.cpp createdialog.cpp extractiondialog.cpp propertiesdialog.cpp queries.cpp addtoarchive.cpp cliinterface.cpp mimetypes.cpp plugin.cpp pluginmanager.cpp archiveentry.cpp archiveentry.h ) kconfig_add_kcfg_files(kerfuffle_SRCS settings.kcfgc) ki18n_wrap_ui(kerfuffle_SRCS createdialog.ui extractiondialog.ui extractionsettings.ui previewsettings.ui propertiesdialog.ui + compressionoptionswidget.ui ) ecm_qt_declare_logging_category(kerfuffle_SRCS HEADER ark_debug.h IDENTIFIER ARK CATEGORY_NAME ark.kerfuffle) add_library(kerfuffle SHARED ${kerfuffle_SRCS}) generate_export_header(kerfuffle BASE_NAME kerfuffle) target_link_libraries(kerfuffle PUBLIC KF5::IconThemes KF5::Pty KF5::Service KF5::I18n KF5::WidgetsAddons PRIVATE Qt5::Concurrent KF5::KIOCore KF5::KIOWidgets KF5::KIOFileWidgets ) set_target_properties(kerfuffle PROPERTIES VERSION ${KERFUFFLE_VERSION_STRING} SOVERSION ${KERFUFFLE_SOVERSION}) install(TARGETS kerfuffle ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) install(FILES kerfufflePlugin.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR}) install(FILES ark.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) install(FILES mime/kerfuffle.xml DESTINATION ${KDE_INSTALL_MIMEDIR}) if(SharedMimeInfo_FOUND) update_xdg_mimetypes(${KDE_INSTALL_MIMEDIR}) endif() diff --git a/kerfuffle/adddialog.cpp b/kerfuffle/adddialog.cpp new file mode 100644 index 00000000..882f145c --- /dev/null +++ b/kerfuffle/adddialog.cpp @@ -0,0 +1,123 @@ +/* + * ark -- archiver for the KDE project + * + * Copyright (C) 2016 Ragnar Thomsen + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "adddialog.h" +#include "ark_debug.h" +#include "archiveformat.h" +#include "compressionoptionswidget.h" +#include "mimetypes.h" + +#include +#include +#include +#include +#include + +namespace Kerfuffle +{ + +AddDialog::AddDialog(QWidget *parent, + const QString &title, + const QUrl &startDir, + const QMimeType &mimeType, + const CompressionOptions &opts) + : QDialog(parent, Qt::Dialog) + , m_mimeType(mimeType) + , m_compOptions(opts) +{ + qCDebug(ARK) << "AddDialog loaded with options:" << m_compOptions; + + setWindowTitle(title); + + QVBoxLayout *vlayout = new QVBoxLayout(this); + m_fileWidget = new KFileWidget(startDir, this); + vlayout->addWidget(m_fileWidget); + + QPushButton *optionsButton = new QPushButton(QIcon::fromTheme(QStringLiteral("settings-configure")), + i18n("Advanced Options")); + optionsButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + m_fileWidget->setCustomWidget(optionsButton); + + connect(optionsButton, &QPushButton::clicked, this, &AddDialog::slotOpenOptions); + + m_fileWidget->setMode(KFile::Files | KFile::Directory | KFile::LocalOnly | KFile::ExistingOnly); + m_fileWidget->setOperationMode(KFileWidget::Opening); + + m_fileWidget->okButton()->setText(i18nc("@action:button", "Add")); + m_fileWidget->okButton()->show(); + connect(m_fileWidget->okButton(), &QPushButton::clicked, m_fileWidget, &KFileWidget::slotOk); + connect(m_fileWidget, &KFileWidget::accepted, m_fileWidget, &KFileWidget::accept); + connect(m_fileWidget, &KFileWidget::accepted, this, &QDialog::accept); + + m_fileWidget->cancelButton()->show(); + connect(m_fileWidget->cancelButton(), &QPushButton::clicked, this, &QDialog::reject); +} + +AddDialog::~AddDialog() +{ +} + +QStringList AddDialog::selectedFiles() const +{ + return m_fileWidget->selectedFiles(); +} + +CompressionOptions AddDialog::compressionOptions() const +{ + return m_compOptions; +} + +void AddDialog::slotOpenOptions() +{ + optionsDialog = new QDialog(this); + QVBoxLayout *vlayout = new QVBoxLayout(optionsDialog); + optionsDialog->setWindowTitle(i18n("Advanced Options")); + + CompressionOptionsWidget *optionsWidget = new CompressionOptionsWidget(optionsDialog, m_compOptions); + optionsWidget->setMimeType(m_mimeType); + optionsWidget->setEncryptionVisible(false); + optionsWidget->collapsibleCompression->expand(); + vlayout->addWidget(optionsWidget); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, optionsDialog); + vlayout->addWidget(buttonBox); + connect(buttonBox, &QDialogButtonBox::accepted, optionsDialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, optionsDialog, &QDialog::reject); + + optionsDialog->layout()->setSizeConstraint(QLayout::SetFixedSize); + + connect(optionsDialog, &QDialog::finished, this, [this, optionsWidget](int result) { + if (result == QDialog::Accepted) { + m_compOptions = optionsWidget->commpressionOptions(); + } + optionsDialog->deleteLater(); + }); + + optionsDialog->open(); +} + +} diff --git a/kerfuffle/adddialog.h b/kerfuffle/adddialog.h new file mode 100644 index 00000000..013b93d9 --- /dev/null +++ b/kerfuffle/adddialog.h @@ -0,0 +1,68 @@ +/* + * ark -- archiver for the KDE project + * + * Copyright (C) 2016 Ragnar Thomsen + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef ADDDIALOG_H +#define ADDDIALOG_H + +#include "kerfuffle_export.h" +#include "archive_kerfuffle.h" +#include "compressionoptionswidget.h" + +#include + +#include +#include + +class QUrl; + +namespace Kerfuffle +{ +class KERFUFFLE_EXPORT AddDialog : public QDialog +{ + Q_OBJECT +public: + explicit AddDialog(QWidget *parent, + const QString &title, + const QUrl &startDir, + const QMimeType &mimeType, + const CompressionOptions &opts = QHash()); + virtual ~AddDialog(); + QStringList selectedFiles() const; + CompressionOptions compressionOptions() const; + QDialog *optionsDialog; + +private: + KFileWidget *m_fileWidget; + QMimeType m_mimeType; + CompressionOptions m_compOptions; + +public slots: + void slotOpenOptions(); +}; +} + +#endif diff --git a/kerfuffle/archive_kerfuffle.cpp b/kerfuffle/archive_kerfuffle.cpp index 91fc2be6..7eaff32e 100644 --- a/kerfuffle/archive_kerfuffle.cpp +++ b/kerfuffle/archive_kerfuffle.cpp @@ -1,484 +1,494 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archive_kerfuffle.h" #include "archiveentry.h" #include "archiveinterface.h" #include "jobs.h" #include "mimetypes.h" #include "pluginmanager.h" #include #include #include namespace Kerfuffle { Archive *Archive::create(const QString &fileName, QObject *parent) { return create(fileName, QString(), parent); } Archive *Archive::create(const QString &fileName, const QString &fixedMimeType, QObject *parent) { qCDebug(ARK) << "Going to create archive" << fileName; PluginManager pluginManager; const QMimeType mimeType = fixedMimeType.isEmpty() ? determineMimeType(fileName) : QMimeDatabase().mimeTypeForName(fixedMimeType); const QVector offers = pluginManager.preferredPluginsFor(mimeType); if (offers.isEmpty()) { qCCritical(ARK) << "Could not find a plugin to handle" << fileName; return new Archive(NoPlugin, parent); } Archive *archive; foreach (Plugin *plugin, offers) { archive = create(fileName, plugin, parent); // Use the first valid plugin, according to the priority sorting. if (archive->isValid()) { return archive; } } qCCritical(ARK) << "Failed to find a usable plugin for" << fileName; return archive; } Archive *Archive::create(const QString &fileName, Plugin *plugin, QObject *parent) { Q_ASSERT(plugin); qCDebug(ARK) << "Checking plugin" << plugin->metaData().pluginId(); KPluginFactory *factory = KPluginLoader(plugin->metaData().fileName()).factory(); if (!factory) { qCWarning(ARK) << "Invalid plugin factory for" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } const QVariantList args = {QVariant(QFileInfo(fileName).absoluteFilePath())}; ReadOnlyArchiveInterface *iface = factory->create(Q_NULLPTR, args); if (!iface) { qCWarning(ARK) << "Could not create plugin instance" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } if (!plugin->isValid()) { qCDebug(ARK) << "Cannot use plugin" << plugin->metaData().pluginId() << "- check whether" << plugin->readOnlyExecutables() << "are installed."; return new Archive(FailedPlugin, parent); } qCDebug(ARK) << "Successfully loaded plugin" << plugin->metaData().pluginId(); return new Archive(iface, !plugin->isReadWrite(), parent); } Archive::Archive(ArchiveError errorCode, QObject *parent) : QObject(parent) , m_iface(Q_NULLPTR) , m_error(errorCode) { qCDebug(ARK) << "Created archive instance with error"; } Archive::Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent) : QObject(parent) , m_iface(archiveInterface) , m_hasBeenListed(false) , m_isReadOnly(isReadOnly) , m_isSingleFolderArchive(false) , m_extractedFilesSize(0) , m_error(NoError) , m_encryptionType(Unencrypted) , m_numberOfFiles(0) { qCDebug(ARK) << "Created archive instance"; Q_ASSERT(archiveInterface); archiveInterface->setParent(this); connect(m_iface, &ReadOnlyArchiveInterface::entry, this, &Archive::onNewEntry); } Archive::~Archive() { } QString Archive::completeBaseName() const { QString base = QFileInfo(fileName()).completeBaseName(); // Special case for compressed tar archives. if (base.right(4).toUpper() == QLatin1String(".TAR")) { base.chop(4); } return base; } QString Archive::fileName() const { return isValid() ? m_iface->filename() : QString(); } QString Archive::comment() const { return isValid() ? m_iface->comment() : QString(); } CommentJob* Archive::addComment(const QString &comment) { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to add comment:" << comment; Q_ASSERT(!isReadOnly()); CommentJob *job = new CommentJob(comment, static_cast(m_iface)); return job; } TestJob* Archive::testArchive() { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to test archive"; TestJob *job = new TestJob(m_iface); return job; } QMimeType Archive::mimeType() { if (!isValid()) { return QMimeType(); } if (!m_mimeType.isValid()) { m_mimeType = determineMimeType(fileName()); } return m_mimeType; } bool Archive::isReadOnly() const { return isValid() ? (m_iface->isReadOnly() || m_isReadOnly) : false; } bool Archive::isSingleFolderArchive() { if (!isValid()) { return false; } listIfNotListed(); return m_isSingleFolderArchive; } bool Archive::hasComment() const { return isValid() ? !comment().isEmpty() : false; } Archive::EncryptionType Archive::encryptionType() { if (!isValid()) { return Unencrypted; } listIfNotListed(); return m_encryptionType; } qulonglong Archive::numberOfFiles() { if (!isValid()) { return 0; } listIfNotListed(); return m_numberOfFiles; } qulonglong Archive::unpackedSize() { if (!isValid()) { return 0; } listIfNotListed(); return m_extractedFilesSize; } qulonglong Archive::packedSize() const { return isValid() ? QFileInfo(fileName()).size() : 0; } QString Archive::subfolderName() { if (!isValid()) { return QString(); } listIfNotListed(); return m_subfolderName; } void Archive::onNewEntry(const Archive::Entry *entry) { if (!entry->isDir()) { m_numberOfFiles++; } } bool Archive::isValid() const { return m_iface && (m_error == NoError); } ArchiveError Archive::error() const { return m_error; } KJob* Archive::open() { return 0; } KJob* Archive::create() { return 0; } ListJob* Archive::list() { if (!isValid() || !QFileInfo::exists(fileName())) { return Q_NULLPTR; } qCDebug(ARK) << "Going to list files"; ListJob *job = new ListJob(m_iface); //if this job has not been listed before, we grab the opportunity to //collect some information about the archive if (!m_hasBeenListed) { connect(job, &ListJob::result, this, &Archive::onListFinished); } return job; } DeleteJob* Archive::deleteFiles(QList &entries) { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to delete entries" << entries; if (m_iface->isReadOnly()) { return 0; } DeleteJob *newJob = new DeleteJob(entries, static_cast(m_iface)); return newJob; } AddJob* Archive::addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral("PasswordProtectedHint")] = true; } qCDebug(ARK) << "Going to add files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); AddJob *newJob = new AddJob(files, destination, newOptions, static_cast(m_iface)); connect(newJob, &AddJob::result, this, &Archive::onAddFinished); return newJob; } MoveJob* Archive::moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral("PasswordProtectedHint")] = true; } qCDebug(ARK) << "Going to move files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); MoveJob *newJob = new MoveJob(files, destination, newOptions, static_cast(m_iface)); return newJob; } CopyJob* Archive::copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral("PasswordProtectedHint")] = true; } qCDebug(ARK) << "Going to copy files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); CopyJob *newJob = new CopyJob(files, destination, newOptions, static_cast(m_iface)); return newJob; } ExtractJob* Archive::extractFiles(const QList &files, const QString &destinationDir, const ExtractionOptions &options) { if (!isValid()) { return Q_NULLPTR; } ExtractionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral( "PasswordProtectedHint" )] = true; } ExtractJob *newJob = new ExtractJob(files, destinationDir, newOptions, m_iface); return newJob; } PreviewJob *Archive::preview(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } PreviewJob *job = new PreviewJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } OpenJob *Archive::open(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } OpenJob *job = new OpenJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } OpenWithJob *Archive::openWith(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } OpenWithJob *job = new OpenWithJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } void Archive::encrypt(const QString &password, bool encryptHeader) { if (!isValid()) { return; } m_iface->setPassword(password); m_iface->setHeaderEncryptionEnabled(encryptHeader); m_encryptionType = encryptHeader ? HeaderEncrypted : Encrypted; } void Archive::onAddFinished(KJob* job) { //if the archive was previously a single folder archive and an add job //has successfully finished, then it is no longer a single folder //archive (for the current implementation, which does not allow adding //folders/files other places than the root. //TODO: handle the case of creating a new file and singlefolderarchive //then. if (m_isSingleFolderArchive && !job->error()) { m_isSingleFolderArchive = false; } } void Archive::onListFinished(KJob* job) { ListJob *ljob = qobject_cast(job); m_extractedFilesSize = ljob->extractedFilesSize(); m_isSingleFolderArchive = ljob->isSingleFolderArchive(); m_subfolderName = ljob->subfolderName(); if (m_subfolderName.isEmpty()) { m_subfolderName = completeBaseName(); } if (ljob->isPasswordProtected()) { // If we already know the password, it means that the archive is header-encrypted. m_encryptionType = m_iface->password().isEmpty() ? Encrypted : HeaderEncrypted; } m_hasBeenListed = true; } void Archive::listIfNotListed() { if (!m_hasBeenListed) { ListJob *job = list(); if (!job) { return; } connect(job, &ListJob::userQuery, this, &Archive::onUserQuery); QEventLoop loop(this); connect(job, &KJob::result, &loop, &QEventLoop::quit); job->start(); loop.exec(); // krazy:exclude=crashy } } void Archive::onUserQuery(Query* query) { query->execute(); } +void Archive::setCompressionOptions(const CompressionOptions &opts) +{ + m_compOptions = opts; +} + +CompressionOptions Archive::compressionOptions() const +{ + return m_compOptions; +} + } // namespace Kerfuffle diff --git a/kerfuffle/archive_kerfuffle.h b/kerfuffle/archive_kerfuffle.h index c9c166ce..ff683e76 100644 --- a/kerfuffle/archive_kerfuffle.h +++ b/kerfuffle/archive_kerfuffle.h @@ -1,213 +1,216 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVE_H #define ARCHIVE_H #include "kerfuffle_export.h" #include #include #include #include #include class KJob; namespace Kerfuffle { class ListJob; class ExtractJob; class DeleteJob; class AddJob; class MoveJob; class CopyJob; class CommentJob; class TestJob; class OpenJob; class OpenWithJob; class Plugin; class PreviewJob; class Query; class ReadOnlyArchiveInterface; enum ArchiveError { NoError = 0, NoPlugin, FailedPlugin }; /** These are the extra options for doing the compression. Naming convention is CamelCase with either Global, or the compression type (such as Zip, Rar, etc), followed by the property name used */ typedef QHash CompressionOptions; typedef QHash ExtractionOptions; class KERFUFFLE_EXPORT Archive : public QObject { Q_OBJECT Q_ENUMS(EncryptionType) /** * Complete base name, without the "tar" extension (if any). */ Q_PROPERTY(QString completeBaseName READ completeBaseName CONSTANT) Q_PROPERTY(QString fileName READ fileName CONSTANT) Q_PROPERTY(QString comment READ comment CONSTANT) Q_PROPERTY(QMimeType mimeType READ mimeType CONSTANT) Q_PROPERTY(bool isReadOnly READ isReadOnly CONSTANT) Q_PROPERTY(bool isSingleFolderArchive READ isSingleFolderArchive) Q_PROPERTY(EncryptionType encryptionType READ encryptionType) Q_PROPERTY(qulonglong numberOfFiles READ numberOfFiles) Q_PROPERTY(qulonglong unpackedSize READ unpackedSize) Q_PROPERTY(qulonglong packedSize READ packedSize) Q_PROPERTY(QString subfolderName READ subfolderName) public: enum EncryptionType { Unencrypted, Encrypted, HeaderEncrypted }; class Entry; QString completeBaseName() const; QString fileName() const; QString comment() const; QMimeType mimeType(); bool isReadOnly() const; bool isSingleFolderArchive(); bool hasComment() const; EncryptionType encryptionType(); qulonglong numberOfFiles(); qulonglong unpackedSize(); qulonglong packedSize() const; QString subfolderName(); + void setCompressionOptions(const CompressionOptions &opts); + CompressionOptions compressionOptions() const; static Archive *create(const QString &fileName, QObject *parent = 0); static Archive *create(const QString &fileName, const QString &fixedMimeType, QObject *parent = 0); /** * Create an archive instance from a given @p plugin. * @param fileName The name of the archive. * @return A valid archive if the plugin could be loaded, an invalid one otherwise (with the FailedPlugin error set). */ static Archive *create(const QString &fileName, Plugin *plugin, QObject *parent = Q_NULLPTR); ~Archive(); ArchiveError error() const; bool isValid() const; KJob* open(); KJob* create(); /** * @return A ListJob if the archive already exists. A null pointer otherwise. */ ListJob* list(); DeleteJob* deleteFiles(QList &entries); CommentJob* addComment(const QString &comment); TestJob* testArchive(); /** * Compression options that should be handled by all interfaces: * * GlobalWorkDir - Change to this dir before adding the new files. * The path names should then be added relative to this directory. */ AddJob* addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); /** * Renames or moves entries within the archive. * * @param files All the renamed or moved files and their child entries (for renaming a directory too). * @param destination New entry name (for renaming) or destination folder (for moving). * If ReadOnlyArchiveInterface::entriesWithoutChildren(files).count() returns 1, then it's renaming, * so you must specify the resulted entry name, even if it's not going to be changed. * Otherwise (if count is more than 1) it's moving, so destination must conatin only targeted folder path * or be empty, if moving to the root. */ MoveJob* moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); /** * Copies entries within the archive. * * @param files All the renamed or moved files and their child entries (for renaming a directory too). * @param destination Destination path. It must conatin only targeted folder path or be empty, * if copying to the root. */ CopyJob* copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); ExtractJob* extractFiles(const QList &files, const QString &destinationDir, const ExtractionOptions &options = ExtractionOptions()); PreviewJob* preview(Archive::Entry *entry); OpenJob* open(Archive::Entry *entry); OpenWithJob* openWith(Archive::Entry *entry); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encrypt(const QString &password, bool encryptHeader); private slots: void onListFinished(KJob*); void onAddFinished(KJob*); void onUserQuery(Kerfuffle::Query*); void onNewEntry(const Archive::Entry *entry); private: Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent = 0); Archive(ArchiveError errorCode, QObject *parent = 0); void listIfNotListed(); ReadOnlyArchiveInterface *m_iface; bool m_hasBeenListed; bool m_isReadOnly; bool m_isSingleFolderArchive; QString m_subfolderName; qulonglong m_extractedFilesSize; ArchiveError m_error; EncryptionType m_encryptionType; qulonglong m_numberOfFiles; + CompressionOptions m_compOptions; QMimeType m_mimeType; }; } // namespace Kerfuffle Q_DECLARE_METATYPE(Kerfuffle::Archive::EncryptionType) #endif // ARCHIVE_H diff --git a/kerfuffle/compressionoptionswidget.cpp b/kerfuffle/compressionoptionswidget.cpp new file mode 100644 index 00000000..4340009d --- /dev/null +++ b/kerfuffle/compressionoptionswidget.cpp @@ -0,0 +1,158 @@ +/* + * ark -- archiver for the KDE project + * + * Copyright (C) 2016 Ragnar Thomsen + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "compressionoptionswidget.h" +#include "ark_debug.h" +#include "archiveformat.h" +#include "pluginmanager.h" + +#include +#include + +#include + +namespace Kerfuffle +{ +CompressionOptionsWidget::CompressionOptionsWidget(QWidget *parent, + const CompressionOptions &opts) + : QWidget(parent) + , m_opts(opts) +{ + setupUi(this); + + KColorScheme colorScheme(QPalette::Active, KColorScheme::View); + pwdWidget->setBackgroundWarningColor(colorScheme.background(KColorScheme::NegativeBackground).color()); + pwdWidget->setPasswordStrengthMeterVisible(false); +} + +CompressionOptions CompressionOptionsWidget::commpressionOptions() const +{ + CompressionOptions opts; + opts[QStringLiteral("CompressionLevel")] = compLevelSlider->value(); + + return opts; +} + +int CompressionOptionsWidget::compressionLevel() const +{ + return compLevelSlider->value(); +} + +void CompressionOptionsWidget::setEncryptionVisible(bool visible) +{ + collapsibleEncryption->setVisible(visible); +} + +QString CompressionOptionsWidget::password() const +{ + return pwdWidget->password(); +} + +void CompressionOptionsWidget::updateWidgets() +{ + const KPluginMetaData metadata = PluginManager().preferredPluginFor(m_mimetype)->metaData(); + const ArchiveFormat archiveFormat = ArchiveFormat::fromMetadata(m_mimetype, metadata); + Q_ASSERT(archiveFormat.isValid()); + + if (archiveFormat.encryptionType() != Archive::Unencrypted) { + collapsibleEncryption->setEnabled(true); + collapsibleEncryption->setToolTip(QString()); + pwdWidget->setEnabled(true); + + if (archiveFormat.encryptionType() == Archive::HeaderEncrypted) { + encryptHeaderCheckBox->setEnabled(true); + encryptHeaderCheckBox->setToolTip(QString()); + } else { + encryptHeaderCheckBox->setEnabled(false); + // Show the tooltip only if the encryption is still enabled. + // This is needed because if the new filter is e.g. tar, the whole encryption group gets disabled. + if (collapsibleEncryption->isEnabled() && collapsibleEncryption->isExpanded()) { + encryptHeaderCheckBox->setToolTip(i18n("Protection of the list of files is not possible with the %1 format.", + m_mimetype.comment())); + } else { + encryptHeaderCheckBox->setToolTip(QString()); + } + } + + } else { + collapsibleEncryption->setEnabled(false); + collapsibleEncryption->setToolTip(i18n("Protection of the archive with password is not possible with the %1 format.", + m_mimetype.comment())); + pwdWidget->setEnabled(false); + encryptHeaderCheckBox->setToolTip(QString()); + } + + + if (archiveFormat.maxCompressionLevel() == 0) { + collapsibleCompression->setEnabled(false); + collapsibleCompression->setToolTip(i18n("It is not possible to set compression level for the %1 format.", + m_mimetype.comment())); + } else { + collapsibleCompression->setEnabled(true); + collapsibleCompression->setToolTip(QString()); + compLevelSlider->setMinimum(archiveFormat.minCompressionLevel()); + compLevelSlider->setMaximum(archiveFormat.maxCompressionLevel()); + if (m_opts.contains(QStringLiteral("CompressionLevel"))) { + compLevelSlider->setValue(m_opts.value(QStringLiteral("CompressionLevel")).toInt()); + } else { + compLevelSlider->setValue(archiveFormat.defaultCompressionLevel()); + } + } +} + +void CompressionOptionsWidget::setMimeType(const QMimeType &mimeType) +{ + m_mimetype = mimeType; + updateWidgets(); +} + +bool CompressionOptionsWidget::isEncryptionAvailable() const +{ + return collapsibleEncryption->isEnabled(); +} + +bool CompressionOptionsWidget::isEncryptionEnabled() const +{ + return isEncryptionAvailable() && collapsibleEncryption->isExpanded(); +} + +bool CompressionOptionsWidget::isHeaderEncryptionAvailable() const +{ + return isEncryptionEnabled() && encryptHeaderCheckBox->isEnabled(); +} + +bool CompressionOptionsWidget::isHeaderEncryptionEnabled() const +{ + return isHeaderEncryptionAvailable() && encryptHeaderCheckBox->isChecked(); +} + +KNewPasswordWidget::PasswordStatus CompressionOptionsWidget::passwordStatus() const +{ + return pwdWidget->passwordStatus(); +} + +} diff --git a/kerfuffle/createdialog.h b/kerfuffle/compressionoptionswidget.h similarity index 53% copy from kerfuffle/createdialog.h copy to kerfuffle/compressionoptionswidget.h index f3fe49d4..350aa7a4 100644 --- a/kerfuffle/createdialog.h +++ b/kerfuffle/compressionoptionswidget.h @@ -1,103 +1,67 @@ /* * ark -- archiver for the KDE project * - * Copyright (C) 2008 Harald Hvaal - * Copyright (C) 2009 Raphael Kubo da Costa - * Copyright (C) 2015 Elvis Angelaccio * Copyright (C) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#ifndef CREATEDIALOG_H -#define CREATEDIALOG_H +#ifndef COMPRESSIONOPTIONSWIDGET_H +#define COMPRESSIONOPTIONSWIDGET_H #include "kerfuffle_export.h" -#include "pluginmanager.h" +#include "archive_kerfuffle.h" +#include "ui_compressionoptionswidget.h" -#include - -#include #include - -class QUrl; -class QVBoxLayout; +#include namespace Kerfuffle { - -class KERFUFFLE_EXPORT CreateDialog : public QDialog +class KERFUFFLE_EXPORT CompressionOptionsWidget : public QWidget, public Ui::CompressionOptionsWidget { Q_OBJECT public: - explicit CreateDialog(QWidget *parent, - const QString &caption, - const QUrl &startDir); - QUrl selectedUrl() const; - QString password() const; - QMimeType currentMimeType() const; - bool setMimeType(const QString &mimeTypeName); + explicit CompressionOptionsWidget(QWidget *parent = Q_NULLPTR, + const CompressionOptions &opts = QHash()); int compressionLevel() const; - - /** - * @return Whether the user can encrypt the new archive. - */ + QString password() const; + CompressionOptions commpressionOptions() const; bool isEncryptionAvailable() const; - - /** - * @return Whether the user has chosen to encrypt the new archive. - */ bool isEncryptionEnabled() const; - - /** - * @return Whether the user can encrypt the list of files in the new archive. - */ bool isHeaderEncryptionAvailable() const; - - /** - * @return Whether the user has chosen to encrypt the list of files in the new archive. - */ bool isHeaderEncryptionEnabled() const; + KNewPasswordWidget::PasswordStatus passwordStatus() const; -public slots: - virtual void accept() Q_DECL_OVERRIDE; + void setEncryptionVisible(bool visible); + void setMimeType(const QMimeType &mimeType); private: - void loadConfiguration(); - - class CreateDialogUI *m_ui; - QVBoxLayout *m_vlayout; - KConfigGroup m_config; - QStringList m_supportedMimeTypes; - PluginManager m_pluginManger; + void updateWidgets(); -private slots: - void slotFileNameEdited(const QString &text); - void slotUpdateWidgets(int index); - void slotEncryptionToggled(); - void slotUpdateDefaultMimeType(); - void slotUpdateFilenameExtension(int index); + QMimeType m_mimetype; + CompressionOptions m_opts; }; } #endif diff --git a/kerfuffle/createdialog.ui b/kerfuffle/compressionoptionswidget.ui similarity index 56% copy from kerfuffle/createdialog.ui copy to kerfuffle/compressionoptionswidget.ui index addf2761..b0a36f4d 100644 --- a/kerfuffle/createdialog.ui +++ b/kerfuffle/compressionoptionswidget.ui @@ -1,249 +1,152 @@ - CreateDialog - + CompressionOptionsWidget + 0 0 - 502 - 369 + 384 + 62 - - - 400 - 0 - - - - - - QLayout::SetFixedSize - - - - - Folder: - - - - - - - - 0 - 0 - - - - - - - - Filename: - - - - - - - Type archive name... - - - - - - - Type: - - - - - - - - 0 - 0 - - - - - - - - Extension: - - - - - - - Automatically add filename extension (extension) - - - true - - - - - - - - - false - - - - 0 - 0 - - - - Password Protection - - - false - - - - - - false - - - - - - - false - - - Qt::LeftToRight - - - Ask for password before showing the list of files in the archive - - - true - - - - - - - false + true Compression false Min Max Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 0 Level: 3 0 + + + 300 + 0 + + 9 1 Qt::Horizontal QSlider::TicksBothSides 1 - - - Qt::Horizontal + + + true - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + 0 + 0 + + + + Password Protection + + + false + + + + + false + + + + + + + false + + + Qt::LeftToRight + + + Ask for password before showing the list of files in the archive + + + true + + + + - - KUrlRequester - QWidget -
kurlrequester.h
-
KCollapsibleGroupBox QWidget
kcollapsiblegroupbox.h
1
KNewPasswordWidget QWidget
KNewPasswordWidget
1
- - destFolderUrlRequester - filenameLineEdit - mimeComboBox - collapsibleEncryption -
diff --git a/kerfuffle/createdialog.cpp b/kerfuffle/createdialog.cpp index ba6a4756..dd21d881 100644 --- a/kerfuffle/createdialog.cpp +++ b/kerfuffle/createdialog.cpp @@ -1,286 +1,232 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * Copyright (C) 2009,2011 Raphael Kubo da Costa * Copyright (C) 2015 Elvis Angelaccio * Copyright (C) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "createdialog.h" #include "archiveformat.h" #include "ark_debug.h" #include "ui_createdialog.h" #include "kerfuffle/archive_kerfuffle.h" #include "mimetypes.h" -#include #include #include #include #include #include #include namespace Kerfuffle { class CreateDialogUI: public QWidget, public Ui::CreateDialog { public: CreateDialogUI(QWidget *parent = 0) : QWidget(parent) { setupUi(this); } }; CreateDialog::CreateDialog(QWidget *parent, const QString &caption, const QUrl &startDir) : QDialog(parent, Qt::Dialog) { qCDebug(ARK) << "CreateDialog loaded"; setWindowTitle(caption); setModal(true); m_supportedMimeTypes = m_pluginManger.supportedWriteMimeTypes(); m_vlayout = new QVBoxLayout(); setLayout(m_vlayout); m_ui = new CreateDialogUI(this); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); m_ui->destFolderUrlRequester->setMode(KFile::Directory); if (startDir.isEmpty()) { m_ui->destFolderUrlRequester->setUrl(QUrl::fromLocalFile(QDir::currentPath() + QLatin1Char('/'))); } else { m_ui->destFolderUrlRequester->setUrl(startDir); } - KColorScheme colorScheme(QPalette::Active, KColorScheme::View); - m_ui->pwdWidget->setBackgroundWarningColor(colorScheme.background(KColorScheme::NegativeBackground).color()); - m_ui->pwdWidget->setPasswordStrengthMeterVisible(false); - // Populate combobox with mimetypes. foreach (const QString &type, m_supportedMimeTypes) { m_ui->mimeComboBox->addItem(QMimeDatabase().mimeTypeForName(type).comment()); } connect(m_ui->filenameLineEdit, &QLineEdit::textChanged, this, &CreateDialog::slotFileNameEdited); - connect(m_ui->collapsibleEncryption, &KCollapsibleGroupBox::expandedChanged, this, &CreateDialog::slotEncryptionToggled); connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(this, &QDialog::accepted, this, &CreateDialog::slotUpdateDefaultMimeType); connect(m_ui->mimeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &CreateDialog::slotUpdateWidgets); connect(m_ui->mimeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &CreateDialog::slotUpdateFilenameExtension); m_vlayout->addWidget(m_ui); + m_ui->optionsWidget->setMimeType(currentMimeType()); + loadConfiguration(); layout()->setSizeConstraint(QLayout::SetFixedSize); m_ui->filenameLineEdit->setFocus(); slotUpdateFilenameExtension(m_ui->mimeComboBox->currentIndex()); } void CreateDialog::slotFileNameEdited(const QString &fileName) { const QMimeType mimeFromFileName = QMimeDatabase().mimeTypeForFile(fileName, QMimeDatabase::MatchExtension); if (m_supportedMimeTypes.contains(mimeFromFileName.name())) { setMimeType(mimeFromFileName.name()); } m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!fileName.isEmpty()); } void CreateDialog::slotUpdateWidgets(int index) { - const QMimeType mimeType = QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(index)); - const KPluginMetaData metadata = m_pluginManger.preferredPluginFor(mimeType)->metaData(); - const ArchiveFormat archiveFormat = ArchiveFormat::fromMetadata(mimeType, metadata); - Q_ASSERT(archiveFormat.isValid()); - - if (archiveFormat.encryptionType() != Archive::Unencrypted) { - m_ui->collapsibleEncryption->setEnabled(true); - m_ui->collapsibleEncryption->setToolTip(QString()); - } else { - m_ui->collapsibleEncryption->setEnabled(false); - m_ui->collapsibleEncryption->setToolTip(i18n("Protection of the archive with password is not possible with the %1 format.", - mimeType.comment())); - } - - if (archiveFormat.maxCompressionLevel() == 0) { - m_ui->collapsibleCompression->setEnabled(false); - } else { - m_ui->collapsibleCompression->setEnabled(true); - m_ui->compLevelSlider->setMinimum(archiveFormat.minCompressionLevel()); - m_ui->compLevelSlider->setMaximum(archiveFormat.maxCompressionLevel()); - m_ui->compLevelSlider->setValue(archiveFormat.defaultCompressionLevel()); - } - - slotEncryptionToggled(); + m_ui->optionsWidget->setMimeType(QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(index))); } void CreateDialog::slotUpdateFilenameExtension(int index) { m_ui->chkAddExtension->setText(i18nc("the argument is a file extension (the period is not a typo)", "Automatically add .%1", QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(index)).preferredSuffix())); } QUrl CreateDialog::selectedUrl() const { QString fileName = m_ui->filenameLineEdit->text(); QString dir = m_ui->destFolderUrlRequester->url().toLocalFile(); if (m_ui->chkAddExtension->isChecked()) { QString detectedSuffix = QMimeDatabase().suffixForFileName(m_ui->filenameLineEdit->text().trimmed()); if (!currentMimeType().suffixes().contains(detectedSuffix)) { if (!fileName.endsWith(QLatin1Char('.'))) { fileName.append(QLatin1Char('.')); } fileName.append(currentMimeType().preferredSuffix()); } } if (!dir.endsWith(QLatin1Char('/'))) { dir.append(QLatin1Char('/')); } return QUrl::fromLocalFile(dir + fileName); } int CreateDialog::compressionLevel() const { - if (m_ui->compLevelSlider->isEnabled()) { - return m_ui->compLevelSlider->value(); - } - return -1; + return m_ui->optionsWidget->compressionLevel(); } QString CreateDialog::password() const { - return m_ui->pwdWidget->password(); + return m_ui->optionsWidget->password(); } bool CreateDialog::isEncryptionAvailable() const { - return m_ui->collapsibleEncryption->isEnabled(); + return m_ui->optionsWidget->isEncryptionAvailable(); } bool CreateDialog::isEncryptionEnabled() const { - return isEncryptionAvailable() && m_ui->collapsibleEncryption->isExpanded(); + return m_ui->optionsWidget->isEncryptionEnabled(); } bool CreateDialog::isHeaderEncryptionAvailable() const { - return isEncryptionEnabled() && m_ui->encryptHeaderCheckBox->isEnabled(); + return m_ui->optionsWidget->isHeaderEncryptionAvailable(); } bool CreateDialog::isHeaderEncryptionEnabled() const { - return isHeaderEncryptionAvailable() && m_ui->encryptHeaderCheckBox->isChecked(); + return m_ui->optionsWidget->isHeaderEncryptionEnabled(); } void CreateDialog::accept() { if (!isEncryptionEnabled()) { QDialog::accept(); return; } - switch (m_ui->pwdWidget->passwordStatus()) { + switch (m_ui->optionsWidget->passwordStatus()) { case KNewPasswordWidget::WeakPassword: case KNewPasswordWidget::StrongPassword: QDialog::accept(); break; case KNewPasswordWidget::PasswordNotVerified: KMessageBox::error(Q_NULLPTR, i18n("The chosen password does not match the given verification password.")); break; default: break; } } -void CreateDialog::slotEncryptionToggled() -{ - const KPluginMetaData metadata = m_pluginManger.preferredPluginFor(currentMimeType())->metaData(); - const ArchiveFormat archiveFormat = ArchiveFormat::fromMetadata(currentMimeType(), metadata); - Q_ASSERT(archiveFormat.isValid()); - - const bool isExpanded = m_ui->collapsibleEncryption->isExpanded(); - if (isExpanded && (archiveFormat.encryptionType() == Archive::HeaderEncrypted)) { - m_ui->encryptHeaderCheckBox->setEnabled(true); - m_ui->encryptHeaderCheckBox->setToolTip(QString()); - } else { - m_ui->encryptHeaderCheckBox->setEnabled(false); - // Show the tooltip only if the encryption is still enabled. - // This is needed because if the new filter is e.g. tar, the whole encryption group gets disabled. - if (isEncryptionEnabled()) { - m_ui->encryptHeaderCheckBox->setToolTip(i18n("Protection of the list of files is not possible with the %1 format.", - currentMimeType().comment())); - } else { - m_ui->encryptHeaderCheckBox->setToolTip(QString()); - } - } - m_ui->pwdWidget->setEnabled(isExpanded); -} - void CreateDialog::slotUpdateDefaultMimeType() { m_config.writeEntry("LastMimeType", currentMimeType().name()); } void CreateDialog::loadConfiguration() { m_config = KConfigGroup(KSharedConfig::openConfig()->group("CreateDialog")); QMimeType lastUsedMime = QMimeDatabase().mimeTypeForName(m_config.readEntry("LastMimeType", QStringLiteral("application/x-compressed-tar"))); setMimeType(lastUsedMime.name()); } QMimeType CreateDialog::currentMimeType() const { Q_ASSERT(m_supportedMimeTypes.size() > m_ui->mimeComboBox->currentIndex()); return QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(m_ui->mimeComboBox->currentIndex())); } bool CreateDialog::setMimeType(const QString &mimeTypeName) { const QMimeType mimeType = QMimeDatabase().mimeTypeForName(mimeTypeName); int index = m_supportedMimeTypes.indexOf(mimeTypeName); if (index == -1) { return false; } m_ui->mimeComboBox->setCurrentIndex(index); // This is needed to make sure widgets get updated in case the mimetype is already selected. slotUpdateWidgets(index); return true; } } diff --git a/kerfuffle/createdialog.h b/kerfuffle/createdialog.h index f3fe49d4..4ba3d401 100644 --- a/kerfuffle/createdialog.h +++ b/kerfuffle/createdialog.h @@ -1,103 +1,104 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa * Copyright (C) 2015 Elvis Angelaccio * Copyright (C) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CREATEDIALOG_H #define CREATEDIALOG_H +#include "archive_kerfuffle.h" #include "kerfuffle_export.h" #include "pluginmanager.h" #include #include #include class QUrl; class QVBoxLayout; namespace Kerfuffle { class KERFUFFLE_EXPORT CreateDialog : public QDialog { Q_OBJECT public: explicit CreateDialog(QWidget *parent, const QString &caption, const QUrl &startDir); QUrl selectedUrl() const; QString password() const; QMimeType currentMimeType() const; bool setMimeType(const QString &mimeTypeName); int compressionLevel() const; /** * @return Whether the user can encrypt the new archive. */ bool isEncryptionAvailable() const; /** * @return Whether the user has chosen to encrypt the new archive. */ bool isEncryptionEnabled() const; /** * @return Whether the user can encrypt the list of files in the new archive. */ bool isHeaderEncryptionAvailable() const; /** * @return Whether the user has chosen to encrypt the list of files in the new archive. */ bool isHeaderEncryptionEnabled() const; public slots: virtual void accept() Q_DECL_OVERRIDE; private: void loadConfiguration(); class CreateDialogUI *m_ui; QVBoxLayout *m_vlayout; KConfigGroup m_config; QStringList m_supportedMimeTypes; PluginManager m_pluginManger; + CompressionOptions m_compOptions; private slots: void slotFileNameEdited(const QString &text); void slotUpdateWidgets(int index); - void slotEncryptionToggled(); void slotUpdateDefaultMimeType(); void slotUpdateFilenameExtension(int index); }; } #endif diff --git a/kerfuffle/createdialog.ui b/kerfuffle/createdialog.ui index addf2761..e0eb481c 100644 --- a/kerfuffle/createdialog.ui +++ b/kerfuffle/createdialog.ui @@ -1,249 +1,135 @@ CreateDialog 0 0 502 369 400 0 - - - QLayout::SetFixedSize - - - - - Folder: + + + + + QLayout::SetFixedSize - - - - - - - 0 - 0 - - - - - - - - Filename: - - + + + + Folder: + + + + + + + + 0 + 0 + + + + + + + + Filename: + + + + + + + Type archive name... + + + + + + + Type: + + + + + + + + 0 + 0 + + + + + + + + Extension: + + + + + + + Automatically add filename extension (extension) + + + true + + + + - - - - Type archive name... - - - - - - - Type: - - - - - - - - 0 - 0 - - - + + - - - - Extension: + + + + Qt::Horizontal - - - - Automatically add filename extension (extension) - - - true + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - false - - - - 0 - 0 - - - - Password Protection - - - false - - - - - - false - - - - - - - false - - - Qt::LeftToRight - - - Ask for password before showing the list of files in the archive - - - true - - - - - - - - - - false - - - Compression - - - false - - - - - - Min - - - - - - - Max - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 1 - 0 - - - - Level: - - - - - - - - 3 - 0 - - - - 9 - - - 1 - - - Qt::Horizontal - - - QSlider::TicksBothSides - - - 1 - - - - - - - - - - Qt::Horizontal - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - KUrlRequester QWidget
kurlrequester.h
- KCollapsibleGroupBox - QWidget -
kcollapsiblegroupbox.h
- 1 -
- - KNewPasswordWidget + Kerfuffle::CompressionOptionsWidget QWidget -
KNewPasswordWidget
+
compressionoptionswidget.h
1
destFolderUrlRequester filenameLineEdit mimeComboBox - collapsibleEncryption
diff --git a/part/ark_part.rc b/part/ark_part.rc index 62c0e688..628fb989 100644 --- a/part/ark_part.rc +++ b/part/ark_part.rc @@ -1,45 +1,44 @@ - + &Archive - &File &Settings Main Toolbar diff --git a/part/part.cpp b/part/part.cpp index 6cd84ede..d3a76508 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1365 +1,1388 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009-2012 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "part.h" #include "ark_debug.h" +#include "adddialog.h" #include "archiveformat.h" #include "archivemodel.h" #include "archiveview.h" #include "arkviewer.h" #include "dnddbusinterfaceadaptor.h" #include "infopanel.h" #include "jobtracker.h" #include "kerfuffle/extractiondialog.h" #include "kerfuffle/extractionsettingspage.h" #include "kerfuffle/jobs.h" #include "kerfuffle/settings.h" #include "kerfuffle/previewsettingspage.h" #include "kerfuffle/propertiesdialog.h" #include "pluginmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kerfuffle; -K_PLUGIN_FACTORY(Factory, registerPlugin();) +K_PLUGIN_FACTORY_WITH_JSON(Factory, "ark_part.json", registerPlugin();) namespace Ark { static quint32 s_instanceCounter = 1; Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args) : KParts::ReadWritePart(parent), m_splitter(Q_NULLPTR), m_busy(false), m_jobTracker(Q_NULLPTR) { Q_UNUSED(args) setComponentData(*createAboutData(), false); new DndExtractAdaptor(this); const QString pathName = QStringLiteral("/DndExtract/%1").arg(s_instanceCounter++); if (!QDBusConnection::sessionBus().registerObject(pathName, this)) { qCCritical(ARK) << "Could not register a D-Bus object for drag'n'drop"; } // m_vlayout is needed for later insertion of QMessageWidget QWidget *mainWidget = new QWidget; m_vlayout = new QVBoxLayout; m_model = new ArchiveModel(pathName, this); m_splitter = new QSplitter(Qt::Horizontal, parentWidget); m_view = new ArchiveView; m_infoPanel = new InfoPanel(m_model); // Add widgets for the comment field. m_commentView = new QPlainTextEdit(); m_commentView->setReadOnly(true); m_commentView->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_commentBox = new QGroupBox(i18n("Comment")); m_commentBox->hide(); QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(m_commentView); m_commentBox->setLayout(vbox); + m_messageWidget = new KMessageWidget(parentWidget); + m_messageWidget->hide(); + m_commentMsgWidget = new KMessageWidget(); m_commentMsgWidget->setText(i18n("Comment has been modified.")); m_commentMsgWidget->setMessageType(KMessageWidget::Information); m_commentMsgWidget->setCloseButtonVisible(false); m_commentMsgWidget->hide(); QAction *saveAction = new QAction(i18n("Save"), m_commentMsgWidget); m_commentMsgWidget->addAction(saveAction); connect(saveAction, &QAction::triggered, this, &Part::slotAddComment); m_commentBox->layout()->addWidget(m_commentMsgWidget); connect(m_commentView, &QPlainTextEdit::textChanged, this, &Part::slotCommentChanged); setWidget(mainWidget); mainWidget->setLayout(m_vlayout); // Configure the QVBoxLayout and add widgets m_vlayout->setContentsMargins(0,0,0,0); + m_vlayout->addWidget(m_messageWidget); m_vlayout->addWidget(m_splitter); // Vertical QSplitter for the file view and comment field. m_commentSplitter = new QSplitter(Qt::Vertical, parentWidget); m_commentSplitter->setOpaqueResize(false); m_commentSplitter->addWidget(m_view); m_commentSplitter->addWidget(m_commentBox); m_commentSplitter->setCollapsible(0, false); // Horizontal QSplitter for the file view and infopanel. m_splitter->addWidget(m_commentSplitter); m_splitter->addWidget(m_infoPanel); // Read settings from config file and show/hide infoPanel. if (!ArkSettings::showInfoPanel()) { m_infoPanel->hide(); } else { m_splitter->setSizes(ArkSettings::splitterSizes()); } setupView(); setupActions(); connect(m_model, &ArchiveModel::loadingStarted, this, &Part::slotLoadingStarted); connect(m_model, &ArchiveModel::loadingFinished, this, &Part::slotLoadingFinished); connect(m_model, &ArchiveModel::droppedFiles, this, static_cast(&Part::slotAddFiles)); connect(m_model, &ArchiveModel::error, this, &Part::slotError); connect(this, &Part::busy, this, &Part::setBusyGui); connect(this, &Part::ready, this, &Part::setReadyGui); connect(this, static_cast(&KParts::ReadOnlyPart::completed), this, &Part::setFileNameFromArchive); m_statusBarExtension = new KParts::StatusBarExtension(this); setXMLFile(QStringLiteral("ark_part.rc")); } Part::~Part() { qDeleteAll(m_tmpOpenDirList); // Only save splitterSizes if infopanel is visible, // because we don't want to store zero size for infopanel. if (m_showInfoPanelAction->isChecked()) { ArkSettings::setSplitterSizes(m_splitter->sizes()); } ArkSettings::setShowInfoPanel(m_showInfoPanelAction->isChecked()); ArkSettings::self()->save(); m_extractArchiveAction->menu()->deleteLater(); m_extractAction->menu()->deleteLater(); } void Part::slotCommentChanged() { + if (!m_model->archive()) { + return; + } + if (m_commentMsgWidget->isHidden() && m_commentView->toPlainText() != m_model->archive()->comment()) { m_commentMsgWidget->animatedShow(); } else if (m_commentMsgWidget->isVisible() && m_commentView->toPlainText() == m_model->archive()->comment()) { m_commentMsgWidget->hide(); } } KAboutData *Part::createAboutData() { return new KAboutData(QStringLiteral("ark"), i18n("ArkPart"), QStringLiteral("3.0")); } void Part::registerJob(KJob* job) { if (!m_jobTracker) { m_jobTracker = new JobTracker(widget()); m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(0), 0, true); m_jobTracker->widget(job)->show(); } m_jobTracker->registerJob(job); emit busy(); connect(job, &KJob::result, this, &Part::ready); } // TODO: KIO::mostLocalHere is used here to resolve some KIO URLs to local // paths (e.g. desktop:/), but more work is needed to support extraction // to non-local destinations. See bugs #189322 and #204323. void Part::extractSelectedFilesTo(const QString& localPath) { if (!m_model) { return; } const QUrl url = QUrl::fromUserInput(localPath, QString()); KIO::StatJob* statJob = nullptr; // Try to resolve the URL to a local path. if (!url.isLocalFile() && !url.scheme().isEmpty()) { statJob = KIO::mostLocalUrl(url); if (!statJob->exec() || statJob->error() != 0) { return; } } const QString destination = statJob ? statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : localPath; delete statJob; // The URL could not be resolved to a local path. if (!url.isLocalFile() && destination.isEmpty()) { qCWarning(ARK) << "Ark cannot extract to non-local destination:" << localPath; KMessageBox::sorry(widget(), xi18nc("@info", "Ark can only extract to local destinations.")); return; } qCDebug(ARK) << "Extract to" << destination; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; options[QStringLiteral("RemoveRootNode")] = true; options[QStringLiteral("DragAndDrop")] = true; // Create and start the ExtractJob. ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), destination, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } void Part::setupView() { m_view->setContextMenuPolicy(Qt::CustomContextMenu); m_view->setModel(m_model); m_view->setSortingEnabled(true); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::updateActions); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::selectionChanged); connect(m_view, &QTreeView::activated, this, &Part::slotActivated); connect(m_view, &QWidget::customContextMenuRequested, this, &Part::slotShowContextMenu); connect(m_model, &QAbstractItemModel::columnsInserted, this, &Part::adjustColumns); } void Part::slotActivated(QModelIndex) { // The activated signal is emitted when items are selected with the mouse, // so do nothing if CTRL or SHIFT key is pressed. if (QGuiApplication::keyboardModifiers() != Qt::ShiftModifier && QGuiApplication::keyboardModifiers() != Qt::ControlModifier) { ArkSettings::defaultOpenAction() == ArkSettings::EnumDefaultOpenAction::Preview ? slotOpenEntry(Preview) : slotOpenEntry(OpenFile); } } void Part::setupActions() { // We use a QSignalMapper for the preview, open and openwith actions. This // way we can connect all three actions to the same slot slotOpenEntry and // pass the OpenFileMode as argument to the slot. m_signalMapper = new QSignalMapper; m_showInfoPanelAction = new KToggleAction(i18nc("@action:inmenu", "Show information panel"), this); actionCollection()->addAction(QStringLiteral( "show-infopanel" ), m_showInfoPanelAction); m_showInfoPanelAction->setChecked(ArkSettings::showInfoPanel()); connect(m_showInfoPanelAction, &QAction::triggered, this, &Part::slotToggleInfoPanel); m_saveAsAction = actionCollection()->addAction(KStandardAction::SaveAs, QStringLiteral("ark_file_save_as"), this, SLOT(slotSaveAs())); m_openFileAction = actionCollection()->addAction(QStringLiteral("openfile")); m_openFileAction->setText(i18nc("open a file with external program", "&Open")); m_openFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with the associated application")); connect(m_openFileAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_openFileAction, OpenFile); m_openFileWithAction = actionCollection()->addAction(QStringLiteral("openfilewith")); m_openFileWithAction->setText(i18nc("open a file with external program", "Open &With...")); m_openFileWithAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileWithAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with an external program")); connect(m_openFileWithAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_openFileWithAction, OpenFileWith); m_previewAction = actionCollection()->addAction(QStringLiteral("preview")); m_previewAction->setText(i18nc("to preview a file inside an archive", "Pre&view")); m_previewAction->setIcon(QIcon::fromTheme(QStringLiteral("document-preview-archive"))); m_previewAction->setToolTip(i18nc("@info:tooltip", "Click to preview the selected file")); actionCollection()->setDefaultShortcut(m_previewAction, Qt::CTRL + Qt::Key_P); connect(m_previewAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_previewAction, Preview); m_extractArchiveAction = actionCollection()->addAction(QStringLiteral("extract_all")); m_extractArchiveAction->setText(i18nc("@action:inmenu", "E&xtract All")); m_extractArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); m_extractArchiveAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose how to extract all the files in the archive")); actionCollection()->setDefaultShortcut(m_extractArchiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_E); connect(m_extractArchiveAction, &QAction::triggered, this, &Part::slotExtractArchive); m_extractAction = actionCollection()->addAction(QStringLiteral("extract")); m_extractAction->setText(i18nc("@action:inmenu", "&Extract")); m_extractAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); actionCollection()->setDefaultShortcut(m_extractAction, Qt::CTRL + Qt::Key_E); m_extractAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose to extract either all files or just the selected ones")); connect(m_extractAction, &QAction::triggered, this, &Part::slotShowExtractionDialog); m_addFilesAction = actionCollection()->addAction(QStringLiteral("add")); m_addFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert"))); - m_addFilesAction->setText(i18n("Add &File...")); + m_addFilesAction->setText(i18n("Add &Files...")); m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); connect(m_addFilesAction, SIGNAL(triggered(bool)), this, SLOT(slotAddFiles())); - m_addDirAction = actionCollection()->addAction(QStringLiteral("add-dir")); - m_addDirAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert-directory"))); - m_addDirAction->setText(i18n("Add Fo&lder...")); - m_addDirAction->setToolTip(i18nc("@info:tooltip", "Click to add a folder to the archive")); - connect(m_addDirAction, &QAction::triggered, - this, &Part::slotAddDir); - m_deleteFilesAction = actionCollection()->addAction(QStringLiteral("delete")); m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove"))); m_deleteFilesAction->setText(i18n("De&lete")); actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete); m_deleteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to delete the selected files")); connect(m_deleteFilesAction, &QAction::triggered, this, &Part::slotDeleteFiles); m_propertiesAction = actionCollection()->addAction(QStringLiteral("properties")); m_propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); m_propertiesAction->setText(i18nc("@action:inmenu", "&Properties")); actionCollection()->setDefaultShortcut(m_propertiesAction, Qt::ALT + Qt::Key_Return); m_propertiesAction->setToolTip(i18nc("@info:tooltip", "Click to see properties for archive")); connect(m_propertiesAction, &QAction::triggered, this, &Part::slotShowProperties); m_editCommentAction = actionCollection()->addAction(QStringLiteral("edit_comment")); m_editCommentAction->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); actionCollection()->setDefaultShortcut(m_editCommentAction, Qt::ALT + Qt::Key_C); m_editCommentAction->setToolTip(i18nc("@info:tooltip", "Click to add or edit comment")); connect(m_editCommentAction, &QAction::triggered, this, &Part::slotShowComment); m_testArchiveAction = actionCollection()->addAction(QStringLiteral("test_archive")); m_testArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("checkmark"))); m_testArchiveAction->setText(i18nc("@action:inmenu", "&Test Integrity")); actionCollection()->setDefaultShortcut(m_testArchiveAction, Qt::ALT + Qt::Key_T); m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity")); connect(m_testArchiveAction, &QAction::triggered, this, &Part::slotTestArchive); connect(m_signalMapper, static_cast(&QSignalMapper::mapped), this, &Part::slotOpenEntry); updateActions(); updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); } void Part::updateActions() { bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); int selectedEntriesCount = m_view->selectionModel()->selectedRows().count(); // Figure out if entry size is larger than preview size limit. const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024; const bool limit = ArkSettings::limitPreviewFileSize(); bool isPreviewable = (!limit || (limit && entry != Q_NULLPTR && entry->property("size").toLongLong() < maxPreviewSize)); m_previewAction->setEnabled(!isBusy() && isPreviewable && !entry->isDir() && (selectedEntriesCount == 1)); m_extractArchiveAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_extractAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_saveAsAction->setEnabled(!isBusy() && m_model->rowCount() > 0); m_addFilesAction->setEnabled(!isBusy() && isWritable); - m_addDirAction->setEnabled(!isBusy() && - isWritable); m_deleteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_openFileAction->setEnabled(!isBusy() && isPreviewable && !entry->isDir() && (selectedEntriesCount == 1)); m_openFileWithAction->setEnabled(!isBusy() && isPreviewable && !entry->isDir() && (selectedEntriesCount == 1)); m_propertiesAction->setEnabled(!isBusy() && m_model->archive()); m_commentView->setEnabled(!isBusy()); m_commentMsgWidget->setEnabled(!isBusy()); m_editCommentAction->setEnabled(false); m_testArchiveAction->setEnabled(false); if (m_model->archive()) { const KPluginMetaData metadata = PluginManager().preferredPluginFor(m_model->archive()->mimeType())->metaData(); bool supportsWriteComment = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsWriteComment(); m_editCommentAction->setEnabled(!isBusy() && supportsWriteComment); m_commentView->setReadOnly(!supportsWriteComment); m_editCommentAction->setText(m_model->archive()->hasComment() ? i18nc("@action:inmenu mutually exclusive with Add &Comment", "Edit &Comment") : i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); bool supportsTesting = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsTesting(); m_testArchiveAction->setEnabled(!isBusy() && supportsTesting); } else { m_commentView->setReadOnly(true); m_editCommentAction->setText(i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); } } void Part::slotShowComment() { if (!m_commentBox->isVisible()) { m_commentBox->show(); m_commentSplitter->setSizes(QList() << m_view->height() * 0.6 << 1); } m_commentView->setFocus(); } void Part::slotAddComment() { CommentJob *job = m_model->archive()->addComment(m_commentView->toPlainText()); if (!job) { return; } registerJob(job); job->start(); m_commentMsgWidget->hide(); if (m_commentView->toPlainText().isEmpty()) { m_commentBox->hide(); } } void Part::slotTestArchive() { TestJob *job = m_model->archive()->testArchive(); if (!job) { return; } registerJob(job); connect(job, &KJob::result, this, &Part::slotTestingDone); job->start(); } +void Part::resetGui() +{ + m_messageWidget->hide(); + m_commentView->clear(); + m_commentBox->hide(); +} + void Part::slotTestingDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else if (static_cast(job)->testSucceeded()) { KMessageBox::information(widget(), i18n("The archive passed the integrity test."), i18n("Test Results")); } else { KMessageBox::error(widget(), i18n("The archive failed the integrity test."), i18n("Test Results")); } } void Part::updateQuickExtractMenu(QAction *extractAction) { if (!extractAction) { return; } QMenu *menu = extractAction->menu(); if (!menu) { menu = new QMenu(); extractAction->setMenu(menu); connect(menu, &QMenu::triggered, this, &Part::slotQuickExtractFiles); // Remember to keep this action's properties as similar to // extractAction's as possible (except where it does not make // sense, such as the text or the shortcut). QAction *extractTo = menu->addAction(i18n("Extract To...")); extractTo->setIcon(extractAction->icon()); extractTo->setToolTip(extractAction->toolTip()); if (extractAction == m_extractArchiveAction) { connect(extractTo, &QAction::triggered, this, &Part::slotExtractArchive); } else { connect(extractTo, &QAction::triggered, this, &Part::slotShowExtractionDialog); } menu->addSeparator(); QAction *header = menu->addAction(i18n("Quick Extract To...")); header->setEnabled(false); header->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); } while (menu->actions().size() > 3) { menu->removeAction(menu->actions().last()); } const KConfigGroup conf(KSharedConfig::openConfig(), "ExtractDialog"); const QStringList dirHistory = conf.readPathEntry("DirHistory", QStringList()); for (int i = 0; i < qMin(10, dirHistory.size()); ++i) { const QString dir = QUrl(dirHistory.value(i)).toString(QUrl::RemoveScheme | QUrl::NormalizePathSegments | QUrl::PreferLocalFile); if (QDir(dir).exists()) { QAction *newAction = menu->addAction(dir); newAction->setData(dir); } } } void Part::slotQuickExtractFiles(QAction *triggeredAction) { // #190507: triggeredAction->data.isNull() means it's the "Extract to..." // action, and we do not want it to run here if (!triggeredAction->data().isNull()) { const QString userDestination = triggeredAction->data().toString(); qCDebug(ARK) << "Extract to user dest" << userDestination; QString finalDestinationDirectory; const QString detectedSubfolder = detectSubfolder(); qCDebug(ARK) << "Detected subfolder" << detectedSubfolder; if (!isSingleFolderArchive()) { finalDestinationDirectory = userDestination + QDir::separator() + detectedSubfolder; QDir(userDestination).mkdir(detectedSubfolder); } else { finalDestinationDirectory = userDestination; } qCDebug(ARK) << "Extract to final dest" << finalDestinationDirectory; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; QList files = filesAndRootNodesForIndexes(m_view->selectionModel()->selectedRows()); ExtractJob *job = m_model->extractFiles(files, finalDestinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } } void Part::selectionChanged() { m_infoPanel->setIndexes(m_view->selectionModel()->selectedRows()); } bool Part::openFile() { qCDebug(ARK) << "Attempting to open archive" << localFilePath(); + resetGui(); + if (!isLocalFileValid()) { return false; } const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; QScopedPointer archive(Kerfuffle::Archive::create(localFilePath(), fixedMimeType, m_model)); Q_ASSERT(archive); if (archive->error() == NoPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. No suitable plugin found." "Ark does not seem to support this file type.", QFileInfo(localFilePath()).fileName())); return false; } if (archive->error() == FailedPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. Failed to load a suitable plugin." "Make sure any executables needed to handle the archive type are installed.", QFileInfo(localFilePath()).fileName())); return false; } Q_ASSERT(archive->isValid()); // Plugin loaded successfully. KJob *job = m_model->setArchive(archive.take()); if (job) { registerJob(job); job->start(); } else { updateActions(); } m_infoPanel->setIndex(QModelIndex()); if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) { QTimer::singleShot(0, this, &Part::slotShowExtractionDialog); } const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")]; if (!password.isEmpty()) { m_model->encryptArchive(password, arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true")); } return true; } bool Part::saveFile() { return true; } bool Part::isBusy() const { return m_busy; } KConfigSkeleton *Part::config() const { return ArkSettings::self(); } QList Part::settingsPages(QWidget *parent) const { QList pages; pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction Settings"), QStringLiteral("archive-extract"))); pages.append(new PreviewSettingsPage(parent, i18nc("@title:tab", "Preview Settings"), QStringLiteral("document-preview-archive"))); return pages; } bool Part::isLocalFileValid() { const QString localFile = localFilePath(); const QFileInfo localFileInfo(localFile); const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); if (localFileInfo.isDir()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "%1 is a directory.", localFile)); return false; } if (creatingNewArchive) { if (localFileInfo.exists()) { if (!confirmAndDelete(localFile)) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Could not overwrite %1. Check whether you have write permission.", localFile)); return false; } } displayMsgWidget(KMessageWidget::Information, xi18nc("@info", "The archive %1 will be created as soon as you add a file.", localFile)); } else { if (!localFileInfo.exists()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 was not found.", localFile)); return false; } if (!localFileInfo.isReadable()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 could not be loaded, as it was not possible to read from it.", localFile)); return false; } } return true; } bool Part::confirmAndDelete(const QString &targetFile) { QFileInfo targetInfo(targetFile); const auto buttonCode = KMessageBox::warningYesNo(widget(), xi18nc("@info", "The archive %1 already exists. Do you wish to overwrite it?", targetInfo.fileName()), i18nc("@title:window", "File Exists"), KGuiItem(i18nc("@action:button", "Overwrite")), KStandardGuiItem::cancel()); if (buttonCode != KMessageBox::Yes || !targetInfo.isWritable()) { return false; } qCDebug(ARK) << "Removing file" << targetFile; return QFile(targetFile).remove(); } void Part::slotLoadingStarted() { } void Part::slotLoadingFinished(KJob *job) { if (job->error()) { if (arguments().metaData()[QStringLiteral("createNewArchive")] != QLatin1String("true")) { if (job->error() != KJob::KilledJobError) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive %1 failed with the following error:%2", localFilePath(), job->errorText())); } // The file failed to open, so reset the open archive, info panel and caption. m_model->setArchive(Q_NULLPTR); m_infoPanel->setPrettyFileName(QString()); m_infoPanel->updateWithDefaults(); emit setWindowCaption(QString()); } } m_view->sortByColumn(0, Qt::AscendingOrder); // #303708: expand the first level only when there is just one root folder. // Typical use case: an archive with source files. if (m_view->model()->rowCount() == 1) { m_view->expandToDepth(0); } // After loading all files, resize the columns to fit all fields m_view->header()->resizeSections(QHeaderView::ResizeToContents); updateActions(); if (!m_model->archive()) { return; } if (!m_model->archive()->comment().isEmpty()) { m_commentView->setPlainText(m_model->archive()->comment()); slotShowComment(); } else { m_commentView->clear(); m_commentBox->hide(); } if (m_model->rowCount() == 0) { qCWarning(ARK) << "No entry listed by the plugin"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "The archive is empty or Ark could not open its content.")); } else if (m_model->rowCount() == 1) { if (m_model->archive()->mimeType().inherits(QStringLiteral("application/x-cd-image")) && m_model->entryForIndex(m_model->index(0, 0))->fullPath() == QLatin1String("README.TXT")) { qCWarning(ARK) << "Detected ISO image with UDF filesystem"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "Ark does not currently support ISO files with UDF filesystem.")); } } } void Part::setReadyGui() { QApplication::restoreOverrideCursor(); m_busy = false; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->hide(); } m_view->setEnabled(true); updateActions(); } void Part::setBusyGui() { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); m_busy = true; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->show(); } m_view->setEnabled(false); updateActions(); } void Part::setFileNameFromArchive() { const QString prettyName = url().fileName(); m_infoPanel->setPrettyFileName(prettyName); m_infoPanel->updateWithDefaults(); emit setWindowCaption(prettyName); } void Part::slotOpenEntry(int mode) { qCDebug(ARK) << "Opening with mode" << mode; QModelIndex index = m_view->selectionModel()->currentIndex(); Archive::Entry *entry = m_model->entryForIndex(index); // Don't open directories. if (entry->isDir()) { return; } // We don't support opening symlinks. if (!entry->property("link").toString().isEmpty()) { displayMsgWidget(KMessageWidget::Information, i18n("Ark cannot open symlinks.")); return; } // Extract the entry. if (!entry->fullPath().isEmpty()) { m_openFileMode = static_cast(mode); KJob *job = Q_NULLPTR; if (m_openFileMode == Preview) { job = m_model->preview(entry); connect(job, &KJob::result, this, &Part::slotPreviewExtractedEntry); } else { job = (m_openFileMode == OpenFile) ? m_model->open(entry) : m_model->openWith(entry); connect(job, &KJob::result, this, &Part::slotOpenExtractedEntry); } registerJob(job); job->start(); } } void Part::slotOpenExtractedEntry(KJob *job) { if (!job->error()) { OpenJob *openJob = qobject_cast(job); Q_ASSERT(openJob); // Since the user could modify the file (unlike the Preview case), // we'll need to manually delete the temp dir in the Part destructor. m_tmpOpenDirList << openJob->tempDir(); const QString fullName = openJob->validatedFilePath(); bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); // If archive is readonly set temporarily extracted file to readonly as // well so user will be notified if trying to modify and save the file. if (!isWritable) { QFile::setPermissions(fullName, QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther); } if (isWritable) { m_fileWatcher = new QFileSystemWatcher; connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &Part::slotWatchedFileModified); m_fileWatcher->addPath(fullName); } if (qobject_cast(job)) { const QList urls = {QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile)}; KRun::displayOpenWithDialog(urls, widget()); } else { KRun::runUrl(QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile), QMimeDatabase().mimeTypeForFile(fullName).name(), widget()); } } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotPreviewExtractedEntry(KJob *job) { if (!job->error()) { PreviewJob *previewJob = qobject_cast(job); Q_ASSERT(previewJob); ArkViewer::view(previewJob->validatedFilePath()); } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotWatchedFileModified(const QString& file) { qCDebug(ARK) << "Watched file modified:" << file; // Find the relative path of the file within the archive. QString relPath = file; foreach (QTemporaryDir *tmpDir, m_tmpOpenDirList) { relPath.remove(tmpDir->path()); //Remove tmpDir. } relPath = relPath.mid(1); //Remove leading slash. if (relPath.contains(QLatin1Char('/'))) { relPath = relPath.section(QLatin1Char('/'), 0, -2); //Remove filename. } else { // File is in the root of the archive, no path. relPath = QString(); } // Set up a string for display in KMessageBox. QString prettyFilename; if (relPath.isEmpty()) { prettyFilename = file.section(QLatin1Char('/'), -1); } else { prettyFilename = relPath + QLatin1Char('/') + file.section(QLatin1Char('/'), -1); } if (KMessageBox::questionYesNo(widget(), xi18n("The file %1 was modified. Do you want to update the archive?", prettyFilename), i18nc("@title:window", "File Modified")) == KMessageBox::Yes) { QStringList list = QStringList() << file; qCDebug(ARK) << "Updating file" << file << "with path" << relPath; slotAddFiles(list, relPath); } // This is needed because some apps, such as Kate, delete and recreate // files when saving. m_fileWatcher->addPath(file); } void Part::slotError(const QString& errorMessage, const QString& details) { if (details.isEmpty()) { KMessageBox::error(widget(), errorMessage); } else { KMessageBox::detailedError(widget(), errorMessage, details); } } bool Part::isSingleFolderArchive() const { return m_model->archive()->isSingleFolderArchive(); } QString Part::detectSubfolder() const { if (!m_model) { return QString(); } return m_model->archive()->subfolderName(); } void Part::slotExtractArchive() { if (m_view->selectionModel()->selectedRows().count() > 0) { m_view->selectionModel()->clear(); } slotShowExtractionDialog(); } void Part::slotShowExtractionDialog() { if (!m_model) { return; } QPointer dialog(new Kerfuffle::ExtractionDialog); dialog.data()->setModal(true); if (m_view->selectionModel()->selectedRows().count() > 0) { dialog.data()->setShowSelectedFiles(true); } dialog.data()->setSingleFolderArchive(isSingleFolderArchive()); dialog.data()->setSubfolder(detectSubfolder()); dialog.data()->setCurrentUrl(QUrl::fromLocalFile(QFileInfo(m_model->archive()->fileName()).absolutePath())); dialog.data()->show(); dialog.data()->restoreWindowSize(); if (dialog.data()->exec()) { updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); QList files; // If the user has chosen to extract only selected entries, fetch these // from the QTreeView. if (!dialog.data()->extractAllFiles()) { files = filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); } qCDebug(ARK) << "Selected " << files; Kerfuffle::ExtractionOptions options; if (dialog.data()->preservePaths()) { options[QStringLiteral("PreservePaths")] = true; } options[QStringLiteral("FollowExtractionDialogSettings")] = true; const QString destinationDirectory = dialog.data()->destinationDirectory().toDisplayString(QUrl::PreferLocalFile); ExtractJob *job = m_model->extractFiles(files, destinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } delete dialog.data(); } QModelIndexList Part::addChildren(const QModelIndexList &list) const { Q_ASSERT(m_model); QModelIndexList ret = list; // Iterate over indexes in list and add all children. for (int i = 0; i < ret.size(); ++i) { QModelIndex index = ret.at(i); for (int j = 0; j < m_model->rowCount(index); ++j) { QModelIndex child = m_model->index(j, 0, index); if (!ret.contains(child)) { ret << child; } } } return ret; } QList Part::filesForIndexes(const QModelIndexList& list) const { QList ret; foreach(const QModelIndex& index, list) { ret << m_model->entryForIndex(index); } return ret; } QList Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { QList fileList; QStringList fullPathsList; foreach (const QModelIndex& index, list) { // Find the topmost unselected parent. This is done by iterating up // through the directory hierarchy and see if each parent is included // in the selection OR if the parent is already part of list. // The latter is needed for unselected folders which are subfolders of // a selected parent folder. QModelIndex selectionRoot = index.parent(); while (m_view->selectionModel()->isSelected(selectionRoot) || list.contains(selectionRoot)) { selectionRoot = selectionRoot.parent(); } // Fetch the root node for the unselected parent. const QString rootFileName = selectionRoot.isValid() ? m_model->entryForIndex(selectionRoot)->fullPath() : QString(); // Append index with root node to fileList. QModelIndexList alist = QModelIndexList() << index; foreach (Archive::Entry *entry, filesForIndexes(alist)) { const QString fullPath = entry->fullPath(); if (!fullPathsList.contains(fullPath)) { entry->rootNode = rootFileName; fileList.append(entry); fullPathsList.append(fullPath); } } } return fileList; } void Part::slotExtractionDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { ExtractJob *extractJob = qobject_cast(job); Q_ASSERT(extractJob); const bool followExtractionDialogSettings = extractJob->extractionOptions().value(QStringLiteral("FollowExtractionDialogSettings"), false).toBool(); if (!followExtractionDialogSettings) { return; } if (ArkSettings::openDestinationFolderAfterExtraction()) { qCDebug(ARK) << "Shall open" << extractJob->destinationDirectory(); QUrl destinationDirectory = QUrl::fromLocalFile(extractJob->destinationDirectory()).adjusted(QUrl::NormalizePathSegments); qCDebug(ARK) << "Shall open URL" << destinationDirectory; KRun::runUrl(destinationDirectory, QStringLiteral("inode/directory"), widget()); } if (ArkSettings::closeAfterExtraction()) { emit quit(); } } } void Part::adjustColumns() { m_view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); } void Part::slotAddFiles(const QStringList& filesToAdd, const QString& path) { if (filesToAdd.isEmpty()) { return; } qCDebug(ARK) << "Adding " << filesToAdd << " to " << path; // GlobalWorkDir is used by AddJob and should contain the part of the // absolute path of files to be added that should NOT be included in the // directory structure within the archive. // Example: We add file "/home/user/somedir/somefile.txt" and want the file // to have the relative path within the archive "somedir/somefile.txt". // GlobalWorkDir is then: "/home/user" QString globalWorkDir = filesToAdd.first(); // path represents the path of the file within the archive. This needs to // be removed from globalWorkDir, otherwise the files will be added to the // root of the archive. In the example above, path would be "somedir/". if (!path.isEmpty()) { globalWorkDir.remove(path); } // Remove trailing slash (needed when adding dirs). if (globalWorkDir.right(1) == QLatin1String("/")) { globalWorkDir.chop(1); } + CompressionOptions options(m_model->archive()->compressionOptions()); + // Now take the absolute path of the parent directory. globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath(); qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir; - CompressionOptions options; options[QStringLiteral("GlobalWorkDir")] = globalWorkDir; - if (arguments().metaData().contains(QStringLiteral("compressionLevel"))) { - options[QStringLiteral("CompressionLevel")] = arguments().metaData()[QStringLiteral("compressionLevel")]; - } - foreach (const QString& file, filesToAdd) { m_jobTempEntries.push_back(new Archive::Entry(Q_NULLPTR, file)); } AddJob *job = m_model->addFiles(m_jobTempEntries, Q_NULLPTR, options); if (!job) { return; } connect(job, &KJob::result, this, &Part::slotAddFilesDone); registerJob(job); job->start(); } void Part::slotAddFiles() { + // If compression options are already set, we dont use the values from CreateDialog. + CompressionOptions opts; + if (m_model->archive()->compressionOptions().isEmpty()) { + if (arguments().metaData().contains(QStringLiteral("compressionLevel"))) { + opts[QStringLiteral("CompressionLevel")] = arguments().metaData()[QStringLiteral("compressionLevel")]; + } + m_model->archive()->setCompressionOptions(opts); + } else { + opts = m_model->archive()->compressionOptions(); + } + + qCDebug(ARK) << "Opening AddDialog with opts:" << opts; + // #264819: passing widget() as the parent will not work as expected. // KFileDialog will create a KFileWidget, which runs an internal // event loop to stat the given directory. This, in turn, leads to // events being delivered to widget(), which is a QSplitter, which // in turn reimplements childEvent() and will end up calling // QWidget::show() on the KFileDialog (thus showing it in a // non-modal state). // When KFileDialog::exec() is called, the widget is already shown // and nothing happens. - const QStringList filesToAdd = QFileDialog::getOpenFileNames(widget(), i18nc("@title:window", "Add Files")); - - slotAddFiles(filesToAdd); -} - -void Part::slotAddDir() -{ - const QString dirToAdd = QFileDialog::getExistingDirectory(widget(), i18nc("@title:window", "Add Folder")); + QPointer dlg = new AddDialog(widget(), + i18nc("@title:window", "Add Files"), + m_lastUsedAddPath, + m_model->archive()->mimeType(), + opts); - if (!dirToAdd.isEmpty()) { - slotAddFiles(QStringList() << dirToAdd); + if (dlg->exec() == QDialog::Accepted) { + qCDebug(ARK) << "Selected files:" << dlg->selectedFiles(); + qCDebug(ARK) << "Options:" << dlg->compressionOptions(); + m_model->archive()->setCompressionOptions(dlg->compressionOptions()); + slotAddFiles(dlg->selectedFiles(), QString()); } + delete dlg; } void Part::slotAddFilesDone(KJob* job) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); + } else { + // Hide the "archive will be created as soon as you add a file" message. + m_messageWidget->hide(); } } void Part::slotDeleteFilesDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } } void Part::slotDeleteFiles() { const int selectionsCount = m_view->selectionModel()->selectedRows().count(); const auto reallyDelete = KMessageBox::questionYesNo(widget(), i18ncp("@info", "Deleting this file is not undoable. Are you sure you want to do this?", "Deleting these files is not undoable. Are you sure you want to do this?", selectionsCount), i18ncp("@title:window", "Delete File", "Delete Files", selectionsCount), KStandardGuiItem::del(), KStandardGuiItem::no(), QString(), KMessageBox::Dangerous | KMessageBox::Notify); if (reallyDelete == KMessageBox::No) { return; } DeleteJob *job = m_model->deleteFiles(filesForIndexes(addChildren(m_view->selectionModel()->selectedRows()))); connect(job, &KJob::result, this, &Part::slotDeleteFilesDone); registerJob(job); job->start(); } void Part::slotShowProperties() { QPointer dialog(new Kerfuffle::PropertiesDialog(0, m_model->archive())); dialog.data()->show(); } void Part::slotToggleInfoPanel(bool visible) { if (visible) { m_splitter->setSizes(ArkSettings::splitterSizes()); m_infoPanel->show(); } else { // We need to save the splitterSizes before hiding, otherwise // Ark won't remember resizing done by the user. ArkSettings::setSplitterSizes(m_splitter->sizes()); m_infoPanel->hide(); } } void Part::slotSaveAs() { QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18nc("@title:window", "Save Archive As"), url()); if ((saveUrl.isValid()) && (!saveUrl.isEmpty())) { auto statJob = KIO::stat(saveUrl, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(statJob, widget()); if (statJob->exec()) { int overwrite = KMessageBox::warningContinueCancel(widget(), xi18nc("@info", "An archive named %1 already exists. Are you sure you want to overwrite it?", saveUrl.fileName()), QString(), KStandardGuiItem::overwrite()); if (overwrite != KMessageBox::Continue) { return; } } QUrl srcUrl = QUrl::fromLocalFile(localFilePath()); if (!QFile::exists(localFilePath())) { if (url().isLocalFile()) { KMessageBox::error(widget(), xi18nc("@info", "The archive %1 cannot be copied to the specified location. The archive does not exist anymore.", localFilePath())); return; } else { srcUrl = url(); } } KIO::Job *copyJob = KIO::file_copy(srcUrl, saveUrl, -1, KIO::Overwrite); KJobWidgets::setWindow(copyJob, widget()); copyJob->exec(); if (copyJob->error()) { KMessageBox::error(widget(), xi18nc("@info", "The archive could not be saved as %1. Try saving it to another location.", saveUrl.path())); } } } void Part::slotShowContextMenu() { if (!factory()) { return; } QMenu *popup = static_cast(factory()->container(QStringLiteral("context_menu"), this)); popup->popup(QCursor::pos()); } void Part::displayMsgWidget(KMessageWidget::MessageType type, const QString& msg) { - KMessageWidget *msgWidget = new KMessageWidget(); - msgWidget->setText(msg); - msgWidget->setMessageType(type); - m_vlayout->insertWidget(0, msgWidget); - msgWidget->animatedShow(); + // The widget could be already visible, so hide it. + m_messageWidget->hide(); + m_messageWidget->setText(msg); + m_messageWidget->setMessageType(type); + m_messageWidget->animatedShow(); } } // namespace Ark #include "part.moc" diff --git a/part/part.h b/part/part.h index a11b92df..1c206811 100644 --- a/part/part.h +++ b/part/part.h @@ -1,183 +1,184 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef PART_H #define PART_H #include "interface.h" #include "kerfuffle/archiveentry.h" #include #include #include #include #include class ArchiveModel; class InfoPanel; class KAboutData; class KAbstractWidgetJobTracker; class KJob; class KToggleAction; class QAction; class QSplitter; class QTreeView; class QTemporaryDir; class QVBoxLayout; class QSignalMapper; class QFileSystemWatcher; class QGroupBox; class QPlainTextEdit; namespace Ark { class Part: public KParts::ReadWritePart, public Interface { Q_OBJECT Q_INTERFACES(Interface) public: enum OpenFileMode { Preview, OpenFile, OpenFileWith }; Part(QWidget *parentWidget, QObject *parent, const QVariantList &); ~Part(); static KAboutData *createAboutData(); bool openFile() Q_DECL_OVERRIDE; bool saveFile() Q_DECL_OVERRIDE; bool isBusy() const Q_DECL_OVERRIDE; KConfigSkeleton *config() const Q_DECL_OVERRIDE; QList settingsPages(QWidget *parent) const Q_DECL_OVERRIDE; /** * Validate the localFilePath() associated to this part. * If the file is not valid, an error message is displayed to the user. * @return Whether the localFilePath() can be loaded by the part. */ bool isLocalFileValid(); /** * Ask the user whether to overwrite @p targetFile, when creating a new archive with the same path. * @return True if the file has been successfully removed upon user's will. False otherwise. */ bool confirmAndDelete(const QString& targetFile); public slots: void extractSelectedFilesTo(const QString& localPath); private slots: void slotLoadingStarted(); void slotLoadingFinished(KJob *job); void slotOpenExtractedEntry(KJob*); void slotPreviewExtractedEntry(KJob* job); void slotOpenEntry(int mode); void slotError(const QString& errorMessage, const QString& details); void slotExtractArchive(); void slotShowExtractionDialog(); void slotExtractionDone(KJob*); void slotQuickExtractFiles(QAction*); void slotAddFiles(); void slotAddFiles(const QStringList& files, const QString& path = QString()); - void slotAddDir(); void slotAddFilesDone(KJob*); void slotTestingDone(KJob*); void slotDeleteFiles(); void slotDeleteFilesDone(KJob*); void slotShowProperties(); void slotShowContextMenu(); void slotActivated(QModelIndex); void slotToggleInfoPanel(bool); void slotSaveAs(); void updateActions(); void updateQuickExtractMenu(QAction *extractAction); void selectionChanged(); void adjustColumns(); void setBusyGui(); void setReadyGui(); void setFileNameFromArchive(); void slotWatchedFileModified(const QString& file); void slotShowComment(); void slotAddComment(); void slotCommentChanged(); void slotTestArchive(); signals: void busy(); void ready(); void quit(); private: + void resetGui(); void setupView(); void setupActions(); bool isSingleFolderArchive() const; QString detectSubfolder() const; QList filesForIndexes(const QModelIndexList& list) const; QList filesAndRootNodesForIndexes(const QModelIndexList& list) const; QModelIndexList addChildren(const QModelIndexList &list) const; void registerJob(KJob *job); void displayMsgWidget(KMessageWidget::MessageType type, const QString& msg); ArchiveModel *m_model; QTreeView *m_view; QAction *m_previewAction; QAction *m_openFileAction; QAction *m_openFileWithAction; QAction *m_extractArchiveAction; QAction *m_extractAction; QAction *m_addFilesAction; - QAction *m_addDirAction; QAction *m_deleteFilesAction; QAction *m_saveAsAction; QAction *m_propertiesAction; QAction *m_editCommentAction; QAction *m_testArchiveAction; KToggleAction *m_showInfoPanelAction; InfoPanel *m_infoPanel; QSplitter *m_splitter; QList m_tmpOpenDirList; bool m_busy; OpenFileMode m_openFileMode; + QUrl m_lastUsedAddPath; QList m_jobTempEntries; KAbstractWidgetJobTracker *m_jobTracker; KParts::StatusBarExtension *m_statusBarExtension; QVBoxLayout *m_vlayout; QSignalMapper *m_signalMapper; QFileSystemWatcher *m_fileWatcher; QSplitter *m_commentSplitter; QGroupBox *m_commentBox; QPlainTextEdit *m_commentView; KMessageWidget *m_commentMsgWidget; + KMessageWidget *m_messageWidget; }; } // namespace Ark #endif // PART_H