diff --git a/applets/notifications/package/contents/ui/NotificationItem.qml b/applets/notifications/package/contents/ui/NotificationItem.qml --- a/applets/notifications/package/contents/ui/NotificationItem.qml +++ b/applets/notifications/package/contents/ui/NotificationItem.qml @@ -27,6 +27,8 @@ 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 + Item { id: notificationItem width: parent.width @@ -48,6 +50,7 @@ property alias body: bodyText.text property alias configurable: settingsButton.visible property var created + property var urls: [] property int maximumTextHeight: -1 @@ -61,6 +64,8 @@ } } + // TODO take into account previewList items so one can click to open the file + if (settingsButton.pressed) { return settingsButton } @@ -309,6 +314,137 @@ } } } + + ListView { + id: previewList + + readonly property int itemSquareSize: units.gridUnit * 4 + + // if it's only one file, show a larger preview + readonly property int itemWidth: previewList.count === 1 ? width : itemSquareSize + readonly property int itemHeight: previewList.count === 1 ? Math.round(width / 2) : 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) + + 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 + + Layout.fillWidth: true + Layout.preferredHeight: itemHeight + visible: count > 0 + + footer: notificationItem.urls.length > maximumItemCount ? moreBadge : null + + 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: previewList.itemWidth + height: previewList.itemHeight + + preventStealing: true + cursorShape: Qt.OpenHandCursor + + onClicked: Qt.openUrlExternally(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 + }) + + 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 + } + + 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] + } + } + } + } + } } } diff --git a/applets/notifications/package/contents/ui/NotificationPopup.qml b/applets/notifications/package/contents/ui/NotificationPopup.qml --- a/applets/notifications/package/contents/ui/NotificationPopup.qml +++ b/applets/notifications/package/contents/ui/NotificationPopup.qml @@ -113,6 +113,7 @@ icon: notificationProperties ? notificationProperties.appIcon : "" image: notificationProperties ? notificationProperties.image : undefined configurable: (notificationProperties ? notificationProperties.configurable : false) && !Settings.isMobile + urls: notificationProperties ? notificationProperties.urls : [] x: units.smallSpacing y: units.smallSpacing diff --git a/applets/notifications/plugin/CMakeLists.txt b/applets/notifications/plugin/CMakeLists.txt --- a/applets/notifications/plugin/CMakeLists.txt +++ b/applets/notifications/plugin/CMakeLists.txt @@ -1,13 +1,16 @@ 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) + Qt5::Quick + KF5::KIOWidgets # PreviewJob + ) install(TARGETS notificationshelperplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/notifications) diff --git a/applets/notifications/plugin/notificationshelperplugin.cpp b/applets/notifications/plugin/notificationshelperplugin.cpp --- a/applets/notifications/plugin/notificationshelperplugin.cpp +++ b/applets/notifications/plugin/notificationshelperplugin.cpp @@ -18,6 +18,7 @@ #include "notificationshelperplugin.h" #include "notificationshelper.h" +#include "thumbnailer.h" #include #include @@ -72,6 +73,8 @@ Q_ASSERT(uri == QLatin1String("org.kde.plasma.private.notifications")); qmlRegisterType(uri, 1, 0, "NotificationsHelper"); + qmlRegisterType(uri, 1, 0, "Thumbnailer"); + qmlRegisterSingletonType(uri, 1, 0, "UrlHelper", urlcheck_singletontype_provider); qmlRegisterSingletonType(uri, 1, 0, "ProcessRunner", processrunner_singleton_provider); } diff --git a/applets/notifications/plugin/thumbnailer.h b/applets/notifications/plugin/thumbnailer.h new file mode 100644 --- /dev/null +++ b/applets/notifications/plugin/thumbnailer.h @@ -0,0 +1,71 @@ +/* + 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 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(QPixmap pixmap READ pixmap NOTIFY pixmapChanged) + Q_PROPERTY(QSize pixmapSize READ pixmapSize NOTIFY pixmapChanged) + +public: + explicit Thumbnailer(QObject *parent = nullptr); + ~Thumbnailer() override; + + QUrl url() const; + void setUrl(const QUrl &url); + + QSize size() const; + void setSize(const QSize &size); + + QPixmap pixmap() const; + QSize pixmapSize() const; + + 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; + +}; diff --git a/applets/notifications/plugin/thumbnailer.cpp b/applets/notifications/plugin/thumbnailer.cpp new file mode 100644 --- /dev/null +++ b/applets/notifications/plugin/thumbnailer.cpp @@ -0,0 +1,109 @@ +/* + 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 + +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(); + } +} + +QPixmap Thumbnailer::pixmap() const +{ + return m_pixmap; +} + +QSize Thumbnailer::pixmapSize() const +{ + return m_pixmap.size(); +} + +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(); + }); + + connect(job, &KIO::PreviewJob::failed, this, [this](const KFileItem &item) { + m_pixmap = QIcon::fromTheme(item.determineMimeType().iconName()).pixmap(m_size); + emit pixmapChanged(); + }); + + job->start(); +} diff --git a/dataengines/notifications/notificationsengine.cpp b/dataengines/notifications/notificationsengine.cpp --- a/dataengines/notifications/notificationsengine.cpp +++ b/dataengines/notifications/notificationsengine.cpp @@ -190,6 +190,7 @@ const QString appRealName = hints[QStringLiteral("x-kde-appname")].toString(); const QString eventId = hints[QStringLiteral("x-kde-eventId")].toString(); const bool skipGrouping = hints[QStringLiteral("x-kde-skipGrouping")].toBool(); + const QStringList &urls = hints[QStringLiteral("x-kde-urls")].toStringList(); // group notifications that have the same title coming from the same app // or if they are on the "blacklist", honor the skipGrouping hint sent @@ -342,6 +343,8 @@ notificationData.insert(QStringLiteral("urgency"), hints[QStringLiteral("urgency")].toInt()); } + notificationData.insert(QStringLiteral("urls"), urls); + setData(source, notificationData); m_activeNotifications.insert(source, app_name + summary);