diff --git a/interfaces/notificationsmodel.cpp b/interfaces/notificationsmodel.cpp index 71f6e051..b22a4c4e 100644 --- a/interfaces/notificationsmodel.cpp +++ b/interfaces/notificationsmodel.cpp @@ -1,251 +1,257 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "notificationsmodel.h" #include "interfaces_debug.h" #include #include #include #include //#include "modeltest.h" NotificationsModel::NotificationsModel(QObject* parent) : QAbstractListModel(parent) , m_dbusInterface(nullptr) { //new ModelTest(this, this); connect(this, &QAbstractItemModel::rowsInserted, this, &NotificationsModel::rowsChanged); connect(this, &QAbstractItemModel::rowsRemoved, this, &NotificationsModel::rowsChanged); connect(this, &QAbstractItemModel::dataChanged, this, &NotificationsModel::anyDismissableChanged); connect(this, &QAbstractItemModel::rowsInserted, this, &NotificationsModel::anyDismissableChanged); QDBusServiceWatcher* watcher = new QDBusServiceWatcher(DaemonDbusInterface::activatedService(), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this); connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &NotificationsModel::refreshNotificationList); connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, &NotificationsModel::clearNotifications); } QHash NotificationsModel::roleNames() const { //Role names for QML QHash names = QAbstractItemModel::roleNames(); names.insert(DbusInterfaceRole, "dbusInterface"); names.insert(AppNameModelRole, "appName"); names.insert(IdModelRole, "notificationId"); names.insert(DismissableModelRole, "dismissable"); + names.insert(RepliableModelRole, "repliable"); + names.insert(IconPathModelRole, "appIcon"); return names; } NotificationsModel::~NotificationsModel() { } QString NotificationsModel::deviceId() const { return m_deviceId; } void NotificationsModel::setDeviceId(const QString& deviceId) { m_deviceId = deviceId; if (m_dbusInterface) { delete m_dbusInterface; } m_dbusInterface = new DeviceNotificationsDbusInterface(deviceId, this); connect(m_dbusInterface, &OrgKdeKdeconnectDeviceNotificationsInterface::notificationPosted, this, &NotificationsModel::notificationAdded); connect(m_dbusInterface, &OrgKdeKdeconnectDeviceNotificationsInterface::notificationRemoved, this, &NotificationsModel::notificationRemoved); connect(m_dbusInterface, &OrgKdeKdeconnectDeviceNotificationsInterface::allNotificationsRemoved, this, &NotificationsModel::clearNotifications); refreshNotificationList(); Q_EMIT deviceIdChanged(deviceId); } void NotificationsModel::notificationAdded(const QString& id) { int currentSize = m_notificationList.size(); beginInsertRows(QModelIndex(), currentSize, currentSize); NotificationDbusInterface* dbusInterface = new NotificationDbusInterface(m_deviceId, id, this); m_notificationList.append(dbusInterface); endInsertRows(); } void NotificationsModel::notificationRemoved(const QString& id) { for (int i = 0; i < m_notificationList.size(); ++i) { if (m_notificationList[i]->notificationId() == id) { beginRemoveRows(QModelIndex(), i, i); m_notificationList.removeAt(i); endRemoveRows(); return; } } qCWarning(KDECONNECT_INTERFACES) << "Attempted to remove unknown notification: " << id; } void NotificationsModel::refreshNotificationList() { if (!m_dbusInterface) { return; } clearNotifications(); if (!m_dbusInterface->isValid()) { qCWarning(KDECONNECT_INTERFACES) << "dbus interface not valid"; return; } QDBusPendingReply pendingNotificationIds = m_dbusInterface->activeNotifications(); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingNotificationIds, this); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &NotificationsModel::receivedNotifications); } void NotificationsModel::receivedNotifications(QDBusPendingCallWatcher* watcher) { watcher->deleteLater(); clearNotifications(); QDBusPendingReply pendingNotificationIds = *watcher; if (pendingNotificationIds.isError()) { qCWarning(KDECONNECT_INTERFACES) << pendingNotificationIds.error(); return; } const QStringList notificationIds = pendingNotificationIds.value(); if (notificationIds.isEmpty()) { return; } beginInsertRows(QModelIndex(), 0, notificationIds.size() - 1); Q_FOREACH (const QString& notificationId, notificationIds) { NotificationDbusInterface* dbusInterface = new NotificationDbusInterface(m_deviceId, notificationId, this); m_notificationList.append(dbusInterface); } endInsertRows(); } QVariant NotificationsModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= m_notificationList.count() || !m_notificationList[index.row()]->isValid()) { return QVariant(); } if (!m_dbusInterface || !m_dbusInterface->isValid()) { return QVariant(); } NotificationDbusInterface* notification = m_notificationList[index.row()]; //FIXME: This function gets called lots of times, producing lots of dbus calls. Add a cache? switch (role) { case IconModelRole: return QIcon::fromTheme(QStringLiteral("device-notifier")); case IdModelRole: return notification->internalId(); case NameModelRole: return notification->ticker(); case ContentModelRole: return QString(); //To implement in the Android side case AppNameModelRole: return notification->appName(); case DbusInterfaceRole: return qVariantFromValue(notification); case DismissableModelRole: return notification->dismissable(); + case RepliableModelRole: + return !notification->replyId().isEmpty(); + case IconPathModelRole: + return notification->iconPath(); default: return QVariant(); } } NotificationDbusInterface* NotificationsModel::getNotification(const QModelIndex& index) const { if (!index.isValid()) { return nullptr; } int row = index.row(); if (row < 0 || row >= m_notificationList.size()) { return nullptr; } return m_notificationList[row]; } int NotificationsModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { //Return size 0 if we are a child because this is not a tree return 0; } return m_notificationList.count(); } bool NotificationsModel::isAnyDimissable() const { Q_FOREACH (NotificationDbusInterface* notification, m_notificationList) { if (notification->dismissable()) { return true; } } return false; } void NotificationsModel::dismissAll() { Q_FOREACH (NotificationDbusInterface* notification, m_notificationList) { if (notification->dismissable()) { notification->dismiss(); } } } void NotificationsModel::clearNotifications() { if (!m_notificationList.isEmpty()) { beginRemoveRows(QModelIndex(), 0, m_notificationList.size() - 1); qDeleteAll(m_notificationList); m_notificationList.clear(); endRemoveRows(); } } diff --git a/interfaces/notificationsmodel.h b/interfaces/notificationsmodel.h index b3b9005a..09235670 100644 --- a/interfaces/notificationsmodel.h +++ b/interfaces/notificationsmodel.h @@ -1,85 +1,87 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef NOTIFICATIONSMODEL_H #define NOTIFICATIONSMODEL_H #include #include #include #include #include "interfaces/dbusinterfaces.h" class KDECONNECTINTERFACES_EXPORT NotificationsModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged) Q_PROPERTY(int count READ rowCount NOTIFY rowsChanged) Q_PROPERTY(bool isAnyDimissable READ isAnyDimissable NOTIFY anyDismissableChanged STORED false) public: enum ModelRoles { IconModelRole = Qt::DecorationRole, NameModelRole = Qt::DisplayRole, ContentModelRole = Qt::UserRole, AppNameModelRole = Qt::UserRole + 1, IdModelRole, DismissableModelRole, - DbusInterfaceRole, + RepliableModelRole, + IconPathModelRole, + DbusInterfaceRole }; explicit NotificationsModel(QObject* parent = nullptr); ~NotificationsModel() override; QString deviceId() const; void setDeviceId(const QString& deviceId); QVariant data(const QModelIndex& index, int role) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; NotificationDbusInterface* getNotification(const QModelIndex& index) const; Q_INVOKABLE bool isAnyDimissable() const; QHash roleNames() const override; public Q_SLOTS: void dismissAll(); private Q_SLOTS: void notificationAdded(const QString& id); void notificationRemoved(const QString& id); void refreshNotificationList(); void receivedNotifications(QDBusPendingCallWatcher* watcher); void clearNotifications(); Q_SIGNALS: void deviceIdChanged(const QString& value); void anyDismissableChanged(); void rowsChanged(); private: DeviceNotificationsDbusInterface* m_dbusInterface; QList m_notificationList; QString m_deviceId; }; #endif // DEVICESMODEL_H diff --git a/plasmoid/package/contents/ui/DeviceDelegate.qml b/plasmoid/package/contents/ui/DeviceDelegate.qml index cd01ad25..a42e5bb7 100644 --- a/plasmoid/package/contents/ui/DeviceDelegate.qml +++ b/plasmoid/package/contents/ui/DeviceDelegate.qml @@ -1,263 +1,278 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.1 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.kdeconnect 1.0 import QtQuick.Controls.Styles 1.4 PlasmaComponents.ListItem { id: root readonly property QtObject device: DeviceDbusInterfaceFactory.create(model.deviceId) RemoteKeyboard { id: remoteKeyboard device: root.device onRemoteStateChanged: { remoteKeyboardInput.available = remoteKeyboard.remoteState; } onKeyPressReceived: { // console.log("XXX received keypress key=" + key + " special=" + specialKey + " shift=" + shift + " ctrl=" + ctrl + " text=" + remoteKeyboardInput.text + " cursorPos=" + remoteKeyboardInput.cursorPosition); // interpret some special keys: if (specialKey == 12 || specialKey == 14) // Return/Esc -> clear remoteKeyboardInput.text = ""; else if (specialKey == 4 // Left && remoteKeyboardInput.cursorPosition > 0) --remoteKeyboardInput.cursorPosition; else if (specialKey == 6 // Right && remoteKeyboardInput.cursorPosition < remoteKeyboardInput.text.length) ++remoteKeyboardInput.cursorPosition; else if (specialKey == 1) { // Backspace -> delete left var pos = remoteKeyboardInput.cursorPosition; if (pos > 0) { remoteKeyboardInput.text = remoteKeyboardInput.text.substring(0, pos-1) + remoteKeyboardInput.text.substring(pos, remoteKeyboardInput.text.length); remoteKeyboardInput.cursorPosition = pos - 1; } } else if (specialKey == 13) { // Delete -> delete right var pos = remoteKeyboardInput.cursorPosition; if (pos < remoteKeyboardInput.text.length) { remoteKeyboardInput.text = remoteKeyboardInput.text.substring(0, pos) + remoteKeyboardInput.text.substring(pos+1, remoteKeyboardInput.text.length); remoteKeyboardInput.cursorPosition = pos; // seems to be set to text.length automatically! } } else if (specialKey == 10) // Home remoteKeyboardInput.cursorPosition = 0; else if (specialKey == 11) // End remoteKeyboardInput.cursorPosition = remoteKeyboardInput.text.length; else { // echo visible keys var sanitized = ""; for (var i = 0; i < key.length; i++) { if (key.charCodeAt(i) > 31) sanitized += key.charAt(i); } if (sanitized.length > 0 && !ctrl && !alt) { // insert sanitized at current pos: var pos = remoteKeyboardInput.cursorPosition; remoteKeyboardInput.text = remoteKeyboardInput.text.substring(0, pos) + sanitized + remoteKeyboardInput.text.substring(pos, remoteKeyboardInput.text.length); remoteKeyboardInput.cursorPosition = pos + 1; // seems to be set to text.length automatically! } } // console.log("XXX After received keypress key=" + key + " special=" + specialKey + " shift=" + shift + " ctrl=" + ctrl + " text=" + remoteKeyboardInput.text + " cursorPos=" + remoteKeyboardInput.cursorPosition); } } Column { width: parent.width RowLayout { Item { //spacer to make the label centre aligned in a row yet still elide and everything implicitWidth: ring.width + browse.width + parent.spacing } PlasmaComponents.Label { horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight text: display Layout.fillWidth: true } //Find my phone PlasmaComponents.Button { FindMyPhone { id: findmyphone device: root.device } id: ring iconSource: "irc-voice" visible: findmyphone.available tooltip: i18n("Ring my phone") onClicked: { findmyphone.ring() } } //SFTP PlasmaComponents.Button { Sftp { id: sftp device: root.device } id: browse iconSource: "document-open-folder" visible: sftp.available tooltip: i18n("Browse this device") onClicked: { sftp.browse() } } height: browse.height width: parent.width } //RemoteKeyboard PlasmaComponents.ListItem { sectionDelegate: true visible: remoteKeyboard.available width: parent.width Row { width: parent.width spacing: 5 PlasmaComponents.Label { id: remoteKeyboardLabel //font.bold: true text: i18n("Remote Keyboard") } PlasmaComponents.TextField { id: remoteKeyboardInput property bool available: remoteKeyboard.remoteState textColor: "black" height: parent.height width: parent.width - 5 - remoteKeyboardLabel.width verticalAlignment: TextInput.AlignVCenter readOnly: !available enabled: available style: TextFieldStyle { textColor: "black" background: Rectangle { radius: 2 border.color: "gray" border.width: 1 color: remoteKeyboardInput.available ? "white" : "lightgray" } } Keys.onPressed: { if (remoteKeyboard.available) remoteKeyboard.sendEvent(event); event.accepted = true; } } } } //Battery PlasmaComponents.ListItem { Battery { id: battery device: root.device } sectionDelegate: true visible: battery.available PlasmaComponents.Label { //font.bold: true text: i18n("Battery") } PlasmaComponents.Label { text: battery.displayString anchors.right: parent.right } } //Notifications PlasmaComponents.ListItem { visible: notificationsModel.count>0 enabled: true sectionDelegate: true PlasmaComponents.Label { //font.bold: true text: i18n("Notifications") } PlasmaComponents.ToolButton { enabled: true visible: notificationsModel.isAnyDimissable; anchors.right: parent.right iconSource: "window-close" onClicked: notificationsModel.dismissAll(); } } Repeater { id: notificationsView model: NotificationsModel { id: notificationsModel deviceId: root.device.id() } delegate: PlasmaComponents.ListItem { id: listitem enabled: true onClicked: checked = !checked + PlasmaCore.IconItem { + id: notificationIcon + source: appIcon + width: (valid && appIcon.length) ? dismissButton.width : 0 + height: width + anchors.left: parent.left + } PlasmaComponents.Label { text: appName + ": " + display - anchors.right: dismissButton.left - anchors.left: parent.left + anchors.right: replyButton.left + anchors.left: notificationIcon.right elide: listitem.checked ? Text.ElideNone : Text.ElideRight maximumLineCount: listitem.checked ? 0 : 1 wrapMode: Text.WordWrap } + PlasmaComponents.ToolButton { + id: replyButton + visible: repliable + enabled: repliable + anchors.right: dismissButton.left + iconSource: "mail-reply-sender" + onClicked: dbusInterface.reply(); + } PlasmaComponents.ToolButton { id: dismissButton visible: notificationsModel.isAnyDimissable; enabled: dismissable anchors.right: parent.right iconSource: "window-close" onClicked: dbusInterface.dismiss(); } } } //NOTE: More information could be displayed here } } diff --git a/plugins/notifications/notification.cpp b/plugins/notifications/notification.cpp index df1b6df6..6d0483dd 100644 --- a/plugins/notifications/notification.cpp +++ b/plugins/notifications/notification.cpp @@ -1,149 +1,154 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "notification.h" #include #include #include #include #include #include #include Notification::Notification(const NetworkPackage& np, QObject* parent) : QObject(parent) { mImagesDir = QDir::temp().absoluteFilePath(QStringLiteral("kdeconnect")); mImagesDir.mkpath(mImagesDir.absolutePath()); mClosed = false; parseNetworkPackage(np); createKNotification(false, np); } Notification::~Notification() { } void Notification::dismiss() { if (mDismissable) { Q_EMIT dismissRequested(mInternalId); } } void Notification::show() { if (!mSilent) { mClosed = false; mNotification->sendEvent(); } } void Notification::applyIconAndShow() { if (!mSilent) { QPixmap icon(mIconPath, "PNG"); mNotification->setPixmap(icon); show(); } } void Notification::update(const NetworkPackage &np) { parseNetworkPackage(np); createKNotification(!mClosed, np); } KNotification* Notification::createKNotification(bool update, const NetworkPackage &np) { if (!update) { mNotification = new KNotification(QStringLiteral("notification"), KNotification::CloseOnTimeout, this); mNotification->setComponentName(QStringLiteral("kdeconnect")); } QString escapedTitle = mTitle.toHtmlEscaped(); QString escapedText = mText.toHtmlEscaped(); QString escapedTicker = mTicker.toHtmlEscaped(); mNotification->setTitle(mAppName.toHtmlEscaped()); if (mTitle.isEmpty() && mText.isEmpty()) { mNotification->setText(escapedTicker); } else if (mAppName==mTitle) { mNotification->setText(escapedText); } else if (mTitle.isEmpty()){ mNotification->setText(escapedText); } else if (mText.isEmpty()){ mNotification->setText(escapedTitle); } else { mNotification->setText(escapedTitle+": "+escapedText); } if (!mHasIcon) { mNotification->setIconName(QStringLiteral("preferences-desktop-notification")); show(); } else { QString filename = mPayloadHash; if (filename.isEmpty()) { mHasIcon = false; } else { mIconPath = mImagesDir.absoluteFilePath(filename); QUrl destinationUrl(mIconPath); FileTransferJob* job = np.createPayloadTransferJob(destinationUrl); job->start(); connect(job, &FileTransferJob::result, this, &Notification::applyIconAndShow); } } if(!mRequestReplyId.isEmpty()) { mNotification->setActions( QStringList(i18n("Reply")) ); - connect(mNotification, &KNotification::action1Activated, this, &Notification::replyRequested); + connect(mNotification, &KNotification::action1Activated, this, &Notification::reply); } connect(mNotification, &KNotification::closed, this, &Notification::closed); return mNotification; } +void Notification::reply() +{ + Q_EMIT replyRequested(); +} + void Notification::closed() { mClosed = true; } void Notification::parseNetworkPackage(const NetworkPackage &np) { mInternalId = np.get(QStringLiteral("id")); mAppName = np.get(QStringLiteral("appName")); mTicker = np.get(QStringLiteral("ticker")); mTitle = np.get(QStringLiteral("title")); mText = np.get(QStringLiteral("text")); mDismissable = np.get(QStringLiteral("isClearable")); mHasIcon = np.hasPayload(); mSilent = np.get(QStringLiteral("silent")); mPayloadHash = np.get(QStringLiteral("payloadHash")); mRequestReplyId = np.get(QStringLiteral("requestReplyId"), QString()); } diff --git a/plugins/notifications/notification.h b/plugins/notifications/notification.h index 967f4630..420347bb 100644 --- a/plugins/notifications/notification.h +++ b/plugins/notifications/notification.h @@ -1,93 +1,94 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef NOTIFICATION_H #define NOTIFICATION_H #include #include #include #include #include class Notification : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.notifications.notification") Q_PROPERTY(QString internalId READ internalId) Q_PROPERTY(QString appName READ appName) Q_PROPERTY(QString ticker READ ticker) Q_PROPERTY(QString title READ title) Q_PROPERTY(QString text READ text) Q_PROPERTY(QString iconPath READ iconPath) Q_PROPERTY(bool dismissable READ dismissable) Q_PROPERTY(bool hasIcon READ hasIcon) Q_PROPERTY(bool silent READ silent) Q_PROPERTY(QString replyId READ replyId) public: Notification(const NetworkPackage& np, QObject* parent); ~Notification() override; QString internalId() const { return mInternalId; } QString appName() const { return mAppName; } QString ticker() const { return mTicker; } QString title() const { return mTitle; } QString text() const { return mText; } QString iconPath() const { return mIconPath; } bool dismissable() const { return mDismissable; } QString replyId() const { return mRequestReplyId; } bool hasIcon() const { return mHasIcon; } void show(); bool silent() const { return mSilent; } void update(const NetworkPackage &np); KNotification* createKNotification(bool update, const NetworkPackage &np); public Q_SLOTS: Q_SCRIPTABLE void dismiss(); Q_SCRIPTABLE void applyIconAndShow(); + Q_SCRIPTABLE void reply(); void closed(); Q_SIGNALS: void dismissRequested(const QString& mInternalId); void replyRequested(); private: QString mInternalId; QString mAppName; QString mTicker; QString mTitle; QString mText; QString mIconPath; QString mRequestReplyId; bool mDismissable; bool mHasIcon; KNotification* mNotification; QDir mImagesDir; bool mSilent; bool mClosed; QString mPayloadHash; void parseNetworkPackage(const NetworkPackage& np); }; #endif