diff --git a/applets/notifications/package/contents/ui/ThumbnailStrip.qml b/applets/notifications/package/contents/ui/ThumbnailStrip.qml index 0847cd1f8..0141192b5 100644 --- a/applets/notifications/package/contents/ui/ThumbnailStrip.qml +++ b/applets/notifications/package/contents/ui/ThumbnailStrip.qml @@ -1,196 +1,204 @@ /* * Copyright 2016 Kai Uwe Broulik * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details * * You should have received a copy of the GNU Library 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. */ import QtQuick 2.0 import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kquickcontrolsaddons 2.0 import org.kde.plasma.private.notifications 1.0 as Notifications ListView { id: previewList readonly property int itemSquareSize: units.gridUnit * 4 // if it's only one file, show a larger preview // however if no preview could be generated, which we only know after we tried, // the delegate will use itemSquareSize instead as there's no point in showing a huge file icon readonly property int itemWidth: previewList.count === 1 ? width : itemSquareSize readonly property int itemHeight: previewList.count === 1 ? Math.round(width / 3) : itemSquareSize // by the time the "model" is populated, the Layout isn't finished yet, causing ListView to have a 0 width // hence it's based on the mainLayout.width instead readonly property int maximumItemCount: Math.floor(mainLayout.width / itemSquareSize) // whether we're currently dragging, this way we can keep the popup around during the entire // drag operation even if the mouse leaves the popup property bool dragging: false model: { var urls = notificationItem.urls if (urls.length <= maximumItemCount) { return urls } // if it wouldn't fit, remove one item in favor of the "+n" badge return urls.slice(0, maximumItemCount - 1) } orientation: ListView.Horizontal spacing: units.smallSpacing interactive: false footer: notificationItem.urls.length > maximumItemCount ? moreBadge : null function pressedAction() { for (var i = 0; i < count; ++i) { var item = itemAtIndex(i) if (item.pressed) { return item } } } // HACK ListView only provides itemAt(x,y) methods but since we don't scroll // we can make assumptions on what our layout looks like... function itemAtIndex(index) { return itemAt(index * (itemSquareSize + spacing), 0) } Component { id: moreBadge // if there's more urls than we can display, show a "+n" badge Item { width: moreLabel.width height: previewList.height PlasmaExtras.Heading { id: moreLabel anchors { left: parent.left // ListView doesn't add spacing before the footer leftMargin: previewList.spacing top: parent.top bottom: parent.bottom } level: 3 verticalAlignment: Text.AlignVCenter text: i18nc("Indicator that there are more urls in the notification than previews shown", "+%1", notificationItem.urls.length - previewList.count) } } } delegate: MouseArea { id: previewDelegate // clip is expensive, only clip if the QPixmapItem would leak outside clip: previewPixmap.height > height width: thumbnailer.hasPreview ? previewList.itemWidth : previewList.itemSquareSize height: thumbnailer.hasPreview ? previewList.itemHeight : previewList.itemSquareSize preventStealing: true cursorShape: Qt.OpenHandCursor onClicked: notificationItem.openUrl(modelData) // cannot drag itself, hence dragging the Pixmap instead drag.target: previewPixmap Drag.dragType: Drag.Automatic Drag.active: previewDelegate.drag.active Drag.mimeData: ({ "text/uri-list": modelData, "text/plain": modelData }) drag.onActiveChanged: { previewList.dragging = drag.active } // first item determins the ListView height Binding { target: previewList property: "implicitHeight" value: previewDelegate.height when: index === 0 } Notifications.Thumbnailer { id: thumbnailer readonly property real ratio: pixmapSize.height ? pixmapSize.width / pixmapSize.height : 1 url: modelData size: Qt.size(previewList.itemWidth, previewList.itemHeight) } QPixmapItem { id: previewPixmap anchors.centerIn: parent width: parent.width height: width / thumbnailer.ratio pixmap: thumbnailer.pixmap smooth: true } PlasmaCore.IconItem { anchors.fill: parent source: thumbnailer.iconName usesPlasmaTheme: false } Rectangle { anchors { left: parent.left right: parent.right bottom: parent.bottom } color: theme.textColor opacity: 0.6 height: fileNameLabel.contentHeight PlasmaComponents.Label { id: fileNameLabel anchors { fill: parent leftMargin: units.smallSpacing rightMargin: units.smallSpacing } wrapMode: Text.NoWrap height: implicitHeight // unset Label default height horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter elide: Text.ElideMiddle font.pointSize: theme.smallestFont.pointSize color: theme.backgroundColor text: { var splitUrl = modelData.split("/") return splitUrl[splitUrl.length - 1] } } } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + onPressed: { + thumbnailer.showContextMenu(mouse.x, mouse.y, modelData, this) + } + } } } diff --git a/applets/notifications/plugin/CMakeLists.txt b/applets/notifications/plugin/CMakeLists.txt index cbb45224a..a942a608b 100644 --- a/applets/notifications/plugin/CMakeLists.txt +++ b/applets/notifications/plugin/CMakeLists.txt @@ -1,17 +1,18 @@ set(notificationshelper_SRCS notificationshelper.cpp notificationshelperplugin.cpp thumbnailer.cpp ) add_library(notificationshelperplugin SHARED ${notificationshelper_SRCS}) target_link_libraries(notificationshelperplugin Qt5::Core Qt5::Gui Qt5::Qml Qt5::Quick - KF5::KIOWidgets # PreviewJob + KF5::KIOWidgets + KF5::I18n ) install(TARGETS notificationshelperplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/notifications) install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/notifications) diff --git a/applets/notifications/plugin/thumbnailer.cpp b/applets/notifications/plugin/thumbnailer.cpp index 83188b309..418cba6dd 100644 --- a/applets/notifications/plugin/thumbnailer.cpp +++ b/applets/notifications/plugin/thumbnailer.cpp @@ -1,130 +1,199 @@ /* Copyright (C) 2016 Kai Uwe Broulik This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "thumbnailer.h" #include +#include +#include #include #include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include Thumbnailer::Thumbnailer(QObject *parent) : QObject(parent) { } Thumbnailer::~Thumbnailer() = default; void Thumbnailer::classBegin() { } void Thumbnailer::componentComplete() { m_inited = true; generatePreview(); } QUrl Thumbnailer::url() const { return m_url; } void Thumbnailer::setUrl(const QUrl &url) { if (m_url != url) { m_url = url; emit urlChanged(); generatePreview(); } } QSize Thumbnailer::size() const { return m_size; } void Thumbnailer::setSize(const QSize &size) { if (m_size != size) { m_size = size; emit sizeChanged(); generatePreview(); } } bool Thumbnailer::hasPreview() const { return !m_pixmap.isNull(); } QPixmap Thumbnailer::pixmap() const { return m_pixmap; } QSize Thumbnailer::pixmapSize() const { return m_pixmap.size(); } QString Thumbnailer::iconName() const { return m_iconName; } +void Thumbnailer::showContextMenu(int x, int y, const QString &path, QQuickItem *ctx) +{ + if (!ctx || !ctx->window()) { + return; + } + + const QUrl url(path); + if (!url.isValid()) { + return; + } + + KFileItem fileItem(url); + + QMenu *menu = new QMenu(); + menu->setAttribute(Qt::WA_DeleteOnClose, true); + + if (KProtocolManager::supportsListing(url)) { + QAction *openContainingFolderAction = menu->addAction(QIcon::fromTheme("folder-open"), i18n("Open Containing Folder")); + connect(openContainingFolderAction, &QAction::triggered, [url] { + KIO::highlightInFileManager({url}); + }); + } + + menu->addSeparator(); + + // KStandardAction? But then the Ctrl+C shortcut makes no sense in this context + QAction *copyAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("&Copy")); + connect(copyAction, &QAction::triggered, [fileItem] { + // inspired by KDirModel::mimeData() + QMimeData *data = new QMimeData(); // who cleans it up? + KUrlMimeData::setUrls({fileItem.url()}, {fileItem.mostLocalUrl()}, data); + QApplication::clipboard()->setMimeData(data); + }); + + KFileItemActions *actions = new KFileItemActions(menu); + KFileItemListProperties itemProperties(KFileItemList({fileItem})); + actions->setItemListProperties(itemProperties); + + actions->addOpenWithActionsTo(menu); + actions->addServiceActionsTo(menu); + actions->addPluginActionsTo(menu); + + if (menu->isEmpty()) { + delete menu; + return; + } + + if (ctx->window()->mouseGrabberItem()) { + ctx->window()->mouseGrabberItem()->ungrabMouse(); + } + + const QPoint pos = ctx->mapToGlobal(QPointF(x, y)).toPoint(); + menu->popup(pos); +} + void Thumbnailer::generatePreview() { if (!m_inited) { return; } if (!m_url.isValid() || !m_url.isLocalFile() || !m_size.isValid()) { return; } KIO::PreviewJob *job = KIO::filePreview(KFileItemList({KFileItem(m_url)}), m_size); job->setIgnoreMaximumSize(true); connect(job, &KIO::PreviewJob::gotPreview, this, [this](const KFileItem &item, const QPixmap &preview) { Q_UNUSED(item); m_pixmap = preview; emit pixmapChanged(); if (!m_iconName.isEmpty()) { m_iconName.clear(); emit iconNameChanged(); } }); connect(job, &KIO::PreviewJob::failed, this, [this](const KFileItem &item) { m_pixmap = QPixmap(); emit pixmapChanged(); const QString &iconName = item.determineMimeType().iconName(); if (m_iconName != iconName) { m_iconName = iconName; emit iconNameChanged(); } }); job->start(); } diff --git a/applets/notifications/plugin/thumbnailer.h b/applets/notifications/plugin/thumbnailer.h index 7bce2cad6..35df6a3bd 100644 --- a/applets/notifications/plugin/thumbnailer.h +++ b/applets/notifications/plugin/thumbnailer.h @@ -1,79 +1,83 @@ /* Copyright (C) 2016 Kai Uwe Broulik This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include #include #include #include #include +class QQuickItem; + class Thumbnailer : public QObject, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) Q_PROPERTY(QSize size READ size WRITE setSize NOTIFY sizeChanged) Q_PROPERTY(bool hasPreview READ hasPreview NOTIFY pixmapChanged) Q_PROPERTY(QPixmap pixmap READ pixmap NOTIFY pixmapChanged) Q_PROPERTY(QSize pixmapSize READ pixmapSize NOTIFY pixmapChanged) Q_PROPERTY(QString iconName READ iconName NOTIFY iconNameChanged) public: explicit Thumbnailer(QObject *parent = nullptr); ~Thumbnailer() override; QUrl url() const; void setUrl(const QUrl &url); QSize size() const; void setSize(const QSize &size); bool hasPreview() const; QPixmap pixmap() const; QSize pixmapSize() const; QString iconName() const; + Q_INVOKABLE void showContextMenu(int x, int y, const QString &path, QQuickItem *ctx); + void classBegin() override; void componentComplete() override; signals: void urlChanged(); void sizeChanged(); void pixmapChanged(); void iconNameChanged(); private: void generatePreview(); bool m_inited = false; QUrl m_url; QSize m_size; QPixmap m_pixmap; QString m_iconName; };