Commit 3251c280 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

Implement more missing stuff and be more spec-compliant

- Support replacing notifications properly
  - Now that we have nice models we can finally do the replacing in the way the spec
  asks for "atomically (ie with no flicker or other visual cues)"
- Catch some more cases in notification details text (trash:/ stuff)
- Prefer notifyrc name over desktop entry name in case a service within another app sends
  an event (e.g. Discover notifier in Plasma, KNotification automatically sends desktop-entry
  of the parent app)
- Implement x-kde-urls thumbnailer with drag and drop and fancy blur
  Originally intended to be able to show multiple files but in practise only used for
  single pictures, so the code is significantly simplified for that usecase
- Add "Open" feature for finished job notification, offers a context menu as well with
  all the KFileItemActions (component from the Thumbnailer)
- Copy description labels to clipboard
  Once menu opens the update is paused so the text you copy is what you see
- Fixup sanitizer so we can properly collapse notifications without body text

... and more
parent fb620e07
......@@ -2,6 +2,8 @@ add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.plasma.notification
set(notificationapplet_SRCS
notificationapplet.cpp
filemenu.cpp
thumbnailer.cpp
)
add_library(plasma_applet_newnotifications MODULE ${notificationapplet_SRCS}) # FIXME
......@@ -11,8 +13,12 @@ kcoreaddons_desktop_to_json(plasma_applet_newnotifications package/metadata.desk
target_link_libraries(plasma_applet_newnotifications # FIXME
# shouldnt require it as it's all QML, isn't it?
#PW::NotificationManager
Qt5::Gui
Qt5::Quick # for QQmlParserStatus
KF5::I18n
KF5::Plasma)
KF5::Plasma
KF5::KIOWidgets # for PreviewJob
)
install(TARGETS plasma_applet_newnotifications DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/applets) # FIXME
......
/*
Copyright (C) 2016,2019 Kai Uwe Broulik <kde@privat.broulik.de>
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 "filemenu.h"
#include <QApplication>
#include <QClipboard>
#include <QIcon>
#include <QMimeData>
#include <QMenu>
#include <QQuickItem>
#include <QTimer>
#include <QQuickWindow>
#include <KFileItemActions>
#include <KFileItemListProperties>
#include <KLocalizedString>
#include <KProtocolManager>
#include <KPropertiesDialog>
#include <KUrlMimeData>
#include <KIO/OpenFileManagerWindowJob>
FileMenu::FileMenu(QObject *parent) : QObject(parent)
{
}
FileMenu::~FileMenu() = default;
QUrl FileMenu::url() const
{
return m_url;
}
void FileMenu::setUrl(const QUrl &url)
{
if (m_url != url) {
m_url = url;
emit urlChanged();
}
}
QQuickItem *FileMenu::visualParent() const
{
return m_visualParent.data();
}
void FileMenu::setVisualParent(QQuickItem *visualParent)
{
if (m_visualParent.data() == visualParent) {
return;
}
if (m_visualParent) {
disconnect(m_visualParent.data(), nullptr, this, nullptr);
}
m_visualParent = visualParent;
if (m_visualParent) {
connect(m_visualParent.data(), &QObject::destroyed, this, &FileMenu::visualParentChanged);
}
emit visualParentChanged();
}
bool FileMenu::visible() const
{
return m_visible;
}
void FileMenu::setVisible(bool visible)
{
if (m_visible == visible) {
return;
}
if (visible) {
open(0, 0);
} else {
// TODO warning or close?
}
}
void FileMenu::open(int x, int y)
{
if (!m_visualParent || !m_visualParent->window()) {
return;
}
if (!m_url.isValid()) {
return;
}
KFileItem fileItem(m_url);
QMenu *menu = new QMenu();
menu->setAttribute(Qt::WA_DeleteOnClose, true);
connect(menu, &QMenu::aboutToHide, this, [this] {
m_visible = false;
emit visibleChanged();
});
if (KProtocolManager::supportsListing(m_url)) {
QAction *openContainingFolderAction = menu->addAction(QIcon::fromTheme(QStringLiteral("folder-open")), i18n("Open Containing Folder"));
connect(openContainingFolderAction, &QAction::triggered, [this] {
KIO::highlightInFileManager({m_url});
});
}
KFileItemActions *actions = new KFileItemActions(menu);
KFileItemListProperties itemProperties(KFileItemList({fileItem}));
actions->setItemListProperties(itemProperties);
actions->addOpenWithActionsTo(menu);
// 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);
});
actions->addServiceActionsTo(menu);
actions->addPluginActionsTo(menu);
QAction *propertiesAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("Properties"));
connect(propertiesAction, &QAction::triggered, [fileItem] {
KPropertiesDialog *dialog = new KPropertiesDialog(fileItem.url());
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
});
//this is a workaround where Qt will fail to realize a mouse has been released
// this happens if a window which does not accept focus spawns a new window that takes focus and X grab
// whilst the mouse is depressed
// https://bugreports.qt.io/browse/QTBUG-59044
// this causes the next click to go missing
//by releasing manually we avoid that situation
auto ungrabMouseHack = [this]() {
if (m_visualParent && m_visualParent->window() && m_visualParent->window()->mouseGrabberItem()) {
m_visualParent->window()->mouseGrabberItem()->ungrabMouse();
}
};
QTimer::singleShot(0, m_visualParent, ungrabMouseHack);
//end workaround
QPoint pos;
if (x == -1 && y == -1) { // align "bottom left of visualParent"
menu->adjustSize();
pos = m_visualParent->mapToGlobal(QPointF(0, m_visualParent->height())).toPoint();
if (!qApp->isRightToLeft()) {
pos.rx() += m_visualParent->width();
pos.rx() -= menu->width();
}
} else {
pos = m_visualParent->mapToGlobal(QPointF(x, y)).toPoint();
}
menu->popup(pos);
m_visible = true;
emit visibleChanged();
}
/*
Copyright (C) 2016,2019 Kai Uwe Broulik <kde@privat.broulik.de>
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 <QObject>
#include <QPointer>
#include <QUrl>
class QQuickItem;
class FileMenu : public QObject
{
Q_OBJECT
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)
Q_PROPERTY(QQuickItem *visualParent READ visualParent WRITE setVisualParent NOTIFY visualParentChanged)
Q_PROPERTY(bool visible READ visible WRITE setVisible NOTIFY visibleChanged)
public:
explicit FileMenu(QObject *parent = nullptr);
~FileMenu() override;
QUrl url() const;
void setUrl(const QUrl &url);
QQuickItem *visualParent() const;
void setVisualParent(QQuickItem *visualParent);
bool visible() const;
void setVisible(bool visible);
Q_INVOKABLE void open(int x, int y);
signals:
void urlChanged();
void visualParentChanged();
void visibleChanged();
private:
QUrl m_url;
QPointer<QQuickItem> m_visualParent;
bool m_visible;
};
......@@ -21,10 +21,26 @@
#include "notificationapplet.h"
#include <QDrag>
#include <QMimeData>
#include <QQuickItem>
#include <QQuickWindow>
#include <QStyleHints>
#include "filemenu.h"
#include "thumbnailer.h"
NotificationApplet::NotificationApplet(QObject *parent, const QVariantList &data)
: Plasma::Applet(parent, data)
{
static bool s_typesRegistered = false;
if (!s_typesRegistered) {
const char uri[] = "org.kde.plasma.private.notifications";
qmlRegisterType<FileMenu>(uri, 2, 0, "FileMenu");
qmlRegisterType<Thumbnailer>(uri, 2, 0, "Thumbnailer");
qmlProtectModule(uri, 2);
s_typesRegistered = true;
}
}
NotificationApplet::~NotificationApplet() = default;
......@@ -39,6 +55,54 @@ void NotificationApplet::configChanged()
}
bool NotificationApplet::dragActive() const
{
return m_dragActive;
}
bool NotificationApplet::isDrag(int oldX, int oldY, int newX, int newY) const
{
return ((QPoint(oldX, oldY) - QPoint(newX, newY)).manhattanLength() >= qApp->styleHints()->startDragDistance());
}
void NotificationApplet::startDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap)
{
// This allows the caller to return, making sure we don't crash if
// the caller is destroyed mid-drag
QMetaObject::invokeMethod(this, "doDrag", Qt::QueuedConnection,
Q_ARG(QQuickItem*, item), Q_ARG(QUrl, url), Q_ARG(QPixmap, pixmap));
}
void NotificationApplet::doDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap)
{
if (item && item->window() && item->window()->mouseGrabberItem()) {
item->window()->mouseGrabberItem()->ungrabMouse();
}
QDrag *drag = new QDrag(item);
QMimeData *mimeData = new QMimeData();
if (!url.isEmpty()) {
mimeData->setUrls(QList<QUrl>() << url);
}
drag->setMimeData(mimeData);
if (!pixmap.isNull()) {
drag->setPixmap(pixmap);
}
m_dragActive = true;
emit dragActiveChanged();
drag->exec();
m_dragActive = false;
emit dragActiveChanged();
}
K_EXPORT_PLASMA_APPLET_WITH_JSON(icon, NotificationApplet, "metadata.json")
#include "notificationapplet.moc"
......@@ -23,10 +23,14 @@
#include <Plasma/Applet>
class QQuickItem;
class NotificationApplet : public Plasma::Applet
{
Q_OBJECT
Q_PROPERTY(bool dragActive READ dragActive NOTIFY dragActiveChanged)
public:
explicit NotificationApplet(QObject *parent, const QVariantList &data);
~NotificationApplet() override;
......@@ -34,6 +38,17 @@ public:
void init() override;
void configChanged() override;
bool dragActive() const;
Q_INVOKABLE bool isDrag(int oldX, int oldY, int newX, int newY) const;
Q_INVOKABLE void startDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap);
signals:
void dragActiveChanged();
private slots:
void doDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap);
private:
bool m_dragActive = false;
};
......@@ -138,8 +138,8 @@ ColumnLayout {
// TODO everything else
onCloseClicked: historyModel.close(historyModel.makeModelIndex(index))
onConfigureClicked: historyModel.configure(historyModel.makeModelIndex(index))
onCloseClicked: historyModel.close(historyModel.index(index, 0))
onConfigureClicked: historyModel.configure(historyModel.index(index, 0))
svg: lineSvg
}
......
......@@ -26,6 +26,7 @@ import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.notificationmanager 1.0 as NotificationManager
import org.kde.plasma.private.notifications 2.0 as Notifications
ColumnLayout {
id: jobItem
......@@ -47,6 +48,8 @@ ColumnLayout {
signal resumeJobClicked
signal killJobClicked
signal openUrl(string url)
spacing: 0
PlasmaComponents.Label {
......@@ -93,14 +96,15 @@ ColumnLayout {
iconSource: "media-playback-stop"
onClicked: jobItem.killJobClicked()
}
}
PlasmaComponents.ToolButton {
id: expandButton
iconSource: checked ? "arrow-down" : (LayoutMirroring.enabled ? "arrow-left" : "arrow-right")
tooltip: checked ? i18nc("A button tooltip; hides item details", "Hide Details")
: i18nc("A button tooltip; expands the item to show details", "Show Details")
checkable: true
PlasmaComponents.ToolButton {
id: expandButton
Layout.leftMargin: units.smallSpacing
iconSource: checked ? "arrow-down" : (LayoutMirroring.enabled ? "arrow-left" : "arrow-right")
tooltip: checked ? i18nc("A button tooltip; hides item details", "Hide Details")
: i18nc("A button tooltip; expands the item to show details", "Show Details")
checkable: true
}
}
}
......@@ -114,29 +118,50 @@ ColumnLayout {
}
}
// TODO Notification actions
// Always show "Open Containing Folder" if possible
// Add "Open"/Open With or even hamburger menu with KFileItemActions etc for single files
/*Flow { // it's a Flow so it can wrap if too long
// FIXME probably doesnt need a flow
id: finishedJobActionsFlow
Flow { // it's a Flow so it can wrap if too long
id: jobDoneActions
Layout.fillWidth: true
spacing: units.smallSpacing
// We want the actions to be right-aligned but Flow also reverses
// the order of items, so we put them in reverse order
layoutDirection: Qt.RightToLeft
visible: false
visible: url && url.toString() !== ""
property var url: {
if (jobItem.jobState !== NotificationManager.Notifications.JobStateStopped
|| jobItem.error
|| jobItem.jobDetails.totalFiles <= 0) {
return null;
}
// For a single file show actions for it
if (jobItem.jobDetails.totalFiles === 1) {
return jobItem.jobDetails.descriptionUrl;
} else {
return jobItem.jobDetails.destUrl;
}
}
PlasmaComponents.Button {
iconSource: "application-menu"
//text: i18n("Open Containing Folder")
id: otherFileActionsButton
iconName: "application-menu"
tooltip: i18n("More Options...")
onClicked: otherFileActionsMenu.open(-1, -1)
Notifications.FileMenu {
id: otherFileActionsMenu
url: jobDoneActions.url
visualParent: otherFileActionsButton
}
}
PlasmaComponents.Button {
// would be nice to have the file icon here?
text: jobItem.jobDetails.totalFiles > 1 ? i18n("Open Containing Folder") : i18n("Open")
onClicked: jobItem.openUrl(jobDoneActions.url)
width: minimumWidth
text: i18n("Open...")
}
}*/
}
states: [
State {
......@@ -157,21 +182,10 @@ ColumnLayout {
target: jobActionsRow
visible: false
}
// FIXME move everything in job actions row?
// TODO Should we keep the details accessible when the job finishes?
PropertyChanges {
target: expandButton
checked: false
visible: false
}
PropertyChanges {
target: finishedJobActionsFlow
visible: true
}
/*PropertyChanges {
target: openButton
visible: !jobItem.error // && we have a sensible location
}*/
}
]
}
/*
* Copyright 2018 Kai Uwe Broulik <kde@privat.broulik.de>
* Copyright 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
......@@ -52,6 +52,7 @@ ColumnLayout {
// This isn't an alias because TextEdit RichText adds some HTML tags to it
property string body
property alias icon: iconItem.source
property var urls: []
property int jobState
property int percentage
......@@ -67,11 +68,17 @@ ColumnLayout {
property var actionNames: []
property var actionLabels: []
property int thumbnailLeftPadding: 0
property int thumbnailRightPadding: 0
property int thumbnailTopPadding: 0
property int thumbnailBottomPadding: 0
signal bodyClicked(var mouse) // TODO bodyClicked?
signal closeClicked
signal configureClicked
signal dismissClicked
signal actionInvoked(string actionName)
signal openUrl(string url)
signal suspendJobClicked
signal resumeJobClicked
......@@ -213,7 +220,8 @@ ColumnLayout {
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
textFormat: Text.PlainText
wrapMode: Text.NoWrap
maximumLineCount: 3
wrapMode: Text.WordWrap
elide: Text.ElideRight
level: 4
text: {
......@@ -289,6 +297,8 @@ ColumnLayout {
onResumeJobClicked: notificationItem.resumeJobClicked()
onKillJobClicked: notificationItem.killJobClicked()
onOpenUrl: notificationItem.openUrl(url)
hovered: notificationItem.hovered
}
}
......@@ -314,4 +324,24 @@ ColumnLayout {
}
}
}
// thumbnails
Loader {
id: thumbnailStripLoader
Layout.leftMargin: notificationItem.thumbnailLeftPadding
Layout.rightMargin: notificationItem.thumbnailRightPadding
Layout.topMargin: notificationItem.thumbnailTopPadding
Layout.bottomMargin: notificationItem.thumbnailBottomPadding
Layout.fillWidth: true
active: notificationItem.urls.length > 0
visible: active
sourceComponent: ThumbnailStrip {
leftPadding: -thumbnailStripLoader.Layout.leftMargin
rightPadding: -thumbnailStripLoader.Layout.rightMargin
topPadding: -thumbnailStripLoader.Layout.topMargin
bottomPadding: -thumbnailStripLoader.Layout.bottomMargin
urls: notificationItem.urls
onOpenUrl: notificationItem.openUrl(url)
}
}
}
/*
* Copyright 2016 Kai Uwe Broulik <kde@privat.broulik.de>
*
* 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 QtGraphicalEffects 1.0
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 as KQCAddons
import org.kde.plasma.private.notifications 2.0 as Notifications
MouseArea {
id: thumbnailArea