diff --git a/applets/icon/CMakeLists.txt b/applets/icon/CMakeLists.txt index c574633d..d6b83124 100644 --- a/applets/icon/CMakeLists.txt +++ b/applets/icon/CMakeLists.txt @@ -1,16 +1,17 @@ set(iconapplet_SRCS iconapplet.cpp ) add_library(plasma_applet_icon MODULE ${iconapplet_SRCS}) kcoreaddons_desktop_to_json(plasma_applet_icon package/metadata.desktop) target_link_libraries(plasma_applet_icon KF5::I18n + KF5::KIOCore # for OpenFileManagerWindowJob KF5::KIOWidgets # for KRun KF5::Plasma) install(TARGETS plasma_applet_icon DESTINATION ${PLUGIN_INSTALL_DIR}/plasma/applets) plasma_install_package(package org.kde.plasma.icon) diff --git a/applets/icon/iconapplet.cpp b/applets/icon/iconapplet.cpp index 81ef4663..d23c2bb9 100644 --- a/applets/icon/iconapplet.cpp +++ b/applets/icon/iconapplet.cpp @@ -1,395 +1,436 @@ /* * Copyright 2016 Kai Uwe Broulik * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . * */ #include "iconapplet.h" +#include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include +#include +#include #include #include +#include #include #include +#include IconApplet::IconApplet(QObject *parent, const QVariantList &data) : Plasma::Applet(parent, data) { } IconApplet::~IconApplet() { // in a handler connected to IconApplet::appletDeleted m_localPath will be empty?! if (destroyed()) { QFile::remove(m_localPath); } } void IconApplet::init() { populate(); } void IconApplet::populate() { m_url = config().readEntry(QStringLiteral("url"), QUrl()); if (!m_url.isValid()) { // the old applet that used a QML plugin and stored its url // in plasmoid.configuration.url had its entries stored in [Configuration][General] // so we look here as well to provide an upgrade path m_url = config().group("General").readEntry(QStringLiteral("url"), QUrl()); } // our backing desktop file already exists? just read all the things from it const QString path = localPath(); if (QFileInfo::exists(path)) { populateFromDesktopFile(path); return; } if (!m_url.isValid()) { // invalid url, use dummy data populateFromDesktopFile(QString()); return; } const QString plasmaIconsFolderPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/plasma_icons"); if (!QDir().mkpath(plasmaIconsFolderPath)) { setLaunchErrorMessage(i18n("Failed to create icon widgets folder '%1'", plasmaIconsFolderPath)); return; } QString desiredDesktopFileName = m_url.fileName(); // in doubt, just hash the URL, e.g. http://www.kde.org/ has no filename if (desiredDesktopFileName.isEmpty()) { desiredDesktopFileName = QString::fromLatin1(QCryptographicHash::hash(m_url.toDisplayString().toUtf8(), QCryptographicHash::Md5).toHex()); } // We always want it to be a .desktop file (e.g. also for the "Type=Link" at the end) if (!desiredDesktopFileName.endsWith(QLatin1String(".desktop"))) { desiredDesktopFileName.append(QLatin1String(".desktop")); } QString backingDesktopFile = plasmaIconsFolderPath + QLatin1Char('/'); // KIO::suggestName always appends a suffix, i.e. it expects that we already know the file already exists if (QFileInfo::exists(backingDesktopFile + desiredDesktopFileName)) { desiredDesktopFileName = KIO::suggestName(QUrl::fromLocalFile(plasmaIconsFolderPath), desiredDesktopFileName); } backingDesktopFile.append(desiredDesktopFileName); QString name; // ends up as "Name" in the .desktop file for "Link" files below if (m_url.isLocalFile()) { const QString localUrlString = m_url.toLocalFile(); // if desktop file just copy it over if (KDesktopFile::isDesktopFile(localUrlString)) { // if this restriction is set, KIO won't allow running desktop files from outside // registered services, applications, and so on, in this case we'll use the original // .desktop file and lose the ability to customize it if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) { populateFromDesktopFile(localUrlString); // we don't call setLocalPath here as we don't want to store localPath to be a system-location // so that the fact that we cannot edit is re-evaluated every time return; } if (!QFile::copy(localUrlString, backingDesktopFile)) { setLaunchErrorMessage(i18n("Failed to copy icon widget desktop file from '%1' to '%2'", localUrlString, backingDesktopFile)); return; } // set executable flag on the desktop file so KIO doesn't complain about executing it QFile file(backingDesktopFile); file.setPermissions(file.permissions() | QFile::ExeOwner); populateFromDesktopFile(backingDesktopFile); setLocalPath(backingDesktopFile); return; } name = QFileInfo(localUrlString).baseName(); } // in all other cases just make it a link // TODO use kio stat job which also works for remote stuff QMimeDatabase db; const QMimeType mimeType = db.mimeTypeForUrl(m_url); KDesktopFile linkDesktopFile(backingDesktopFile); auto desktopGroup = linkDesktopFile.desktopGroup(); if (name.isEmpty()) { if (m_url.scheme().startsWith(QLatin1String("http"))) { name = m_url.host(); } else { name = m_url.fileName(); } } desktopGroup.writeEntry(QStringLiteral("Name"), name); desktopGroup.writeEntry(QStringLiteral("Type"), QStringLiteral("Link")); desktopGroup.writeEntry(QStringLiteral("URL"), m_url); desktopGroup.writeEntry(QStringLiteral("Icon"), KIO::iconNameForUrl(m_url)); // when in doubt Qt returns application/octet-stream which will show as "Unknown" usually, so don't write it down then if (mimeType.name() != QLatin1String("application/octet-stream")) { desktopGroup.writeEntry(QStringLiteral("GenericName"), mimeType.comment()); } linkDesktopFile.sync(); populateFromDesktopFile(backingDesktopFile); setLocalPath(backingDesktopFile); } void IconApplet::populateFromDesktopFile(const QString &path) { // path empty? just set icon to "unknown" and call it a day if (path.isEmpty()) { setIconName({}); return; } KDesktopFile desktopFile(path); const QString &name = desktopFile.readName(); if (m_name != name) { m_name = name; emit nameChanged(name); } const QString &genericName = desktopFile.readGenericName(); if (m_genericName != genericName) { m_genericName = genericName; emit genericNameChanged(genericName); } setIconName(desktopFile.readIcon()); - QVariantList jumpListActions; + delete m_openContainingFolderAction; + m_openContainingFolderAction = nullptr; + m_openWithActions.clear(); + + if (desktopFile.hasLinkType()) { + const QUrl &linkUrl = QUrl(desktopFile.readUrl()); + + if (!m_fileItemActions) { + m_fileItemActions = new KFileItemActions(this); + } + KFileItemListProperties itemProperties(KFileItemList({KFileItem(linkUrl)})); + m_fileItemActions->setItemListProperties(itemProperties); + + if (!m_openWithMenu) { + m_openWithMenu.reset(new QMenu()); + } + m_openWithMenu->clear(); + m_fileItemActions->addOpenWithActionsTo(m_openWithMenu.data()); + + m_openWithActions = m_openWithMenu->actions(); + + if (KProtocolManager::supportsListing(linkUrl)) { + if (!m_openContainingFolderAction) { + m_openContainingFolderAction = new QAction(QIcon::fromTheme(QStringLiteral("document-open-folder")), i18n("Open Containing Folder"), this); + connect(m_openContainingFolderAction, &QAction::triggered, this, [this] { + KIO::highlightInFileManager({m_openContainingFolderAction->property("linkUrl").toUrl()}); + }); + } + m_openContainingFolderAction->setProperty("linkUrl", linkUrl); + } + } + + m_jumpListActions.clear(); foreach (const QString &actionName, desktopFile.readActions()) { const KConfigGroup &actionGroup = desktopFile.actionGroup(actionName); if (!actionGroup.isValid() || !actionGroup.exists()) { continue; } const QString &name = actionGroup.readEntry(QStringLiteral("Name")); const QString &exec = actionGroup.readEntry(QStringLiteral("Exec")); if (name.isEmpty() || exec.isEmpty()) { continue; } - jumpListActions << QVariantMap{ - {QStringLiteral("name"), name}, - {QStringLiteral("icon"), actionGroup.readEntry("Icon")}, - {QStringLiteral("exec"), exec} - }; - } + QAction *action = new QAction(QIcon::fromTheme(actionGroup.readEntry("Icon")), name, this); + connect(action, &QAction::triggered, this, [this, exec] { + KRun::run(exec, {}, nullptr, m_name, m_iconName); + }); - if (m_jumpListActions != jumpListActions) { - m_jumpListActions = jumpListActions; - emit jumpListActionsChanged(jumpListActions); + m_jumpListActions << action; } m_localPath = path; } QUrl IconApplet::url() const { return m_url; } void IconApplet::setUrl(const QUrl &url) { if (m_url != url) { m_url = url; urlChanged(url); config().writeEntry(QStringLiteral("url"), url); populate(); } } void IconApplet::setIconName(const QString &iconName) { const QString newIconName = (!iconName.isEmpty() ? iconName : QStringLiteral("unknown")); if (m_iconName != newIconName) { m_iconName = newIconName; emit iconNameChanged(newIconName); } } QString IconApplet::name() const { return m_name; } QString IconApplet::iconName() const { return m_iconName; } QString IconApplet::genericName() const { return m_genericName; } -QVariantList IconApplet::jumpListActions() const +QList IconApplet::contextualActions() { - return m_jumpListActions; + QList actions; + actions << m_jumpListActions; + + if (!actions.isEmpty()) { + if (!m_separatorAction) { + m_separatorAction = new QAction(this); + m_separatorAction->setSeparator(true); + } + actions << m_separatorAction; + } + + actions << m_openWithActions; + + if (m_openContainingFolderAction) { + actions << m_openContainingFolderAction; + } + + return actions; } void IconApplet::open() { new KRun(QUrl::fromLocalFile(m_localPath), QApplication::desktop()); } void IconApplet::processDrop(QObject *dropEvent) { Q_ASSERT(dropEvent); // DeclarativeDropEvent and co aren't public const QObject *mimeData = qvariant_cast(dropEvent->property("mimeData")); Q_ASSERT(mimeData); const QJsonArray &droppedUrls = mimeData->property("urls").toJsonArray(); QList urls; urls.reserve(droppedUrls.count()); foreach (const QJsonValue &droppedUrl, droppedUrls) { const QUrl url(droppedUrl.toString()); if (url.isValid()) { urls.append(url); } } if (urls.isEmpty()) { return; } const QString &localPath = m_url.toLocalFile(); if (KDesktopFile::isDesktopFile(localPath)) { KRun::runService(KService(localPath), urls, nullptr); return; } QMimeDatabase db; const QMimeType mimeType = db.mimeTypeForUrl(m_url); if (KAuthorized::authorize(QStringLiteral("shell_access")) && (mimeType.inherits(QStringLiteral("application/x-executable")) || mimeType.inherits(QStringLiteral("application/x-shellscript")))) { QProcess::startDetached(m_url.toLocalFile(), QUrl::toStringList(urls)); return; } if (mimeType.inherits(QStringLiteral("inode/directory"))) { QMimeData mimeData; mimeData.setUrls(urls); // DeclarativeDropEvent isn't public QDropEvent de(QPointF(dropEvent->property("x").toInt(), dropEvent->property("y").toInt()), static_cast(dropEvent->property("proposedActions").toInt()), &mimeData, static_cast(dropEvent->property("buttons").toInt()), static_cast(dropEvent->property("modifiers").toInt())); KIO::DropJob *dropJob = KIO::drop(&de, m_url); KJobWidgets::setWindow(dropJob, QApplication::desktop()); return; } } -void IconApplet::execJumpList(int index) -{ - const QString &exec = m_jumpListActions.at(index).toMap().value(QStringLiteral("exec")).toString(); - if (exec.isEmpty()) { - return; - } - - KRun::run(exec, {}, nullptr, m_name, m_iconName); -} - void IconApplet::configure() { KPropertiesDialog *dialog = m_configDialog.data(); if (dialog) { dialog->show(); dialog->raise(); return; } dialog = new KPropertiesDialog(QUrl::fromLocalFile(m_localPath)); m_configDialog = dialog; connect(dialog, &KPropertiesDialog::applied, this, [this] { KDesktopFile desktopFile(m_localPath); if (desktopFile.hasLinkType()) { // make sure to fully repopulate in case the user changed the Link URL QFile::remove(m_localPath); setUrl(QUrl(desktopFile.readUrl())); // calls populate() itself } else { populate(); } }); dialog->setAttribute(Qt::WA_DeleteOnClose, true); dialog->setFileNameReadOnly(true); dialog->setWindowTitle(i18n("Properties for %1", m_name)); dialog->setWindowIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); dialog->show(); } QString IconApplet::localPath() const { return config().readEntry(QStringLiteral("localPath")); } void IconApplet::setLocalPath(const QString &localPath) { m_localPath = localPath; config().writeEntry(QStringLiteral("localPath"), localPath); } K_EXPORT_PLASMA_APPLET_WITH_JSON(icon, IconApplet, "metadata.json") #include "iconapplet.moc" diff --git a/applets/icon/iconapplet.h b/applets/icon/iconapplet.h index 18e528ab..bdfbc0f5 100644 --- a/applets/icon/iconapplet.h +++ b/applets/icon/iconapplet.h @@ -1,87 +1,97 @@ /* * Copyright 2016 Kai Uwe Broulik * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . * */ #pragma once #include #include #include +class KFileItemActions; +class QMenu; + class IconApplet : public Plasma::Applet { Q_OBJECT Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString iconName READ iconName NOTIFY iconNameChanged) Q_PROPERTY(QString genericName READ genericName NOTIFY genericNameChanged) - Q_PROPERTY(QVariantList jumpListActions READ jumpListActions NOTIFY jumpListActionsChanged) public: explicit IconApplet(QObject *parent, const QVariantList &data); ~IconApplet() override; void init() override; QUrl url() const; void setUrl(const QUrl &url); QString name() const; QString iconName() const; QString genericName() const; - QVariantList jumpListActions() const; + + QList contextualActions() override; Q_INVOKABLE void open(); Q_INVOKABLE void processDrop(QObject *dropEvent); - Q_INVOKABLE void execJumpList(int index); Q_INVOKABLE void configure(); signals: void urlChanged(const QUrl &url); void nameChanged(const QString &name); void iconNameChanged(const QString &iconName); void genericNameChanged(const QString &genericName); void jumpListActionsChanged(const QVariantList &jumpListActions); private: void setIconName(const QString &iconName); void populate(); void populateFromDesktopFile(const QString &path); QString localPath() const; void setLocalPath(const QString &localPath); QUrl m_url; QString m_localPath; QString m_name; QString m_iconName; QString m_genericName; - QVariantList m_jumpListActions; + + QList m_jumpListActions; + QAction *m_separatorAction = nullptr; + QList m_openWithActions; + + QAction *m_openContainingFolderAction = nullptr; + + KFileItemActions *m_fileItemActions = nullptr; + QScopedPointer m_openWithMenu; QPointer m_configDialog; }; diff --git a/applets/icon/package/contents/ui/main.qml b/applets/icon/package/contents/ui/main.qml index 709d2214..5d865fdc 100644 --- a/applets/icon/package/contents/ui/main.qml +++ b/applets/icon/package/contents/ui/main.qml @@ -1,164 +1,146 @@ /* * Copyright 2013 Bhushan Shah * Copyright 2016 Kai Uwe Broulik * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see */ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtGraphicalEffects 1.0 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as Components import org.kde.kquickcontrolsaddons 2.0 import org.kde.draganddrop 2.0 as DragDrop MouseArea { id: root readonly property bool constrained: plasmoid.formFactor === PlasmaCore.Types.Vertical || plasmoid.formFactor === PlasmaCore.Types.Horizontal property bool containsAcceptableDrag: false height: Math.round(units.iconSizes.desktop + 2 * theme.mSize(theme.defaultFont).height) width: Math.round(units.iconSizes.desktop * 1.5) Layout.minimumWidth: plasmoid.formFactor === PlasmaCore.Types.Horizontal ? height : units.iconSizes.small Layout.minimumHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical ? width : units.iconSizes.small hoverEnabled: true onClicked: plasmoid.nativeInterface.open() Plasmoid.preferredRepresentation: Plasmoid.fullRepresentation Plasmoid.icon: plasmoid.nativeInterface.iconName Plasmoid.title: plasmoid.nativeInterface.name Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground Plasmoid.onActivated: plasmoid.nativeInterface.open() Plasmoid.onContextualActionsAboutToShow: updateActions() Component.onCompleted: updateActions() function updateActions() { plasmoid.clearActions() - var actions = plasmoid.nativeInterface.jumpListActions - var jumpListCount = actions.length - for (var i = 0; i < jumpListCount; ++i) { - var item = actions[i] - plasmoid.setAction("jumplist_" + i, item.name, item.icon) - } - - if (jumpListCount) { - plasmoid.setActionSeparator("separator0") - } - plasmoid.removeAction("configure"); if (plasmoid.immutability !== PlasmaCore.Types.SystemImmutable) { plasmoid.setAction("configure", i18n("Properties"), "document-properties"); } } - function actionTriggered(name) { - if (name.indexOf("jumplist_") === 0) { - var actionIndex = parseInt(name.substr("jumplist_".length)) - plasmoid.nativeInterface.execJumpList(actionIndex) - } - } - function action_configure() { plasmoid.nativeInterface.configure() } Connections { target: plasmoid onExternalData: plasmoid.nativeInterface.url = data } DragDrop.DropArea { id: dropArea anchors.fill: parent preventStealing: true onDragEnter: root.containsAcceptableDrag = event.mimeData.hasUrls onDragLeave: root.containsAcceptableDrag = false onDrop: { plasmoid.nativeInterface.processDrop(event) root.containsAcceptableDrag = false } } PlasmaCore.IconItem { id: icon anchors{ left: parent.left right: parent.right top: parent.top bottom: constrained ? parent.bottom : text.top } source: plasmoid.icon enabled: root.enabled active: root.containsMouse || root.containsAcceptableDrag usesPlasmaTheme: false } DropShadow { id: textShadow anchors.fill: text visible: !constrained horizontalOffset: units.devicePixelRatio * 2 verticalOffset: horizontalOffset radius: 9.0 samples: 18 spread: 0.15 color: "black" source: constrained ? null : text } Components.Label { id : text text : plasmoid.title anchors { left : parent.left bottom : parent.bottom right : parent.right } height: undefined // unset Label defaults horizontalAlignment : Text.AlignHCenter visible: false // rendered by DropShadow maximumLineCount: 2 color: "white" elide: Text.ElideRight wrapMode: Text.WrapAtWordBoundaryOrAnywhere } PlasmaCore.ToolTipArea { anchors.fill: parent mainText: plasmoid.title subText: plasmoid.nativeInterface.genericName !== mainText ? plasmoid.nativeInterface.genericName :"" icon: plasmoid.icon } }