Commit 5f6f4634 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇
Browse files

[Notifications] Add quick reply feature

This adds a quick reply feature with a text field inline in the notification popup.
An action named "inline-reply" will spawn the text field and a NotificationReplied signal is emitted then.
There's additional kde hints for changing the placeholder text (defaults to "Type a reply..."),
submit button text (defaults to "Send") and submit button icon name (defaults to "document-send",
that paper aeroplane icon).
parent cd6e94a7
......@@ -31,6 +31,9 @@
#include <QQuickWindow>
#include <QScreen>
#include <QStyleHints>
#include <QWindow>
#include <KWindowSystem>
#include <PlasmaQuick/Dialog>
......@@ -165,6 +168,13 @@ QString NotificationApplet::iconNameForUrl(const QUrl &url) const
return mime.iconName();
}
void NotificationApplet::forceActivateWindow(QWindow *window)
{
if (window && window->winId()) {
KWindowSystem::forceActiveWindow(window->winId());
}
}
K_EXPORT_PLASMA_APPLET_WITH_JSON(icon, NotificationApplet, "metadata.json")
#include "notificationapplet.moc"
......@@ -26,6 +26,7 @@
class QQuickItem;
class QString;
class QRect;
class QWindow;
class NotificationApplet : public Plasma::Applet
{
......@@ -59,6 +60,7 @@ public:
Q_INVOKABLE bool isPrimaryScreen(const QRect &rect) const;
Q_INVOKABLE QString iconNameForUrl(const QUrl &url) const;
Q_INVOKABLE void forceActivateWindow(QWindow *window);
signals:
void dragActiveChanged();
......
......@@ -71,6 +71,12 @@ ColumnLayout {
property var actionNames: []
property var actionLabels: []
property bool hasReplyAction
property string replyActionLabel
property string replyPlaceholderText
property string replySubmitButtonText
property string replySubmitButtonIconName
property int headingLeftPadding: 0
property int headingRightPadding: 0
......@@ -85,14 +91,17 @@ ColumnLayout {
readonly property bool menuOpen: bodyLabel.contextMenu !== null
|| (thumbnailStripLoader.item && thumbnailStripLoader.item.menuOpen)
|| (jobLoader.item && jobLoader.item.menuOpen)
readonly property bool dragging: (thumbnailStripLoader.item && thumbnailStripLoader.item.dragging)
|| (jobLoader.item && jobLoader.item.dragging)
property bool replying: false
signal bodyClicked(var mouse)
signal closeClicked
signal configureClicked
signal dismissClicked
signal actionInvoked(string actionName)
signal replied(string text)
signal openUrl(string url)
signal fileActionInvoked
......@@ -282,13 +291,46 @@ ColumnLayout {
}
}
RowLayout {
Item {
id: actionContainer
Layout.fillWidth: true
Layout.preferredHeight: Math.max(actionFlow.implicitHeight, replyLoader.height)
visible: actionRepeater.count > 0
states: [
State {
when: notificationItem.replying
PropertyChanges {
target: actionFlow
enabled: false
opacity: 0
}
PropertyChanges {
target: replyLoader
active: true
visible: true
opacity: 1
x: 0
}
}
]
transitions: [
Transition {
to: "*" // any state
NumberAnimation {
targets: [actionFlow, replyLoader]
properties: "opacity,scale,x"
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
]
// Notification actions
Flow { // it's a Flow so it can wrap if too long
Layout.fillWidth: true
id: actionFlow
width: parent.width
spacing: units.smallSpacing
layoutDirection: Qt.RightToLeft
......@@ -306,6 +348,14 @@ ColumnLayout {
label: actionLabels[i]
});
}
if (notificationItem.hasReplyAction) {
buttons.unshift({
actionName: "inline-reply",
label: notificationItem.replyActionLabel || i18nc("Reply to message", "Reply")
});
}
return buttons;
}
......@@ -314,10 +364,38 @@ ColumnLayout {
// why does it spit "cannot assign undefined to string" when a notification becomes expired?
text: modelData.label || ""
Layout.preferredWidth: minimumWidth
onClicked: notificationItem.actionInvoked(modelData.actionName)
onClicked: {
if (modelData.actionName === "inline-reply") {
notificationItem.replying = true;
plasmoid.nativeInterface.forceActivateWindow(notificationItem.Window.window);
replyLoader.item.activate();
return;
}
notificationItem.actionInvoked(modelData.actionName);
}
}
}
}
// inline reply field
Loader {
id: replyLoader
width: parent.width
height: active ? item.implicitHeight : 0
active: false
visible: false
opacity: 0
x: parent.width
sourceComponent: NotificationReplyField {
placeholderText: notificationItem.replyPlaceholderText
buttonIconName: notificationItem.replySubmitButtonIconName
buttonText: notificationItem.replySubmitButtonText
onReplied: notificationItem.replied(text)
}
}
}
// thumbnails
......
......@@ -66,12 +66,19 @@ PlasmaCore.Dialog {
property alias actionNames: notificationItem.actionNames
property alias actionLabels: notificationItem.actionLabels
property alias hasReplyAction: notificationItem.hasReplyAction
property alias replyActionLabel: notificationItem.replyActionLabel
property alias replyPlaceholderText: notificationItem.replyPlaceholderText
property alias replySubmitButtonText: notificationItem.replySubmitButtonText
property alias replySubmitButtonIconName: notificationItem.replySubmitButtonIconName
signal configureClicked
signal dismissClicked
signal closeClicked
signal defaultActionInvoked
signal actionInvoked(string actionName)
signal replied(string text)
signal openUrl(string url)
signal fileActionInvoked
......@@ -95,8 +102,7 @@ PlasmaCore.Dialog {
}
location: PlasmaCore.Types.Floating
flags: Qt.WindowDoesNotAcceptFocus
flags: notificationItem.replying ? 0 : Qt.WindowDoesNotAcceptFocus
visible: false
......@@ -136,7 +142,7 @@ PlasmaCore.Dialog {
id: timer
interval: notificationPopup.effectiveTimeout
running: notificationPopup.visible && !area.containsMouse && interval > 0
&& !notificationItem.dragging && !notificationItem.menuOpen
&& !notificationItem.dragging && !notificationItem.menuOpen && !notificationItem.replying
onTriggered: {
if (notificationPopup.dismissTimeout) {
notificationPopup.dismissClicked();
......@@ -182,6 +188,7 @@ PlasmaCore.Dialog {
onDismissClicked: notificationPopup.dismissClicked()
onConfigureClicked: notificationPopup.configureClicked()
onActionInvoked: notificationPopup.actionInvoked(actionName)
onReplied: notificationPopup.replied(text)
onOpenUrl: notificationPopup.openUrl(url)
onFileActionInvoked: notificationPopup.fileActionInvoked()
......
......@@ -416,6 +416,12 @@ QtObject {
actionNames: model.actionNames
actionLabels: model.actionLabels
hasReplyAction: model.hasReplyAction || false
replyActionLabel: model.replyActionLabel || ""
replyPlaceholderText: model.replyPlaceholderText || ""
replySubmitButtonText: model.replySubmitButtonText || ""
replySubmitButtonIconName: model.replySubmitButtonIconName || ""
onExpired: popupNotificationsModel.expire(popupNotificationsModel.index(index, 0))
onHoverEntered: model.read = true
onCloseClicked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
......@@ -429,6 +435,10 @@ QtObject {
popupNotificationsModel.invokeAction(popupNotificationsModel.index(index, 0), actionName)
popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
}
onReplied: {
popupNotificationsModel.reply(popupNotificationsModel.index(index, 0), text);
popupNotificationsModel.close(popupNotificationsModel.index(index, 0));
}
onOpenUrl: {
Qt.openUrlExternally(url);
popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
......
......@@ -9,6 +9,11 @@
<arg name="id" type="u" direction="out"/>
<arg name="action_key" type="s" direction="out"/>
</signal>
<!-- non-standard -->
<signal name="NotificationReplied">
<arg name="id" type="u" direction="out"/>
<arg name="text" type="s" direction="out"/>
</signal>
<method name="Notify">
<annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="QVariantMap"/>
<arg type="u" direction="out"/>
......
......@@ -388,6 +388,10 @@ void Notification::Private::processHints(const QVariantMap &hints)
urls = QUrl::fromStringList(hints.value(QStringLiteral("x-kde-urls")).toStringList());
replyPlaceholderText = hints.value(QStringLiteral("x-kde-reply-placeholder-text")).toString();
replySubmitButtonText = hints.value(QStringLiteral("x-kde-reply-submit-button-text")).toString();
replySubmitButtonIconName = hints.value(QStringLiteral("x-kde-reply-submit-button-icon-name")).toString();
// Underscored hints was in use in version 1.1 of the spec but has been
// replaced by dashed hints in version 1.2. We need to support it for
// users of the 1.2 version of the spec.
......@@ -615,6 +619,7 @@ void Notification::setActions(const QStringList &actions)
d->hasDefaultAction = false;
d->hasConfigureAction = false;
d->hasReplyAction = false;
QStringList names;
QStringList labels;
......@@ -635,6 +640,12 @@ void Notification::setActions(const QStringList &actions)
continue;
}
if (!d->hasReplyAction && name == QLatin1String("inline-reply")) {
d->hasReplyAction = true;
d->replyActionLabel = label;
continue;
}
names << name;
labels << label;
}
......@@ -683,6 +694,31 @@ QString Notification::configureActionLabel() const
return d->configureActionLabel;
}
bool Notification::hasReplyAction() const
{
return d->hasReplyAction;
}
QString Notification::replyActionLabel() const
{
return d->replyActionLabel;
}
QString Notification::replyPlaceholderText() const
{
return d->replyPlaceholderText;
}
QString Notification::replySubmitButtonText() const
{
return d->replySubmitButtonText;
}
QString Notification::replySubmitButtonIconName() const
{
return d->replySubmitButtonIconName;
}
bool Notification::expired() const
{
return d->expired;
......
......@@ -109,6 +109,12 @@ public:
bool configurable() const;
QString configureActionLabel() const;
bool hasReplyAction() const;
QString replyActionLabel() const;
QString replyPlaceholderText() const;
QString replySubmitButtonText() const;
QString replySubmitButtonIconName() const;
bool expired() const;
void setExpired(bool expired);
......
......@@ -88,6 +88,12 @@ public:
QString notifyRcName;
QString eventId;
bool hasReplyAction = false;
QString replyActionLabel;
QString replyPlaceholderText;
QString replySubmitButtonText;
QString replySubmitButtonIconName;
QList<QUrl> urls;
bool userActionFeedback = false;
......
......@@ -726,6 +726,13 @@ void Notifications::invokeAction(const QModelIndex &idx, const QString &actionId
}
}
void Notifications::reply(const QModelIndex &idx, const QString &text)
{
if (d->notificationsModel) {
d->notificationsModel->reply(Private::notificationId(idx), text);
}
}
void Notifications::startTimeout(const QModelIndex &idx)
{
startTimeout(Private::notificationId(idx));
......
......@@ -260,6 +260,12 @@ public:
ReadRole, ///< Whether the notification got read by the user. If true, the notification isn't considered unread even if created after lastRead. @since 5.17
UserActionFeedbackRole, ///< Whether this notification is a response/confirmation to an explicit user action. @since 5.18
HasReplyActionRole, ///< Whether the action has a reply action. @since 5.18
ReplyActionLabelRole, ///< The user-visible label for the reply action. @since 5.18
ReplyPlaceholderTextRole, ///< A custom placeholder text for the reply action, e.g. "Reply to Max...". @since 5.18
ReplySubmitButtonTextRole, ///< A custom text for the reply submit button, e.g. "Submit Comment". @since 5.18
ReplySubmitButtonIconNameRole, ///< A custom icon name for the reply submit button. @since 5.18
};
Q_ENUM(Roles)
......@@ -432,6 +438,14 @@ public:
*/
Q_INVOKABLE void invokeAction(const QModelIndex &idx, const QString &actionId);
/**
* @brief Reply to a notification
*
* Replies to the given notification with the given text.
* @since 5.18
*/
Q_INVOKABLE void reply(const QModelIndex &idx, const QString &text);
/**
* @brief Start automatic timeout of notifications
*
......
......@@ -310,6 +310,12 @@ QVariant NotificationsModel::data(const QModelIndex &index, int role) const
case Notifications::ExpiredRole: return notification.expired();
case Notifications::ReadRole: return notification.read();
case Notifications::HasReplyActionRole: return notification.hasReplyAction();
case Notifications::ReplyActionLabelRole: return notification.replyActionLabel();
case Notifications::ReplyPlaceholderTextRole: return notification.replyPlaceholderText();
case Notifications::ReplySubmitButtonTextRole: return notification.replySubmitButtonText();
case Notifications::ReplySubmitButtonIconNameRole: return notification.replySubmitButtonIconName();
}
return QVariant();
......@@ -439,6 +445,22 @@ void NotificationsModel::invokeAction(uint notificationId, const QString &action
Server::self().invokeAction(notificationId, actionName);
}
void NotificationsModel::reply(uint notificationId, const QString &text)
{
const int row = d->rowOfNotification(notificationId);
if (row == -1) {
return;
}
const Notification &notification = d->notifications.at(row);
if (!notification.hasReplyAction()) {
qCWarning(NOTIFICATIONMANAGER) << "Trying to reply to a notification which doesn't have a reply action";
return;
}
Server::self().reply(notificationId, text);
}
void NotificationsModel::startTimeout(uint notificationId)
{
const int row = d->rowOfNotification(notificationId);
......
......@@ -52,6 +52,7 @@ public:
void configure(const QString &desktopEntry, const QString &notifyRcName, const QString &eventId);
void invokeDefaultAction(uint notificationId);
void invokeAction(uint notificationId, const QString &actionName);
void reply(uint notificationId, const QString &text);
void startTimeout(uint notificationId);
void stopTimeout(uint notificationId);
......
......@@ -80,6 +80,11 @@ void Server::invokeAction(uint notificationId, const QString &actionName)
emit d->ActionInvoked(notificationId, actionName);
}
void Server::reply(uint notificationId, const QString &text)
{
emit d->NotificationReplied(notificationId, text);
}
uint Server::add(const Notification &notification)
{
return d->add(notification);
......
......@@ -148,6 +148,15 @@ public:
*/
void invokeAction(uint id, const QString &actionName);
/**
* Sends a notification reply text
*
* @param id The notification ID
* @param text The reply message text
* @since 5.18
*/
void reply(uint id, const QString &text);
/**
* Adds a notification
*
......
......@@ -231,6 +231,7 @@ QStringList ServerPrivate::GetCapabilities() const
QStringLiteral("body-images"),
QStringLiteral("icon-static"),
QStringLiteral("actions"),
QStringLiteral("inline-reply"),
QStringLiteral("x-kde-urls"),
QStringLiteral("x-kde-origin-name"),
......
......@@ -72,6 +72,8 @@ Q_SIGNALS:
// DBus
void NotificationClosed(uint id, uint reason);
void ActionInvoked(uint id, const QString &actionKey);
// non-standard
void NotificationReplied(uint id, const QString &text);
void validChanged();
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment