┌   ┐
54
└   ┘

summaryrefslogtreecommitdiff
path: root/src/settings/contextmenu
diff options
context:
space:
mode:
authorDuong Do Minh Chau <[email protected]>2020-11-12 16:38:44 +0700
committerElvis Angelaccio <[email protected]>2020-12-28 20:18:31 +0000
commita512176b4bdbf0f0471a9b9089f4a936c14e2732 (patch)
tree51f1ecf98d29b9994af950d9ca2fb0085f29e337 /src/settings/contextmenu
parentb339ac1b5f22efb57619c738eb39268c3e00948d (diff)
Add options to hide some context menu entries
This commit add options to hide the following context menu entries: - Add to Places - Copy Location - Duplicate Here - Open in New Tab and Open in New Tabs - Open in New Window - Sort By - View Mode The Services settings page is renamed to Context Menu ShowCopyMoveMenu option is moved from GeneralSettings to ContextMenuSettings BUG: 314594
Diffstat (limited to 'src/settings/contextmenu')
-rw-r--r--src/settings/contextmenu/contextmenusettingspage.cpp346
-rw-r--r--src/settings/contextmenu/contextmenusettingspage.h69
-rw-r--r--src/settings/contextmenu/servicemenu.knsrc9
-rw-r--r--src/settings/contextmenu/servicemenuinstaller/CMakeLists.txt15
-rwxr-xr-xsrc/settings/contextmenu/servicemenuinstaller/Messages.sh2
-rw-r--r--src/settings/contextmenu/servicemenuinstaller/servicemenuinstaller.cpp462
-rw-r--r--src/settings/contextmenu/test/service_menu_deinstallation_test.rb112
-rw-r--r--src/settings/contextmenu/test/service_menu_installation_test.rb106
-rw-r--r--src/settings/contextmenu/test/test_helper.rb18
-rwxr-xr-xsrc/settings/contextmenu/test/test_run.rb11
10 files changed, 1150 insertions, 0 deletions
diff --git a/src/settings/contextmenu/contextmenusettingspage.cpp b/src/settings/contextmenu/contextmenusettingspage.cpp
new file mode 100644
index 000000000..4f126d3e2
--- /dev/null
+++ b/src/settings/contextmenu/contextmenusettingspage.cpp
@@ -0,0 +1,346 @@
+/*
+ * SPDX-FileCopyrightText: 2009-2010 Peter Penz <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "contextmenusettingspage.h"
+
+#include "dolphin_generalsettings.h"
+#include "dolphin_versioncontrolsettings.h"
+#include "dolphin_contextmenusettings.h"
+#include "settings/serviceitemdelegate.h"
+#include "settings/servicemodel.h"
+
+#include <KDesktopFile>
+#include <KLocalizedString>
+#include <KMessageBox>
+#include <KNS3/Button>
+#include <KPluginMetaData>
+#include <KService>
+#include <KServiceTypeTrader>
+#include <KDesktopFileActions>
+
+#include <QGridLayout>
+#include <QLabel>
+#include <QListWidget>
+#include <QScroller>
+#include <QShowEvent>
+#include <QSortFilterProxyModel>
+#include <QLineEdit>
+
+namespace
+{
+ const bool ShowDeleteDefault = false;
+ const char VersionControlServicePrefix[] = "_version_control_";
+ const char DeleteService[] = "_delete";
+ const char CopyToMoveToService[] ="_copy_to_move_to";
+ const char AddToPlacesService[] = "_add_to_places";
+ const char SortByService[] = "_sort_by";
+ const char ViewModeService[] = "_view_mode";
+ const char OpenInNewTabService[] = "_open_in_new_tab";
+ const char OpenInNewWindowService[] = "_open_in_new_window";
+ const char CopyLocationService[] = "_copy_location";
+ const char DuplicateHereService[] = "_duplicate_here";
+}
+
+ContextMenuSettingsPage::ContextMenuSettingsPage(QWidget* parent) :
+ SettingsPageBase(parent),
+ m_initialized(false),
+ m_serviceModel(nullptr),
+ m_sortModel(nullptr),
+ m_listView(nullptr),
+ m_enabledVcsPlugins()
+{
+ QVBoxLayout* topLayout = new QVBoxLayout(this);
+
+ QLabel* label = new QLabel(i18nc("@label:textbox",
+ "Select which services should "
+ "be shown in the context menu:"), this);
+ label->setWordWrap(true);
+ m_searchLineEdit = new QLineEdit(this);
+ m_searchLineEdit->setPlaceholderText(i18nc("@label:textbox", "Search..."));
+ connect(m_searchLineEdit, &QLineEdit::textChanged, this, [this](const QString &filter){
+ m_sortModel->setFilterFixedString(filter);
+ });
+
+ m_listView = new QListView(this);
+ QScroller::grabGesture(m_listView->viewport(), QScroller::TouchGesture);
+
+ auto *delegate = new ServiceItemDelegate(m_listView, m_listView);
+ m_serviceModel = new ServiceModel(this);
+ m_sortModel = new QSortFilterProxyModel(this);
+ m_sortModel->setSourceModel(m_serviceModel);
+ m_sortModel->setSortRole(Qt::DisplayRole);
+ m_sortModel->setSortLocaleAware(true);
+ m_sortModel->setFilterRole(Qt::DisplayRole);
+ m_sortModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ m_listView->setModel(m_sortModel);
+ m_listView->setItemDelegate(delegate);
+ m_listView->setVerticalScrollMode(QListView::ScrollPerPixel);
+ connect(m_listView, &QListView::clicked, this, &ContextMenuSettingsPage::changed);
+
+#ifndef Q_OS_WIN
+ auto *downloadButton = new KNS3::Button(i18nc("@action:button", "Download New Services..."),
+ QStringLiteral("servicemenu.knsrc"),
+ this);
+ connect(downloadButton, &KNS3::Button::dialogFinished, this, [this](const KNS3::Entry::List &changedEntries) {
+ if (!changedEntries.isEmpty()) {
+ m_serviceModel->clear();
+ loadServices();
+ }
+ });
+
+#endif
+
+ topLayout->addWidget(label);
+ topLayout->addWidget(m_searchLineEdit);
+ topLayout->addWidget(m_listView);
+#ifndef Q_OS_WIN
+ topLayout->addWidget(downloadButton);
+#endif
+
+ m_enabledVcsPlugins = VersionControlSettings::enabledPlugins();
+ std::sort(m_enabledVcsPlugins.begin(), m_enabledVcsPlugins.end());
+}
+
+ContextMenuSettingsPage::~ContextMenuSettingsPage() {
+}
+
+void ContextMenuSettingsPage::applySettings()
+{
+ if (!m_initialized) {
+ return;
+ }
+
+ KConfig config(QStringLiteral("kservicemenurc"), KConfig::NoGlobals);
+ KConfigGroup showGroup = config.group("Show");
+
+ QStringList enabledPlugins;
+
+ const QAbstractItemModel *model = m_listView->model();
+ for (int i = 0; i < model->rowCount(); ++i) {
+ const QModelIndex index = model->index(i, 0);
+ const QString service = model->data(index, ServiceModel::DesktopEntryNameRole).toString();
+ const bool checked = model->data(index, Qt::CheckStateRole).toBool();
+
+ if (service.startsWith(VersionControlServicePrefix)) {
+ if (checked) {
+ enabledPlugins.append(model->data(index, Qt::DisplayRole).toString());
+ }
+ } else if (service == QLatin1String(DeleteService)) {
+ KSharedConfig::Ptr globalConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::NoGlobals);
+ KConfigGroup configGroup(globalConfig, "KDE");
+ configGroup.writeEntry("ShowDeleteCommand", checked);
+ configGroup.sync();
+ } else if (service == QLatin1String(CopyToMoveToService)) {
+ ContextMenuSettings::setShowCopyMoveMenu(checked);
+ ContextMenuSettings::self()->save();
+ } else if (service == QLatin1String(AddToPlacesService)) {
+ ContextMenuSettings::setShowAddToPlaces(checked);
+ ContextMenuSettings::self()->save();
+ } else if (service == QLatin1String(SortByService)) {
+ ContextMenuSettings::setShowSortBy(checked);
+ ContextMenuSettings::self()->save();
+ } else if (service == QLatin1String(ViewModeService)) {
+ ContextMenuSettings::setShowViewMode(checked);
+ ContextMenuSettings::self()->save();
+ } else if (service == QLatin1String(OpenInNewTabService)) {
+ ContextMenuSettings::setShowOpenInNewTab(checked);
+ ContextMenuSettings::self()->save();
+ } else if (service == QLatin1String(OpenInNewWindowService)) {
+ ContextMenuSettings::setShowOpenInNewWindow(checked);
+ ContextMenuSettings::self()->save();
+ } else if (service == QLatin1String(CopyLocationService)) {
+ ContextMenuSettings::setShowCopyLocation(checked);
+ ContextMenuSettings::self()->save();
+ } else if (service == QLatin1String(DuplicateHereService)) {
+ ContextMenuSettings::setShowDuplicateHere(checked);
+ ContextMenuSettings::self()->save();
+ } else {
+ showGroup.writeEntry(service, checked);
+ }
+ }
+
+ showGroup.sync();
+
+ if (m_enabledVcsPlugins != enabledPlugins) {
+ VersionControlSettings::setEnabledPlugins(enabledPlugins);
+ VersionControlSettings::self()->save();
+
+ KMessageBox::information(window(),
+ i18nc("@info", "Dolphin must be restarted to apply the "
+ "updated version control systems settings."),
+ QString(), // default title
+ QStringLiteral("ShowVcsRestartInformation"));
+ }
+}
+
+void ContextMenuSettingsPage::restoreDefaults()
+{
+ QAbstractItemModel* model = m_listView->model();
+ for (int i = 0; i < model->rowCount(); ++i) {
+ const QModelIndex index = model->index(i, 0);
+ const QString service = model->data(index, ServiceModel::DesktopEntryNameRole).toString();
+
+ const bool checked = !service.startsWith(VersionControlServicePrefix)
+ && service != QLatin1String(DeleteService)
+ && service != QLatin1String(CopyToMoveToService);
+ model->setData(index, checked, Qt::CheckStateRole);
+ }
+}
+
+void ContextMenuSettingsPage::showEvent(QShowEvent* event)
+{
+ if (!event->spontaneous() && !m_initialized) {
+ loadServices();
+
+ loadVersionControlSystems();
+
+ // Add "Show 'Delete' command" as service
+ KSharedConfig::Ptr globalConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::IncludeGlobals);
+ KConfigGroup configGroup(globalConfig, "KDE");
+ addRow(QStringLiteral("edit-delete"),
+ i18nc("@option:check", "Delete"),
+ DeleteService,
+ configGroup.readEntry("ShowDeleteCommand", ShowDeleteDefault));
+
+ // Add "Show 'Copy To' and 'Move To' commands" as service
+ addRow(QStringLiteral("edit-copy"),
+ i18nc("@option:check", "'Copy To' and 'Move To' commands"),
+ CopyToMoveToService,
+ ContextMenuSettings::showCopyMoveMenu());
+
+ // Add other built-in actions
+ addRow(QStringLiteral("bookmark-new"),
+ i18nc("@option:check", "Add to Places"),
+ AddToPlacesService,
+ ContextMenuSettings::showAddToPlaces());
+ addRow(QStringLiteral("view-sort"),
+ i18nc("@option:check", "Sort By"),
+ SortByService,
+ ContextMenuSettings::showSortBy());
+ addRow(QStringLiteral("view-list-icons"),
+ i18nc("@option:check", "View Mode"),
+ ViewModeService,
+ ContextMenuSettings::showViewMode());
+ addRow(QStringLiteral("folder-new"),
+ i18nc("@option:check", "'Open in New Tab' and 'Open in New Tabs'"),
+ OpenInNewTabService,
+ ContextMenuSettings::showOpenInNewTab());
+ addRow(QStringLiteral("window-new"),
+ i18nc("@option:check", "Open in New Window"),
+ OpenInNewWindowService,
+ ContextMenuSettings::showOpenInNewWindow());
+ addRow(QStringLiteral("edit-copy"),
+ i18nc("@option:check", "Copy Location"),
+ CopyLocationService,
+ ContextMenuSettings::showCopyLocation());
+ addRow(QStringLiteral("edit-copy"),
+ i18nc("@option:check", "Duplicate Here"),
+ DuplicateHereService,
+ ContextMenuSettings::showDuplicateHere());
+
+ m_sortModel->sort(Qt::DisplayRole);
+
+ m_initialized = true;
+ }
+ SettingsPageBase::showEvent(event);
+}
+
+void ContextMenuSettingsPage::loadServices()
+{
+ const KConfig config(QStringLiteral("kservicemenurc"), KConfig::NoGlobals);
+ const KConfigGroup showGroup = config.group("Show");
+
+ // Load generic services
+ const KService::List entries = KServiceTypeTrader::self()->query(QStringLiteral("KonqPopupMenu/Plugin"));
+ for (const KService::Ptr &service : entries) {
+ const QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kservices5/" % service->entryPath());
+ const QList<KServiceAction> serviceActions = KDesktopFileActions::userDefinedServices(file, true);
+
+ const KDesktopFile desktopFile(file);
+ const QString subMenuName = desktopFile.desktopGroup().readEntry("X-KDE-Submenu");
+
+ for (const KServiceAction &action : serviceActions) {
+ const QString serviceName = action.name();
+ const bool addService = !action.noDisplay() && !action.isSeparator() && !isInServicesList(serviceName);
+
+ if (addService) {
+ const QString itemName = subMenuName.isEmpty()
+ ? action.text()
+ : i18nc("@item:inmenu", "%1: %2", subMenuName, action.text());
+ const bool checked = showGroup.readEntry(serviceName, true);
+ addRow(action.icon(), itemName, serviceName, checked);
+ }
+ }
+ }
+
+ // Load service plugins that implement the KFileItemActionPlugin interface
+ const KService::List pluginServices = KServiceTypeTrader::self()->query(QStringLiteral("KFileItemAction/Plugin"));
+ for (const KService::Ptr &service : pluginServices) {
+ const QString desktopEntryName = service->desktopEntryName();
+ if (!isInServicesList(desktopEntryName)) {
+ const bool checked = showGroup.readEntry(desktopEntryName, true);
+ addRow(service->icon(), service->name(), desktopEntryName, checked);
+ }
+ }
+
+ // Load JSON-based plugins that implement the KFileItemActionPlugin interface
+ const auto jsonPlugins = KPluginLoader::findPlugins(QStringLiteral("kf5/kfileitemaction"), [](const KPluginMetaData& metaData) {
+ return metaData.serviceTypes().contains(QLatin1String("KFileItemAction/Plugin"));
+ });
+
+ for (const auto &jsonMetadata : jsonPlugins) {
+ const QString desktopEntryName = jsonMetadata.pluginId();
+ if (!isInServicesList(desktopEntryName)) {
+ const bool checked = showGroup.readEntry(desktopEntryName, true);
+ addRow(jsonMetadata.iconName(), jsonMetadata.name(), desktopEntryName, checked);
+ }
+ }
+
+ m_sortModel->sort(Qt::DisplayRole);
+ m_searchLineEdit->setFocus(Qt::OtherFocusReason);
+}
+
+void ContextMenuSettingsPage::loadVersionControlSystems()
+{
+ const QStringList enabledPlugins = VersionControlSettings::enabledPlugins();
+
+ // Create a checkbox for each available version control plugin
+ const KService::List pluginServices = KServiceTypeTrader::self()->query(QStringLiteral("FileViewVersionControlPlugin"));
+ for (const auto &plugin : pluginServices) {
+ const QString pluginName = plugin->name();
+ addRow(QStringLiteral("code-class"),
+ pluginName,
+ VersionControlServicePrefix + pluginName,
+ enabledPlugins.contains(pluginName));
+ }
+
+ m_sortModel->sort(Qt::DisplayRole);
+}
+
+bool ContextMenuSettingsPage::isInServicesList(const QString &service) const
+{
+ for (int i = 0; i < m_serviceModel->rowCount(); ++i) {
+ const QModelIndex index = m_serviceModel->index(i, 0);
+ if (m_serviceModel->data(index, ServiceModel::DesktopEntryNameRole).toString() == service) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void ContextMenuSettingsPage::addRow(const QString &icon,
+ const QString &text,
+ const QString &value,
+ bool checked)
+{
+ m_serviceModel->insertRow(0);
+
+ const QModelIndex index = m_serviceModel->index(0, 0);
+ m_serviceModel->setData(index, icon, Qt::DecorationRole);
+ m_serviceModel->setData(index, text, Qt::DisplayRole);
+ m_serviceModel->setData(index, value, ServiceModel::DesktopEntryNameRole);
+ m_serviceModel->setData(index, checked, Qt::CheckStateRole);
+}
diff --git a/src/settings/contextmenu/contextmenusettingspage.h b/src/settings/contextmenu/contextmenusettingspage.h
new file mode 100644
index 000000000..3825e6f86
--- /dev/null
+++ b/src/settings/contextmenu/contextmenusettingspage.h
@@ -0,0 +1,69 @@
+/*
+ * SPDX-FileCopyrightText: 2009-2010 Peter Penz <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+#ifndef CONTEXTMENUSETTINGSPAGE_H
+#define CONTEXTMENUSETTINGSPAGE_H
+
+#include "settings/settingspagebase.h"
+
+#include <QString>
+
+class QListView;
+class QSortFilterProxyModel;
+class ServiceModel;
+class QLineEdit;
+
+/**
+ * @brief Configurations for services in the context menu.
+ */
+class ContextMenuSettingsPage : public SettingsPageBase
+{
+ Q_OBJECT
+
+public:
+ explicit ContextMenuSettingsPage(QWidget* parent);
+ ~ContextMenuSettingsPage() override;
+
+ /** @see SettingsPageBase::applySettings() */
+ void applySettings() override;
+
+ /** @see SettingsPageBase::restoreDefaults() */
+ void restoreDefaults() override;
+
+protected:
+ void showEvent(QShowEvent* event) override;
+
+private slots:
+ /**
+ * Loads locally installed services.
+ */
+ void loadServices();
+
+private:
+ /**
+ * Loads installed version control systems.
+ */
+ void loadVersionControlSystems();
+
+ bool isInServicesList(const QString &service) const;
+
+ /**
+ * Adds a row to the model of m_listView.
+ */
+ void addRow(const QString &icon,
+ const QString &text,
+ const QString &value,
+ bool checked);
+
+private:
+ bool m_initialized;
+ ServiceModel *m_serviceModel;
+ QSortFilterProxyModel *m_sortModel;
+ QListView* m_listView;
+ QLineEdit *m_searchLineEdit;
+ QStringList m_enabledVcsPlugins;
+};
+
+#endif
diff --git a/src/settings/contextmenu/servicemenu.knsrc b/src/settings/contextmenu/servicemenu.knsrc
new file mode 100644
index 000000000..0d1c103f6
--- /dev/null
+++ b/src/settings/contextmenu/servicemenu.knsrc
@@ -0,0 +1,9 @@
+[KNewStuff2]
+ProvidersUrl=https://download.kde.org/ocs/providers.xml
+Categories=Dolphin Service Menus
+ChecksumPolicy=ifpossible
+SignaturePolicy=ifpossible
+TargetDir=servicemenu-download
+Uncompress=never
+InstallationCommand=servicemenuinstaller install %f
+UninstallCommand=servicemenuinstaller uninstall %f
diff --git a/src/settings/contextmenu/servicemenuinstaller/CMakeLists.txt b/src/settings/contextmenu/servicemenuinstaller/CMakeLists.txt
new file mode 100644
index 000000000..46b159079
--- /dev/null
+++ b/src/settings/contextmenu/servicemenuinstaller/CMakeLists.txt
@@ -0,0 +1,15 @@
+remove_definitions(-DTRANSLATION_DOMAIN=\"dolphin\")
+add_definitions(-DTRANSLATION_DOMAIN=\"dolphin_servicemenuinstaller\")
+
+add_executable(servicemenuinstaller servicemenuinstaller.cpp)
+target_link_libraries(servicemenuinstaller PRIVATE
+ Qt5::Core
+ Qt5::Gui
+ KF5::I18n
+ KF5::CoreAddons
+)
+
+if(HAVE_PACKAGEKIT)
+ target_link_libraries(servicemenuinstaller PRIVATE PK::packagekitqt5)
+endif()
+install(TARGETS servicemenuinstaller ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/src/settings/contextmenu/servicemenuinstaller/Messages.sh b/src/settings/contextmenu/servicemenuinstaller/Messages.sh
new file mode 100755
index 000000000..5012eead6
--- /dev/null
+++ b/src/settings/contextmenu/servicemenuinstaller/Messages.sh
@@ -0,0 +1,2 @@
+#! /usr/bin/env bash
+$XGETTEXT `find . -name \*.cpp` -o $podir/dolphin_servicemenuinstaller.pot
diff --git a/src/settings/contextmenu/servicemenuinstaller/servicemenuinstaller.cpp b/src/settings/contextmenu/servicemenuinstaller/servicemenuinstaller.cpp
new file mode 100644
index 000000000..91da3d256
--- /dev/null
+++ b/src/settings/contextmenu/servicemenuinstaller/servicemenuinstaller.cpp
@@ -0,0 +1,462 @@
+/*
+ * SPDX-FileCopyrightText: 2019 Alexander Potashev <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+ */
+
+#include <QDebug>
+#include <QProcess>
+#include <QTimer>
+#include <QStandardPaths>
+#include <QDir>
+#include <QDirIterator>
+#include <QCommandLineParser>
+#include <QMimeDatabase>
+#include <QUrl>
+#include <QGuiApplication>
+#include <KLocalizedString>
+#include <KShell>
+
+#include "../../../config-packagekit.h"
+
+Q_GLOBAL_STATIC_WITH_ARGS(QStringList, binaryPackages, ({QLatin1String("application/vnd.debian.binary-package"),
+ QLatin1String("application/x-rpm"),
+ QLatin1String("application/x-xz"),
+ QLatin1String("application/zstd")}))
+
+enum PackageOperation {
+ Install,
+ Uninstall
+};
+
+#ifdef HAVE_PACKAGEKIT
+#include <PackageKit/Daemon>
+#include <PackageKit/Details>
+#include <PackageKit/Transaction>
+#else
+#include <QDesktopServices>
+#endif
+
+// @param msg Error that gets logged to CLI
+Q_NORETURN void fail(const QString &str)
+{
+ qCritical() << str;
+ const QStringList args = {"--detailederror" ,i18n("Dolphin service menu installation failed"), str};
+ QProcess::startDetached("kdialog", args);
+
+ exit(1);
+}
+
+QString getServiceMenusDir()
+{
+ const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
+ return QDir(dataLocation).absoluteFilePath("kservices5/ServiceMenus");
+}
+
+#ifdef HAVE_PACKAGEKIT
+void packageKitInstall(const QString &fileName)
+{
+ PackageKit::Transaction *transaction = PackageKit::Daemon::installFile(fileName, PackageKit::Transaction::TransactionFlagNone);
+
+ const auto exitWithError = [=](PackageKit::Transaction::Error, const QString &details) {
+ fail(details);
+ };
+
+ QObject::connect(transaction, &PackageKit::Transaction::finished,
+ [=](PackageKit::Transaction::Exit status, uint) {
+ if (status == PackageKit::Transaction::ExitSuccess) {
+ exit(0);
+ }
+ // Fallback error handling
+ QTimer::singleShot(500, [=](){
+ fail(i18n("Failed to install \"%1\", exited with status \"%2\"",
+ fileName, QVariant::fromValue(status).toString()));
+ });
+ });
+ QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
+}
+
+void packageKitUninstall(const QString &fileName)
+{
+ const auto exitWithError = [=](PackageKit::Transaction::Error, const QString &details) {
+ fail(details);
+ };
+ const auto uninstallLambda = [=](PackageKit::Transaction::Exit status, uint) {
+ if (status == PackageKit::Transaction::ExitSuccess) {
+ exit(0);
+ }
+ };
+
+ PackageKit::Transaction *transaction = PackageKit::Daemon::getDetailsLocal(fileName);
+ QObject::connect(transaction, &PackageKit::Transaction::details,
+ [=](const PackageKit::Details &details) {
+ PackageKit::Transaction *transaction = PackageKit::Daemon::removePackage(details.packageId());
+ QObject::connect(transaction, &PackageKit::Transaction::finished, uninstallLambda);
+ QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
+ });
+
+ QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
+ // Fallback error handling
+ QObject::connect(transaction, &PackageKit::Transaction::finished,
+ [=](PackageKit::Transaction::Exit status, uint) {
+ if (status != PackageKit::Transaction::ExitSuccess) {
+ QTimer::singleShot(500, [=]() {
+ fail(i18n("Failed to uninstall \"%1\", exited with status \"%2\"",
+ fileName, QVariant::fromValue(status).toString()));
+ });
+ }
+ });
+ }
+#endif
+
+Q_NORETURN void packageKit(PackageOperation operation, const QString &fileName)
+{
+#ifdef HAVE_PACKAGEKIT
+ QFileInfo fileInfo(fileName);
+ if (!fileInfo.exists()) {
+ fail(i18n("The file does not exist!"));
+ }
+ const QString absPath = fileInfo.absoluteFilePath();
+ if (operation == PackageOperation::Install) {
+ packageKitInstall(absPath);
+ } else {
+ packageKitUninstall(absPath);
+ }
+ QGuiApplication::exec(); // For event handling, no return after signals finish
+ fail(i18n("Unknown error when installing package"));
+#else
+ Q_UNUSED(operation)
+ QDesktopServices::openUrl(QUrl(fileName));
+ exit(0);
+#endif
+}
+
+struct UncompressCommand
+{
+ QString command;
+ QStringList args1;
+ QStringList args2;
+};
+
+enum ScriptExecution{
+ Process,
+ Konsole
+};
+
+void runUncompress(const QString &inputPath, const QString &outputPath)
+{
+ QVector<QPair<QStringList, UncompressCommand>> mimeTypeToCommand;
+ mimeTypeToCommand.append({{"application/x-tar", "application/tar", "application/x-gtar", "multipart/x-tar"},
+ UncompressCommand({"tar", {"-xf"}, {"-C"}})});
+ mimeTypeToCommand.append({{"application/x-gzip", "application/gzip",
+ "application/x-gzip-compressed-tar", "application/gzip-compressed-tar",
+ "application/x-gzip-compressed", "application/gzip-compressed",
+ "application/tgz", "application/x-compressed-tar",
+ "application/x-compressed-gtar", "file/tgz",
+ "multipart/x-tar-gz", "application/x-gunzip", "application/gzipped",
+ "gzip/document"},
+ UncompressCommand({"tar", {"-zxf"}, {"-C"}})});
+ mimeTypeToCommand.append({{"application/bzip", "application/bzip2", "application/x-bzip",
+ "application/x-bzip2", "application/bzip-compressed",
+ "application/bzip2-compressed", "application/x-bzip-compressed",
+ "application/x-bzip2-compressed", "application/bzip-compressed-tar",
+ "application/bzip2-compressed-tar", "application/x-bzip-compressed-tar",
+ "application/x-bzip2-compressed-tar", "application/x-bz2"},
+ UncompressCommand({"tar", {"-jxf"}, {"-C"}})});
+ mimeTypeToCommand.append({{"application/zip", "application/x-zip", "application/x-zip-compressed",
+ "multipart/x-zip"},
+ UncompressCommand({"unzip", {}, {"-d"}})});
+
+ const auto mime = QMimeDatabase().mimeTypeForFile(inputPath).name();
+
+ UncompressCommand command{};
+ for (const auto &pair : qAsConst(mimeTypeToCommand)) {
+ if (pair.first.contains(mime)) {
+ command = pair.second;
+ break;
+ }
+ }
+
+ if (command.command.isEmpty()) {
+ fail(i18n("Unsupported archive type %1: %2", mime, inputPath));
+ }
+
+ QProcess process;
+ process.start(
+ command.command,
+ QStringList() << command.args1 << inputPath << command.args2 << outputPath,
+ QIODevice::NotOpen);
+ if (!process.waitForStarted()) {
+ fail(i18n("Failed to run uncompressor command for %1", inputPath));
+ }
+
+ if (!process.waitForFinished()) {
+ fail(
+ i18n("Process did not finish in reasonable time: %1 %2", process.program(), process.arguments().join(" ")));
+ }
+
+ if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
+ fail(i18n("Failed to uncompress %1", inputPath));
+ }
+}
+
+QString findRecursive(const QString &dir, const QString &basename)
+{
+ QDirIterator it(dir, QStringList{basename}, QDir::Files, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ return QFileInfo(it.next()).canonicalFilePath();
+ }
+
+ return QString();
+}
+
+bool runScriptOnce(const QString &path, const QStringList &args, ScriptExecution execution)
+{
+ QProcess process;
+ process.setWorkingDirectory(QFileInfo(path).absolutePath());
+
+ const static bool konsoleAvailable = !QStandardPaths::findExecutable("konsole").isEmpty();
+ if (konsoleAvailable && execution == ScriptExecution::Konsole) {
+ QString bashCommand = KShell::quoteArg(path) + ' ';
+ if (!args.isEmpty()) {
+ bashCommand.append(args.join(' '));
+ }
+ bashCommand.append("|| $SHELL");
+ // If the install script fails a shell opens and the user can fix the problem
+ // without an error konsole closes
+ process.start("konsole", QStringList() << "-e" << "bash" << "-c" << bashCommand, QIODevice::NotOpen);
+ } else {
+ process.start(path, args, QIODevice::NotOpen);
+ }
+ if (!process.waitForStarted()) {
+ fail(i18n("Failed to run installer script %1", path));
+ }
+
+ // Wait until installer exits, without timeout
+ if (!process.waitForFinished(-1)) {
+ qWarning() << "Failed to wait on installer:" << process.program() << process.arguments().join(" ");
+ return false;
+ }
+
+ if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
+ qWarning() << "Installer script exited with error:" << process.program() << process.arguments().join(" ");
+ return false;
+ }
+
+ return true;
+}
+
+// If hasArgVariants is true, run "path".
+// If hasArgVariants is false, run "path argVariants[i]" until successful.
+bool runScriptVariants(const QString &path, bool hasArgVariants, const QStringList &argVariants, QString &errorText)
+{
+ QFile file(path);
+ if (!file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) {
+ errorText = i18n("Failed to set permissions on %1: %2", path, file.errorString());
+ return false;
+ }
+
+ qInfo() << "[servicemenuinstaller]: Trying to run installer/uninstaller" << path;
+ if (hasArgVariants) {
+ for (const auto &arg : argVariants) {
+ if (runScriptOnce(path, {arg}, ScriptExecution::Process)) {
+ return true;
+ }
+ }
+ } else if (runScriptOnce(path, {}, ScriptExecution::Konsole)) {
+ return true;
+ }
+
+ errorText = i18nc(
+ "%2 = comma separated list of arguments",
+ "Installer script %1 failed, tried arguments \"%2\".", path, argVariants.join(i18nc("Separator between arguments", "\", \"")));
+ return false;
+}
+
+QString generateDirPath(const QString &archive)
+{
+ return QStringLiteral("%1-dir").arg(archive);
+}
+
+bool cmdInstall(const QString &archive, QString &errorText)
+{
+ const auto serviceDir = getServiceMenusDir();
+ if (!QDir().mkpath(serviceDir)) {
+ // TODO Cannot get error string because of this bug: https://bugreports.qt.io/browse/QTBUG-1483
+ errorText = i18n("Failed to create path %1", serviceDir);
+ return false;
+ }
+
+ if (archive.endsWith(QLatin1String(".desktop"))) {
+ // Append basename to destination directory
+ const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
+ if (QFileInfo::exists(dest)) {
+ QFile::remove(dest);
+ }
+ qInfo() << "Single-File Service-Menu" << archive << dest;
+
+ QFile source(archive);
+ if (!source.copy(dest)) {
+ errorText = i18n("Failed to copy .desktop file %1 to %2: %3", archive, dest, source.errorString());
+ return false;
+ }
+ } else {
+ if (binaryPackages->contains(QMimeDatabase().mimeTypeForFile(archive).name())) {
+ packageKit(PackageOperation::Install, archive);
+ }
+ const QString dir = generateDirPath(archive);
+ if (QFile::exists(dir)) {
+ if (!QDir(dir).removeRecursively()) {
+ errorText = i18n("Failed to remove directory %1", dir);
+ return false;
+ }
+ }
+
+ if (QDir().mkdir(dir)) {
+ errorText = i18n("Failed to create directory %1", dir);
+ }
+
+ runUncompress(archive, dir);
+
+ // Try "install-it" first
+ QString installItPath;
+ const QStringList basenames1 = {"install-it.sh", "install-it"};
+ for (const auto &basename : basenames1) {
+ const auto path = findRecursive(dir, basename);
+ if (!path.isEmpty()) {
+ installItPath = path;
+ break;
+ }
+ }
+
+ if (!installItPath.isEmpty()) {
+ return runScriptVariants(installItPath, false, QStringList{}, errorText);
+ }
+
+ // If "install-it" is missing, try "install"
+ QString installerPath;
+ const QStringList basenames2 = {"installKDE4.sh", "installKDE4", "install.sh", "install"};
+ for (const auto &basename : basenames2) {
+ const auto path = findRecursive(dir, basename);
+ if (!path.isEmpty()) {
+ installerPath = path;
+ break;
+ }
+ }
+
+ if (!installerPath.isEmpty()) {
+ // Try to run script without variants first
+ if (!runScriptVariants(installerPath, false, {}, errorText)) {
+ return runScriptVariants(installerPath, true, {"--local", "--local-install", "--install"}, errorText);
+ }
+ return true;
+ }
+
+ fail(i18n("Failed to find an installation script in %1", dir));
+ }
+
+ return true;
+}
+
+bool cmdUninstall(const QString &archive, QString &errorText)
+{
+ const auto serviceDir = getServiceMenusDir();
+ if (archive.endsWith(QLatin1String(".desktop"))) {
+ // Append basename to destination directory
+ const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
+ QFile file(dest);
+ if (!file.remove()) {
+ errorText = i18n("Failed to remove .desktop file %1: %2", dest, file.errorString());
+ return false;
+ }
+ } else {
+ if (binaryPackages->contains(QMimeDatabase().mimeTypeForFile(archive).name())) {
+ packageKit(PackageOperation::Uninstall, archive);
+ }
+ const QString dir = generateDirPath(archive);
+
+ // Try "deinstall" first
+ QString deinstallPath;
+ const QStringList basenames1 = {"uninstall.sh", "uninstal", "deinstall.sh", "deinstall"};
+ for (const auto &basename : basenames1) {
+ const auto path = findRecursive(dir, basename);
+ if (!path.isEmpty()) {
+ deinstallPath = path;
+ break;
+ }
+ }
+
+ if (!deinstallPath.isEmpty()) {
+ const bool ok = runScriptVariants(deinstallPath, false, {}, errorText);
+ if (!ok) {
+ return ok;
+ }
+ } else {
+ // If "deinstall" is missing, try "install --uninstall"
+ QString installerPath;
+ const QStringList basenames2 = {"install-it.sh", "install-it", "installKDE4.sh",
+ "installKDE4", "install.sh", "install"};
+ for (const auto &basename : basenames2) {
+ const auto path = findRecursive(dir, basename);
+ if (!path.isEmpty()) {
+ installerPath = path;
+ break;
+ }
+ }
+
+ if (!installerPath.isEmpty()) {
+ const bool ok = runScriptVariants(installerPath, true,
+ {"--remove", "--delete", "--uninstall", "--deinstall"}, errorText);
+ if (!ok) {
+ return ok;
+ }
+ } else {
+ fail(i18n("Failed to find an uninstallation script in %1", dir));
+ }
+ }
+
+ QDir dirObject(dir);
+ if (!dirObject.removeRecursively()) {
+ errorText = i18n("Failed to remove directory %1", dir);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+int main(int argc, char *argv[])
+{
+ QGuiApplication app(argc, argv);
+
+ QCommandLineParser parser;
+ parser.addPositionalArgument(QStringLiteral("command"), i18nc("@info:shell", "Command to execute: install or uninstall."));
+ parser.addPositionalArgument(QStringLiteral("path"), i18nc("@info:shell", "Path to archive."));
+ parser.process(app);
+
+ const QStringList args = parser.positionalArguments();
+ if (args.isEmpty()) {
+ fail(i18n("Command is required."));
+ }
+ if (args.size() == 1) {
+ fail(i18n("Path to archive is required."));
+ }
+
+ const QString cmd = args[0];
+ const QString archive = args[1];
+
+ QString errorText;
+ if (cmd == QLatin1String("install")) {
+ if (!cmdInstall(archive, errorText)) {
+ fail(errorText);
+ }
+ } else if (cmd == QLatin1String("uninstall")) {
+ if (!cmdUninstall(archive, errorText)) {
+ fail(errorText);
+ }
+ } else {
+ fail(i18n("Unsupported command %1", cmd));
+ }
+
+ return 0;
+}
diff --git a/src/settings/contextmenu/test/service_menu_deinstallation_test.rb b/src/settings/contextmenu/test/service_menu_deinstallation_test.rb
new file mode 100644
index 000000000..bf44b7b7f
--- /dev/null
+++ b/src/settings/contextmenu/test/service_menu_deinstallation_test.rb
@@ -0,0 +1,112 @@
+#!/usr/bin/env ruby
+
+# SPDX-FileCopyrightText: 2019 Harald Sitter <[email protected]>
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+require_relative 'test_helper'
+
+require 'tmpdir'
+
+class ServiceMenuDeinstallationTest < Test::Unit::TestCase
+ def setup
+ @tmpdir = Dir.mktmpdir("dolphintest-#{self.class.to_s.tr(':', '_')}")
+ @pwdir = Dir.pwd
+ Dir.chdir(@tmpdir)
+
+ ENV['XDG_DATA_HOME'] = File.join(@tmpdir, 'data')
+ end
+
+ def teardown
+ Dir.chdir(@pwdir)
+ FileUtils.rm_rf(@tmpdir)
+
+ ENV.delete('XDG_DATA_HOME')
+ end
+
+ def test_run_deinstall
+ service_dir = File.join(Dir.pwd, 'share/servicemenu-download')
+ archive_base = "#{service_dir}/foo.zip"
+ archive_dir = "#{archive_base}-dir/foo-1.1/"
+ FileUtils.mkpath(archive_dir)
+ File.write("#{archive_dir}/deinstall.sh", <<-DEINSTALL_SH)
+#!/bin/sh
+set -e
+cat deinstall.sh
+touch #{@tmpdir}/deinstall.sh-run
+ DEINSTALL_SH
+ File.write("#{archive_dir}/install.sh", <<-INSTALL_SH)
+#!/bin/sh
+set -e
+cat install.sh
+touch #{@tmpdir}/install.sh-run
+ INSTALL_SH
+
+ assert(system('servicemenuinstaller', 'uninstall', archive_base))
+
+ # deinstaller should be run
+ # installer should not be run
+ # archive_dir should have been correctly removed
+
+ assert_path_exist('deinstall.sh-run')
+ assert_path_not_exist('install.sh-run')
+ assert_path_not_exist(archive_dir)
+ end
+
+ def test_run_install_with_arg
+ service_dir = File.join(Dir.pwd, 'share/servicemenu-download')
+ archive_base = "#{service_dir}/foo.zip"
+ archive_dir = "#{archive_base}-dir/foo-1.1/"
+ FileUtils.mkpath(archive_dir)
+
+ File.write("#{archive_dir}/install.sh", <<-INSTALL_SH)
+#!/bin/sh
+if [ "$@" = "--uninstall" ]; then
+ touch #{@tmpdir}/install.sh-run
+ exit 0
+fi
+exit 1
+ INSTALL_SH
+
+ assert(system('servicemenuinstaller', 'uninstall', archive_base))
+
+ assert_path_not_exist('deinstall.sh-run')
+ assert_path_exist('install.sh-run')
+ assert_path_not_exist(archive_dir)
+ end
+
+ # no scripts in sight
+ def test_run_fail
+ service_dir = File.join(Dir.pwd, 'share/servicemenu-download')
+ archive_base = "#{service_dir}/foo.zip"
+ archive_dir = "#{archive_base}-dir/foo-1.1/"
+ FileUtils.mkpath(archive_dir)
+
+ refute(system('servicemenuinstaller', 'uninstall', archive_base))
+
+ # I am unsure if deinstallation really should keep the files around. But
+ # that's how it behaved originally so it's supposedly intentional
+ # - sitter, 2019
+ assert_path_exist(archive_dir)
+ end
+
+ # For desktop files things are a bit special. There is one in .local/share/servicemenu-download
+ # and another in the actual ServiceMenus dir. The latter gets removed by the
+ # script, the former by KNS.
+ def test_run_desktop
+ service_dir = File.join(Dir.pwd, 'share/servicemenu-download')
+ downloaded_file = "#{service_dir}/foo.desktop"
+ FileUtils.mkpath(service_dir)
+ FileUtils.touch(downloaded_file)
+
+ menu_dir = "#{ENV['XDG_DATA_HOME']}/kservices5/ServiceMenus/"
+ installed_file = "#{menu_dir}/foo.desktop"
+ FileUtils.mkpath(menu_dir)
+ FileUtils.touch(installed_file)
+
+ assert(system('servicemenuinstaller', 'uninstall', downloaded_file))
+
+ assert_path_exist(downloaded_file)
+ assert_path_not_exist(installed_file)
+ end
+end
diff --git a/src/settings/contextmenu/test/service_menu_installation_test.rb b/src/settings/contextmenu/test/service_menu_installation_test.rb
new file mode 100644
index 000000000..7c05a40e3
--- /dev/null
+++ b/src/settings/contextmenu/test/service_menu_installation_test.rb
@@ -0,0 +1,106 @@
+#!/usr/bin/env ruby
+
+# SPDX-FileCopyrightText: 2019 Harald Sitter <[email protected]>
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+require_relative 'test_helper'
+
+require 'tmpdir'
+
+class ServiceMenuInstallationTest < Test::Unit::TestCase
+ def setup
+ @tmpdir = Dir.mktmpdir("dolphintest-#{self.class.to_s.tr(':', '_')}")
+ @pwdir = Dir.pwd
+ Dir.chdir(@tmpdir)
+
+ ENV['XDG_DATA_HOME'] = File.join(@tmpdir, 'data')
+ end
+
+ def teardown
+ Dir.chdir(@pwdir)
+ FileUtils.rm_rf(@tmpdir)
+
+ ENV.delete('XDG_DATA_HOME')
+ end
+
+ def test_run_install
+ service_dir = File.join(Dir.pwd, 'share/servicemenu-download')
+ FileUtils.mkpath(service_dir)
+ archive = "#{service_dir}/foo.tar"
+
+ archive_dir = 'foo' # relative so tar cf is relative without fuzz
+ FileUtils.mkpath(archive_dir)
+ File.write("#{archive_dir}/install-it.sh", <<-INSTALL_IT_SH)
+#!/bin/sh
+touch #{@tmpdir}/install-it.sh-run
+INSTALL_IT_SH
+ File.write("#{archive_dir}/install.sh", <<-INSTALL_SH)
+#!/bin/sh
+touch #{@tmpdir}/install.sh-run
+ INSTALL_SH
+ assert(system('tar', '-cf', archive, archive_dir))
+
+ assert(system('servicemenuinstaller', 'install', archive))
+
+ tar_dir = "#{service_dir}/foo.tar-dir"
+ tar_extract_dir = "#{service_dir}/foo.tar-dir/foo"
+ assert_path_exist(tar_dir)
+ assert_path_exist(tar_extract_dir)
+ assert_path_exist("#{tar_extract_dir}/install-it.sh")
+ assert_path_exist("#{tar_extract_dir}/install.sh")
+ end
+
+ def test_run_install_with_arg
+ service_dir = File.join(Dir.pwd, 'share/servicemenu-download')
+ FileUtils.mkpath(service_dir)
+ archive = "#{service_dir}/foo.tar"
+
+ archive_dir = 'foo' # relative so tar cf is relative without fuzz
+ FileUtils.mkpath(archive_dir)
+ File.write("#{archive_dir}/install.sh", <<-INSTALL_SH)
+#!/bin/sh
+if [ "$@" = "--install" ]; then
+ touch #{@tmpdir}/install.sh-run
+ exit 0
+fi
+exit 1
+ INSTALL_SH
+ assert(system('tar', '-cf', archive, archive_dir))
+
+ assert(system('servicemenuinstaller', 'install', archive))
+
+ tar_dir = "#{service_dir}/foo.tar-dir"
+ tar_extract_dir = "#{service_dir}/foo.tar-dir/foo"
+ assert_path_exist(tar_dir)
+ assert_path_exist(tar_extract_dir)
+ assert_path_not_exist("#{tar_extract_dir}/install-it.sh")
+ assert_path_exist("#{tar_extract_dir}/install.sh")
+ end
+
+ def test_run_fail
+ service_dir = File.join(Dir.pwd, 'share/servicemenu-download')
+ FileUtils.mkpath(service_dir)
+ archive = "#{service_dir}/foo.tar"
+
+ archive_dir = 'foo' # relative so tar cf is relative without fuzz
+ FileUtils.mkpath(archive_dir)
+ assert(system('tar', '-cf', archive, archive_dir))
+
+ refute(system('servicemenuinstaller', 'install', archive))
+ end
+
+ def test_run_desktop
+ service_dir = File.join(Dir.pwd, 'share/servicemenu-download')
+ downloaded_file = "#{service_dir}/foo.desktop"
+ FileUtils.mkpath(service_dir)
+ FileUtils.touch(downloaded_file)
+
+ installed_file = "#{ENV['XDG_DATA_HOME']}/kservices5/ServiceMenus/foo.desktop"
+
+ assert(system('servicemenuinstaller', 'install', downloaded_file))
+
+ assert_path_exist(downloaded_file)
+ assert_path_exist(installed_file)
+ end
+end
diff --git a/src/settings/contextmenu/test/test_helper.rb b/src/settings/contextmenu/test/test_helper.rb
new file mode 100644
index 000000000..b4e4dded2
--- /dev/null
+++ b/src/settings/contextmenu/test/test_helper.rb
@@ -0,0 +1,18 @@
+# SPDX-FileCopyrightText: 2019 Harald Sitter <[email protected]>
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+$LOAD_PATH.unshift(File.absolute_path('../', __dir__)) # ../
+
+def __test_method_name__
+ return @method_name if defined?(:@method_name)
+ index = 0
+ caller = ''
+ until caller.start_with?('test_')
+ caller = caller_locations(index, 1)[0].label
+ index += 1
+ end
+ caller
+end
+
+require 'test/unit'
diff --git a/src/settings/contextmenu/test/test_run.rb b/src/settings/contextmenu/test/test_run.rb
new file mode 100755
index 000000000..ab298a0b0
--- /dev/null
+++ b/src/settings/contextmenu/test/test_run.rb
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+
+# SPDX-FileCopyrightText: 2019 Harald Sitter <[email protected]>
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+# This is a fancy wrapper around test_helper to prevent the collector from
+# loading the helper twice as it would occur if we ran the helper directly.
+
+require_relative 'test_helper'
+
+Test::Unit::AutoRunner.run(true, File.absolute_path(__dir__))