From 3ea9b789191adfaa738b8121631fc30f37f8e318 Mon Sep 17 00:00:00 2001 From: Valentijn Date: Mon, 20 Aug 2018 18:37:37 +0200 Subject: [PATCH 1/2] Support the action_hints hint --- src/notification.cpp | 14 +++++++++----- src/notificationwidgets.cpp | 32 +++++++++++++++++++++++++++----- src/notificationwidgets.h | 4 ++-- src/notifyd.cpp | 2 +- 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/notification.cpp b/src/notification.cpp index 213d7b63..3e5938c6 100644 --- a/src/notification.cpp +++ b/src/notification.cpp @@ -175,13 +175,17 @@ void Notification::setValues(const QString &application, // TODO/FIXME: Urgencies - how to handle it? } + bool action_icons = !hints[QL1S("action-icons")].isNull(); // Actions - if (actions.count() && m_actionWidget == 0) + if (actions.count()) { + if (m_actionWidget != 0) + delete m_actionWidget; + if (actions.count()/2 < 4) - m_actionWidget = new NotificationActionsButtonsWidget(actions, this); + m_actionWidget = new NotificationActionsButtonsWidget(actions, this, action_icons); else - m_actionWidget = new NotificationActionsComboWidget(actions, this); + m_actionWidget = new NotificationActionsComboWidget(actions, this, action_icons); connect(m_actionWidget, &NotificationActionsWidget::actionTriggered, this, &Notification::actionTriggered); @@ -256,7 +260,7 @@ QPixmap Notification::getPixmapFromHint(const QVariant &argument) const QPixmap Notification::getPixmapFromString(const QString &str) const { QUrl url(str); - if (url.isValid() && QFile::exists(url.toLocalFile())) + if (url.isLocalFile() && QFile::exists(url.toLocalFile())) { // qDebug() << " getPixmapFromString by URL" << url; return QPixmap(url.toLocalFile()); @@ -265,7 +269,7 @@ QPixmap Notification::getPixmapFromString(const QString &str) const { // qDebug() << " getPixmapFromString by XdgIcon theme" << str << ICONSIZE << XdgIcon::themeName(); // qDebug() << " " << XdgIcon::fromTheme(str) << "isnull:" << XdgIcon::fromTheme(str).isNull(); - // They say: do not display an icon if it;s not found - see #325 + // They say: do not display an icon if it's not found - see #325 return XdgIcon::fromTheme(str/*, XdgIcon::defaultApplicationIcon()*/).pixmap(ICONSIZE); } } diff --git a/src/notificationwidgets.cpp b/src/notificationwidgets.cpp index 82359037..9744e433 100644 --- a/src/notificationwidgets.cpp +++ b/src/notificationwidgets.cpp @@ -27,16 +27,18 @@ #include +#include #include #include #include +#include #include #include - #include "notificationwidgets.h" #include +#define ICONSIZE QSize(32, 32) NotificationActionsWidget::NotificationActionsWidget(const QStringList& actions, QWidget *parent) : QWidget(parent) @@ -65,8 +67,7 @@ NotificationActionsWidget::NotificationActionsWidget(const QStringList& actions, m_defaultAction = m_actions[0].first; } - -NotificationActionsButtonsWidget::NotificationActionsButtonsWidget(const QStringList& actions, QWidget *parent) +NotificationActionsButtonsWidget::NotificationActionsButtonsWidget(const QStringList& actions, QWidget *parent, const bool action_icons) : NotificationActionsWidget(actions, parent) { QHBoxLayout *l = new QHBoxLayout(); @@ -78,12 +79,24 @@ NotificationActionsButtonsWidget::NotificationActionsButtonsWidget(const QString { QPushButton *b = new QPushButton(action.second, this); b->setObjectName(action.first); + + if (action_icons) + { + QIcon icon = XdgIcon::fromTheme(action.first).pixmap(ICONSIZE); + + if (! icon.isNull()) { + b->setText(QString()); + b->setIcon(icon); + } + } + l->addWidget(b); group->addButton(b); if (action.first == m_defaultAction) b->setFocus(Qt::OtherFocusReason); } + connect(group, static_cast(&QButtonGroup::buttonClicked), this, &NotificationActionsButtonsWidget::actionButtonActivated); } @@ -94,7 +107,7 @@ void NotificationActionsButtonsWidget::actionButtonActivated(QAbstractButton* bu } -NotificationActionsComboWidget::NotificationActionsComboWidget(const QStringList& actions, QWidget *parent) +NotificationActionsComboWidget::NotificationActionsComboWidget(const QStringList& actions, QWidget *parent, bool action_icons) : NotificationActionsWidget(actions, parent) { QHBoxLayout *l = new QHBoxLayout(); @@ -107,8 +120,17 @@ NotificationActionsComboWidget::NotificationActionsComboWidget(const QStringList for (int i = 0; i < m_actions.count(); ++i) { auto const & action = m_actions[i]; - m_comboBox->addItem(action.second, action.first); + + if (action_icons) + { + QIcon icon = XdgIcon::fromTheme(action.first).pixmap(ICONSIZE); + if (!icon.isNull()) + { + m_comboBox->setItemIcon(i, icon); + } + } + if (action.first == m_defaultAction) { currentIndex = i; diff --git a/src/notificationwidgets.h b/src/notificationwidgets.h index e50642f3..c60290a5 100644 --- a/src/notificationwidgets.h +++ b/src/notificationwidgets.h @@ -76,7 +76,7 @@ class NotificationActionsButtonsWidget : public NotificationActionsWidget /*! Create new widget. * \param actions a list of actions in form: (key1, display1, key2, display2, ..., keyN, displayN) */ - NotificationActionsButtonsWidget(const QStringList& actions, QWidget *parent); + NotificationActionsButtonsWidget(const QStringList& actions, QWidget *parent, const bool action_icons); private slots: void actionButtonActivated(QAbstractButton* button); }; @@ -89,7 +89,7 @@ class NotificationActionsComboWidget : public NotificationActionsWidget /*! Create new widget. * \param actions a list of actions in form: (key1, display1, key2, display2, ..., keyN, displayN) */ - NotificationActionsComboWidget(const QStringList& actions, QWidget *parent); + NotificationActionsComboWidget(const QStringList& actions, QWidget *parent, const bool action_icons); private: QComboBox *m_comboBox; diff --git a/src/notifyd.cpp b/src/notifyd.cpp index 1135a211..58f24bab 100644 --- a/src/notifyd.cpp +++ b/src/notifyd.cpp @@ -73,7 +73,7 @@ QStringList Notifyd::GetCapabilities() QStringList caps; caps << QSL("actions") - // << "action-icons" + << QSL("action-icons") << QSL("body") << QSL("body-hyperlinks") << QSL("body-images") From 4f8b9a30bd389362a5787cba131f4b42bb6c2cca Mon Sep 17 00:00:00 2001 From: Valentijn Date: Thu, 13 Sep 2018 00:14:27 +0200 Subject: [PATCH 2/2] Port the tests to Python 3 and write a test for actions --- test/lxqt-notification-test-multiples.sh | 12 -- test/lxqt-notification-test-overflow.sh | 19 --- test/lxqt-notification-test.py | 164 +++++++++++++++++++++++ test/lxqt-notification-test.sh | 15 --- 4 files changed, 164 insertions(+), 46 deletions(-) delete mode 100755 test/lxqt-notification-test-multiples.sh delete mode 100755 test/lxqt-notification-test-overflow.sh create mode 100755 test/lxqt-notification-test.py delete mode 100755 test/lxqt-notification-test.sh diff --git a/test/lxqt-notification-test-multiples.sh b/test/lxqt-notification-test-multiples.sh deleted file mode 100755 index 66711fb6..00000000 --- a/test/lxqt-notification-test-multiples.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -# -# required: notify-send app installed (part of libnotify, or libnotify-tools) -# - - -# basic notifications -notify-send -u low -t 3000 --icon="document-open" "simple notification" "expires in 3s. No action there" & -notify-send -u normal -t 4000 --icon="document-close" "simple notification" "expires in 4s. No action there" & -notify-send -u low -t 3000 --icon="document-open" "simple notification" "expires in 3s. No action there" & -notify-send -u low -t 3000 --icon="document-open" "simple notification" "expires in 3s. No action there" & - diff --git a/test/lxqt-notification-test-overflow.sh b/test/lxqt-notification-test-overflow.sh deleted file mode 100755 index c65de5fc..00000000 --- a/test/lxqt-notification-test-overflow.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -# -# required: notify-send app installed (part of libnotify, or libnotify-tools) -# - -notify-send -t 8000 "tested notification" "Is it still the same size?" & - -sleep 2 - -for i in $(seq 10) -do - notify-send -t 4000 "a notification" "4 seconds notification #${i}" & -done - -for i in $(seq 40) -do - notify-send -t 2000 "a notification" "2 seconds notification #${i}" & -done - diff --git a/test/lxqt-notification-test.py b/test/lxqt-notification-test.py new file mode 100755 index 00000000..61297cf0 --- /dev/null +++ b/test/lxqt-notification-test.py @@ -0,0 +1,164 @@ +#!/usr/bin/python3 +# +# Required: python3, Glib & pydbus +# +import time +from gi.repository import GLib +from pydbus import SessionBus, Variant + +# List of example songs for the action test +SONGS = ['Revolution', 'Rocket Man', 'Brainstorm', 'Hard Rock Hallelujah'] + +class NotificationdTest: + """ + A small Python class with various tests for lxqt-notificationd + """ + def __init__(self): + self.bus = SessionBus() + self.notifications = self.bus.get(".Notifications") + self.notifications.onActionInvoked = self.action_handler + self._playing = False # keep state for a test + self._songs_index = 0 + self._id = None + + def _notify(self, summary, body, icon, timeout=-1, actions=None, hints=None, replace=0): + """ + A simple implementation of a Desktop Notifications Specification 1.2 client. + For a more detailed explanation see: https://developer.gnome.org/notification-spec/ + """ + return self.notifications.Notify("LXQt-Notificationd Test", replace, icon, summary, body, + list() if actions is None else actions, + dict() if hints is None else hints, + timeout) + + def simple(self): + """ + Basic notifications + """ + self._notify("simple notification", "expires in three seconds", "document-open", + timeout=3000, hints={'urgency': Variant('b', 0)}) # low urgency + self._notify("simple notification", "expires in four seconds", "document-close", + timeout=4000, hints={'urgency': Variant('b', 1)}) # medium urgency + self._notify("simple notification", "expires in five seconds", "application-exit", + timeout=5000, hints={'urgency': Variant('b', 2)}) # high urgency + self._notify("simple notification", "expires when the server decides", "go-next", timeout=-1) + self._notify("simple notification", "never expires.", "go-up", timeout=0) + + long_body = """expires in three seconds. No action there. Lorem ipsum +dolor sit amet, consectetur adipiscing elit. Cras vel leo quam. Morbi +sit amet lorem vel dui commodo porttitor nec ut libero. Maecenas risus +mauris, faucibus id tempus eu, auctor id purus. Vestibulum eget sapien +non sem fermentum fermentum id sed turpis. Morbi pretium sem at turpis +faucibus facilisis vel lacinia ante. Quisque a turpis lectus, quis +posuere magna. Etiam magna velit, sagittis sed tincidunt et, +adipiscing rutrum est. Aliquam aliquam aliquet tortor non +varius. Quisque sollicitudin, ligula ac pulvinar laoreet, lacus metus +sagittis nulla, ac sodales felis diam sed urna. In hac habitasse +platea dictumst. Pellentesque habitant morbi tristique senectus et +netus et malesuada fames ac turpis egestas. Pellentesque habitant +morbi tristique senectus et netus et malesuada fames ac turpis +egestas.""" + self._notify("simple notification with a very long text inside it", + long_body, "document-open", timeout=3000, hints={'urgency': Variant('b', 0)}) + + def multiple(self): + """ + Send multiple notifications with the same information which are not update requests + """ + base = lambda: self._notify("simple notification", "expires in three seconds.", \ + "document-open", hints={'urgency': Variant('b', 0)}) + base() + self._notify("simple notifications", "expires in four seconds", "document-close") + base() + base() + + def overflow(self): + """ + Deliberately overflow lxqt-notificationd + """ + self._notify("tested notification", "is it still the same size?", "", timeout=8000) + time.sleep(2) + + for i in range(0, 10): + self._notify("a notification", "four seconds notification {}".format(i), "", + timeout=4000) + + for i in range(0, 40): + self._notify("a notification", "two seconds notification {}".format(i), "", + timeout=2000) + + def _send_music_notification(self): + """ + A simple wrapper which fills in the boilerplate for the music notifications + """ + state = "start" if self._playing else "pause" + self._notify("actions notification", SONGS[self._songs_index], 'audio-headphones', + actions=['media-skip-backward', 'Previous', + 'media-playback-{}'.format(state), state, + 'media-skip-forward', 'Next'], + replace=self._id, hints={'action-icons': Variant('b', 1)}) + + def actions(self): + """ + Tests whether lxqt-notificationd properly reacts to actions and whether + it properly replaces the notifications. + + Since actions are inherently interactive, it makes the layout of the test somewhat + confusing. + """ + # Start of by sending a notification without icons + self._id = self._notify("action notification", "♫ song", "audio-headphones", + actions=['prev', 'Previous', 'toggle', 'Play', + 'next', 'Next', 'wrong', 'Wrong'], + timeout=0) + + def action_handler(self, _, key): + """ + A handler for the various actions you can invoke + """ + if key in ('media-skip-backward', 'media-playback-backward', 'prev'): + self._songs_index += 1 + if self._songs_index == len(SONGS): + self._songs_index = 0 + elif key in ('media-playback-pause', 'media-playback-start', 'toggle'): + self._playing = not self._playing + self._send_music_notification() + elif key in ('media-skip-forward', 'next'): + self._songs_index -= 1 + if self._songs_index < 0: + self._songs_index = len(SONGS) - 1 + elif key == 'wrong': + # Test the behaviour when the icon cannot be loaded + self._notify("actions notifications", SONGS[self._songs_index], 'audio-headphones', + actions=['media-playback-backward', 'Previous', + 'media-playback-start', 'Play', + 'media-skip-forward', 'Next'], + replace=self._id, hints={'action-icons': Variant('b', 1)}) + return + else: + print("Unknown action") + return + + self._send_music_notification() + + +if __name__ == "__main__": + notify = NotificationdTest() + print(""" What test do you want to run? +1. Basic notifications +2. Multiple notifications +3. Overflow lxqt-notificationd +4. Notification with actions +""") + choice = int(input("> ")) + + if choice == 1: + notify.simple() + elif choice == 2: + notify.multiple() + elif choice == 3: + notify.overflow() + elif choice == 4: + print("Exit using ^C when you're finished") + notify.actions() + GLib.MainLoop().run() diff --git a/test/lxqt-notification-test.sh b/test/lxqt-notification-test.sh deleted file mode 100755 index d260938e..00000000 --- a/test/lxqt-notification-test.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -# -# required: notify-send app installed (part of libnotify, or libnotify-tools) -# - - -# basic notifications -notify-send -u low -t 3000 --icon="document-open" "simple notification" "expires in 3s. No action there" & -notify-send -u normal -t 4000 --icon="document-close" "simple notification" "expires in 4s. No action there" & -notify-send -u critical -t 5000 --icon="application-exit" "simple notification" "expires in 5s. No action there" & -notify-send -u normal -t -1 --icon="go-next" "simple notification" "expires when server decies. No action there" & -notify-send -u normal -t 0 --icon="go-up" "simple notification" "never expires. No action there" & - -notify-send -u low -t 3000 --icon="document-open" "simple notification with a very long text inside it" "expires in 3s. No action there. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vel leo quam. Morbi sit amet lorem vel dui commodo porttitor nec ut libero. Maecenas risus mauris, faucibus id tempus eu, auctor id purus. Vestibulum eget sapien non sem fermentum fermentum id sed turpis. Morbi pretium sem at turpis faucibus facilisis vel lacinia ante. Quisque a turpis lectus, quis posuere magna. Etiam magna velit, sagittis sed tincidunt et, adipiscing rutrum est. Aliquam aliquam aliquet tortor non varius. Quisque sollicitudin, ligula ac pulvinar laoreet, lacus metus sagittis nulla, ac sodales felis diam sed urna. In hac habitasse platea dictumst. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." & -