diff --git a/applets/icon/iconapplet.cpp b/applets/icon/iconapplet.cpp index 4afa76bba..89ce3a549 100644 --- a/applets/icon/iconapplet.cpp +++ b/applets/icon/iconapplet.cpp @@ -1,474 +1,515 @@ /* * 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::configChanged() { 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; } setBusy(true); // unset in populateFromDesktopFile where we'll end up in if all goes well auto *statJob = KIO::stat(m_url, KIO::HideProgressInfo); connect(statJob, &KIO::StatJob::finished, this, [=] { QString desiredDesktopFileName = m_url.fileName(); // in doubt, just encode the entire URL, e.g. http://www.kde.org/ has no filename if (desiredDesktopFileName.isEmpty()) { desiredDesktopFileName = KIO::encodeFileName(m_url.toDisplayString()); } // 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 const QUrl url = statJob->mostLocalUrl(); if (url.isLocalFile()) { const QString localUrlString = 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; } } // in all other cases just make it a link QString iconName; QString genericName; if (!statJob->error()) { KFileItem item(statJob->statResult(), url); if (name.isEmpty()) { name = item.text(); } if (item.mimetype() != QLatin1String("application/octet-stream")) { iconName = item.iconName(); genericName = item.mimeComment(); } } // KFileItem might return "." as text for e.g. root folders if (name == QLatin1String(".")) { name.clear(); } if (name.isEmpty()) { name = url.fileName(); } if (name.isEmpty()) { // TODO would be cool to just show the parent folder name instead of the full path name = url.path(); } // For websites the filename e.g. "index.php" is usually not what you want // also "/" isn't very descript when it's not our local "root" folder if (name.isEmpty() || url.scheme().startsWith(QLatin1String("http")) || (!url.isLocalFile() && name == QLatin1String("/"))) { name = url.host(); } if (iconName.isEmpty()) { // In doubt ask KIO::iconNameForUrl, KFileItem can't cope with http:// URLs for instance iconName = KIO::iconNameForUrl(url); } KDesktopFile linkDesktopFile(backingDesktopFile); auto desktopGroup = linkDesktopFile.desktopGroup(); desktopGroup.writeEntry(QStringLiteral("Name"), name); desktopGroup.writeEntry(QStringLiteral("Type"), QStringLiteral("Link")); desktopGroup.writeEntry(QStringLiteral("URL"), url); desktopGroup.writeEntry(QStringLiteral("Icon"), iconName); if (!genericName.isEmpty()) { desktopGroup.writeEntry(QStringLiteral("GenericName"), genericName); } 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()); 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; } 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); }); m_jumpListActions << action; } m_localPath = path; setBusy(false); } 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; } QList IconApplet::contextualActions() { 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::run() { new KRun(QUrl::fromLocalFile(m_localPath), QApplication::desktop()); } void IconApplet::processDrop(QObject *dropEvent) { Q_ASSERT(dropEvent); + Q_ASSERT(isAcceptableDrag(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); - } - } + const auto &urls = urlsFromDrop(dropEvent); 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")))) { + if (isExecutable(mimeType)) { // isAcceptableDrag has the KAuthorized check for this 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; } } +bool IconApplet::isAcceptableDrag(QObject *dropEvent) +{ + Q_ASSERT(dropEvent); + + const auto &urls = urlsFromDrop(dropEvent); + + if (urls.isEmpty()) { + return false; + } + + const QString &localPath = m_url.toLocalFile(); + if (KDesktopFile::isDesktopFile(localPath)) { + return true; + } + + QMimeDatabase db; + const QMimeType mimeType = db.mimeTypeForUrl(m_url); + + if (KAuthorized::authorize(QStringLiteral("shell_access")) && isExecutable(mimeType)) { + return true; + } + + if (mimeType.inherits(QStringLiteral("inode/directory"))) { + return true; + } + + return false; +} + +QList IconApplet::urlsFromDrop(QObject *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()); + for (const QJsonValue &droppedUrl : droppedUrls) { + const QUrl url(droppedUrl.toString()); + if (url.isValid()) { + urls.append(url); + } + } + + return urls; +} + +bool IconApplet::isExecutable(const QMimeType &mimeType) +{ + return (mimeType.inherits(QStringLiteral("application/x-executable")) + || mimeType.inherits(QStringLiteral("application/x-shellscript"))); +} + 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 605ae02cb..7018f3483 100644 --- a/applets/icon/iconapplet.h +++ b/applets/icon/iconapplet.h @@ -1,98 +1,103 @@ /* * 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) public: explicit IconApplet(QObject *parent, const QVariantList &data); ~IconApplet() override; void init() override; void configChanged() override; QUrl url() const; void setUrl(const QUrl &url); QString name() const; QString iconName() const; QString genericName() const; QList contextualActions() override; Q_INVOKABLE void run(); Q_INVOKABLE void processDrop(QObject *dropEvent); Q_INVOKABLE void configure(); + Q_INVOKABLE bool isAcceptableDrag(QObject *dropEvent); + 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); + static QList urlsFromDrop(QObject *dropEvent); + static bool isExecutable(const QMimeType &mimeType); + 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; 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 2093427b6..0a7f3c00a 100644 --- a/applets/icon/package/contents/ui/main.qml +++ b/applets/icon/package/contents/ui/main.qml @@ -1,147 +1,159 @@ /* * 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 + 2 * theme.mSize(theme.defaultFont).height) hoverEnabled: true onClicked: plasmoid.nativeInterface.run() Plasmoid.preferredRepresentation: Plasmoid.fullRepresentation Plasmoid.icon: plasmoid.nativeInterface.iconName Plasmoid.title: plasmoid.nativeInterface.name Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground Plasmoid.onActivated: plasmoid.nativeInterface.run() Plasmoid.onContextualActionsAboutToShow: updateActions() Component.onCompleted: updateActions() function updateActions() { plasmoid.clearActions() plasmoid.removeAction("configure"); if (plasmoid.immutability !== PlasmaCore.Types.SystemImmutable) { plasmoid.setAction("configure", i18n("Properties"), "document-properties"); } } 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 + onDragEnter: { + var acceptable = plasmoid.nativeInterface.isAcceptableDrag(event); + root.containsAcceptableDrag = acceptable; + + if (!acceptable) { + event.ignore(); + } + } onDragLeave: root.containsAcceptableDrag = false onDrop: { - plasmoid.nativeInterface.processDrop(event) + if (root.containsAcceptableDrag) { + plasmoid.nativeInterface.processDrop(event) + } else { + event.ignore(); + } + 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 opacity: plasmoid.busy ? 0.6 : 1 } 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 } }