diff --git a/imports/Spectral/Panel/RoomListPanel.qml b/imports/Spectral/Panel/RoomListPanel.qml index 7c71e236c..3fa248bff 100644 --- a/imports/Spectral/Panel/RoomListPanel.qml +++ b/imports/Spectral/Panel/RoomListPanel.qml @@ -34,6 +34,12 @@ Item { onNewMessage: if (!window.active && MSettings.showNotification) notificationsManager.postNotification(roomId, eventId, roomName, senderName, text, icon) } + Binding { + target: trayIcon + property: "notificationCount" + value: roomListModel.notificationCount + } + SortFilterProxyModel { id: sortedRoomListModel diff --git a/qml/main.qml b/qml/main.qml index ff51639a7..70a47744a 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -2,8 +2,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 -import Qt.labs.settings 1.0 -import Qt.labs.platform 1.0 as Platform +import Qt.labs.settings 1.1 import Spectral.Panel 2.0 import Spectral.Component 2.0 @@ -35,20 +34,14 @@ ApplicationWindow { color: MSettings.darkTheme ? "#303030" : "#FFFFFF" } - Platform.SystemTrayIcon { - visible: MSettings.showTray - iconSource: "qrc:/assets/img/icon.png" + TrayIcon { + id: trayIcon - menu: Platform.Menu { - Platform.MenuItem { - text: qsTr("Toggle Window") - onTriggered: window.visible ? hideWindow() : showWindow() - } - Platform.MenuItem { - text: qsTr("Quit") - onTriggered: Qt.quit() - } - } + visible: MSettings.showTray + + iconSource: ":/assets/img/icon.png" + + onShowWindow: window.showWindow() } Controller { diff --git a/spectral.pro b/spectral.pro index 5733de243..94e55502f 100644 --- a/spectral.pro +++ b/spectral.pro @@ -44,7 +44,8 @@ HEADERS += \ include/hoedown/escape.h \ include/hoedown/html.h \ include/hoedown/stack.h \ - include/hoedown/version.h + include/hoedown/version.h \ + src/trayicon.h SOURCES += \ include/hoedown/autolink.c \ @@ -55,7 +56,8 @@ SOURCES += \ include/hoedown/html_blocks.c \ include/hoedown/html_smartypants.c \ include/hoedown/stack.c \ - include/hoedown/version.c + include/hoedown/version.c \ + src/trayicon.cpp # The following define makes your compiler emit warnings if you use # any feature of Qt which as been marked deprecated (the exact warnings diff --git a/src/controller.cpp b/src/controller.cpp index 96f241df8..4f4fd8232 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -307,7 +307,3 @@ int Controller::dpi() { void Controller::setDpi(int dpi) { SettingsGroup("Interface").setValue("dpi", dpi); } - -QString Controller::removeReply(const QString& text) { - return utils::removeReply(text); -} diff --git a/src/controller.h b/src/controller.h index 0d741155c..310f38973 100644 --- a/src/controller.h +++ b/src/controller.h @@ -74,14 +74,12 @@ class Controller : public QObject { QByteArray loadAccessTokenFromKeyChain(const AccountSettings& account); bool saveAccessTokenToFile(const AccountSettings& account, - const QByteArray& accessToken); + const QByteArray& accessToken); bool saveAccessTokenToKeyChain(const AccountSettings& account, - const QByteArray& accessToken); + const QByteArray& accessToken); void loadSettings(); void saveSettings() const; - Q_INVOKABLE QString removeReply(const QString& text); - private slots: void invokeLogin(); @@ -94,6 +92,7 @@ class Controller : public QObject { void initiated(); void notificationClicked(const QString roomId, const QString eventId); void quitOnLastWindowClosedChanged(); + void unreadCountChanged(); void connectionChanged(); public slots: diff --git a/src/main.cpp b/src/main.cpp index f54d83ff7..e75f216fd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,7 @@ #include "roomlistmodel.h" #include "spectralroom.h" #include "spectraluser.h" +#include "trayicon.h" #include "userlistmodel.h" #include "csapi/joining.h" @@ -47,6 +48,7 @@ int main(int argc, char* argv[]) { qmlRegisterType("Spectral", 0, 1, "EmojiModel"); qmlRegisterType("Spectral", 0, 1, "NotificationsManager"); + qmlRegisterType("Spectral", 0, 1, "TrayIcon"); qmlRegisterType("Spectral", 0, 1, "ImageClipboard"); qmlRegisterUncreatableType("Spectral", 0, 1, "RoomMessageEvent", "ENUM"); diff --git a/src/notifications/managerlinux.cpp b/src/notifications/managerlinux.cpp index cd5c7d0a6..af9f8b94c 100644 --- a/src/notifications/managerlinux.cpp +++ b/src/notifications/managerlinux.cpp @@ -26,7 +26,7 @@ NotificationsManager::NotificationsManager(QObject *parent) void NotificationsManager::postNotification( const QString &roomid, const QString &eventid, const QString &roomname, const QString &sender, const QString &text, const QImage &icon) { - uint id = showNotification(roomname, sender + ": " + text, icon); + uint id = showNotification(sender + " (" + roomname + ")", text, icon); notificationIds[id] = roomEventId{roomid, eventid}; } /** diff --git a/src/roomlistmodel.cpp b/src/roomlistmodel.cpp index cee3f607f..5b3bdd9e1 100644 --- a/src/roomlistmodel.cpp +++ b/src/roomlistmodel.cpp @@ -62,6 +62,7 @@ void RoomListModel::doResetModel() { for (auto r : m_connection->roomMap()) doAddRoom(r); endResetModel(); + refreshNotificationCount(); } SpectralRoom* RoomListModel::roomAt(int row) { @@ -104,6 +105,17 @@ void RoomListModel::connectRoomSignals(SpectralRoom* room) { sender->displayname(), room->eventToString(*lastEvent), room->avatar(128)); }); + connect(room, &Room::notificationCountChanged, this, + &RoomListModel::refreshNotificationCount); +} + +void RoomListModel::refreshNotificationCount() { + int count = 0; + for (auto room : m_rooms) { + count += room->notificationCount(); + } + m_notificationCount = count; + emit notificationCountChanged(); } void RoomListModel::updateRoom(Room* room, Room* prev) { diff --git a/src/roomlistmodel.h b/src/roomlistmodel.h index 340bfa4fa..e24609c91 100644 --- a/src/roomlistmodel.h +++ b/src/roomlistmodel.h @@ -27,6 +27,8 @@ class RoomType : public QObject { class RoomListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(Connection* connection READ connection WRITE setConnection) + Q_PROPERTY(int notificationCount READ notificationCount NOTIFY + notificationCountChanged) public: enum EventRoles { @@ -59,19 +61,27 @@ class RoomListModel : public QAbstractListModel { QHash roleNames() const; + int notificationCount() { return m_notificationCount; } + private slots: void doAddRoom(Room* room); void updateRoom(Room* room, Room* prev); void deleteRoom(Room* room); void refresh(SpectralRoom* room, const QVector& roles = {}); + void refreshNotificationCount(); private: Connection* m_connection = nullptr; QList m_rooms; + + int m_notificationCount = 0; + void connectRoomSignals(SpectralRoom* room); signals: void connectionChanged(); + void notificationCountChanged(); + void roomAdded(SpectralRoom* room); void newMessage(const QString& roomId, const QString& eventId, diff --git a/src/trayicon.cpp b/src/trayicon.cpp new file mode 100644 index 000000000..2b2635378 --- /dev/null +++ b/src/trayicon.cpp @@ -0,0 +1,138 @@ +#include "trayicon.h" + +// Modified from mujx/nheko's TrayIcon. + +#include +#include +#include +#include +#include + +#if defined(Q_OS_MAC) +#include +#endif + +MsgCountComposedIcon::MsgCountComposedIcon(const QString& filename) + : QIconEngine() { + icon_ = QIcon(filename); +} + +void MsgCountComposedIcon::paint(QPainter* painter, + const QRect& rect, + QIcon::Mode mode, + QIcon::State state) { + painter->setRenderHint(QPainter::TextAntialiasing); + painter->setRenderHint(QPainter::SmoothPixmapTransform); + painter->setRenderHint(QPainter::Antialiasing); + + icon_.paint(painter, rect, Qt::AlignCenter, mode, state); + + if (msgCount <= 0) + return; + + QColor backgroundColor("red"); + QColor textColor("white"); + + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(backgroundColor); + + painter->setBrush(brush); + painter->setPen(Qt::NoPen); + painter->setFont(QFont("Open Sans", 8, QFont::Black)); + + QRectF bubble(rect.width() - BubbleDiameter, rect.height() - BubbleDiameter, + BubbleDiameter, BubbleDiameter); + painter->drawEllipse(bubble); + painter->setPen(QPen(textColor)); + painter->setBrush(Qt::NoBrush); + if (msgCount < 100) { + painter->drawText(bubble, Qt::AlignCenter, QString::number(msgCount)); + } else { + painter->drawText(bubble, Qt::AlignCenter, "99+"); + } +} + +QIconEngine* MsgCountComposedIcon::clone() const { + return new MsgCountComposedIcon(*this); +} + +QList MsgCountComposedIcon::availableSizes(QIcon::Mode mode, + QIcon::State state) const { + Q_UNUSED(mode) + Q_UNUSED(state) + QList sizes; + sizes.append(QSize(24, 24)); + sizes.append(QSize(32, 32)); + sizes.append(QSize(48, 48)); + sizes.append(QSize(64, 64)); + sizes.append(QSize(128, 128)); + sizes.append(QSize(256, 256)); + return sizes; +} + +QPixmap MsgCountComposedIcon::pixmap(const QSize& size, + QIcon::Mode mode, + QIcon::State state) { + QImage img(size, QImage::Format_ARGB32); + img.fill(qRgba(0, 0, 0, 0)); + QPixmap result = QPixmap::fromImage(img, Qt::NoFormatConversion); + { + QPainter painter(&result); + paint(&painter, QRect(QPoint(0, 0), size), mode, state); + } + return result; +} + +TrayIcon::TrayIcon(QObject* parent) : QSystemTrayIcon(parent) { + QMenu* menu = new QMenu(); + viewAction_ = new QAction(tr("Show"), parent); + quitAction_ = new QAction(tr("Quit"), parent); + + connect(viewAction_, &QAction::triggered, this, &TrayIcon::showWindow); + connect(quitAction_, &QAction::triggered, this, QApplication::quit); + + menu->addAction(viewAction_); + menu->addAction(quitAction_); + + setContextMenu(menu); +} + +void TrayIcon::setNotificationCount(int count) { + m_notificationCount = count; +// Use the native badge counter in MacOS. +#if defined(Q_OS_MAC) + auto labelText = count == 0 ? "" : QString::number(count); + + if (labelText == QtMac::badgeLabelText()) + return; + + QtMac::setBadgeLabelText(labelText); +#elif defined(Q_OS_WIN) +// FIXME: Find a way to use Windows apis for the badge counter (if any). +#else + if (count == icon_->msgCount) + return; + + // Custom drawing on Linux. + MsgCountComposedIcon* tmp = + static_cast(icon_->clone()); + tmp->msgCount = count; + + setIcon(QIcon(tmp)); + + icon_ = tmp; +#endif + emit notificationCountChanged(); +} + +void TrayIcon::setIconSource(const QString& source) { + m_iconSource = source; +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + setIcon(QIcon(source)); +#else + icon_ = new MsgCountComposedIcon(source); + setIcon(QIcon(icon_)); +#endif + emit iconSourceChanged(); +} diff --git a/src/trayicon.h b/src/trayicon.h new file mode 100644 index 000000000..73763b219 --- /dev/null +++ b/src/trayicon.h @@ -0,0 +1,67 @@ +#ifndef TRAYICON_H +#define TRAYICON_H + +// Modified from mujx/nheko's TrayIcon. + +#include +#include +#include +#include +#include +#include + +class MsgCountComposedIcon : public QIconEngine { + public: + MsgCountComposedIcon(const QString& filename); + + virtual void paint(QPainter* p, + const QRect& rect, + QIcon::Mode mode, + QIcon::State state); + virtual QIconEngine* clone() const; + virtual QList availableSizes(QIcon::Mode mode, + QIcon::State state) const; + virtual QPixmap pixmap(const QSize& size, + QIcon::Mode mode, + QIcon::State state); + + int msgCount = 0; + + private: + const int BubbleDiameter = 14; + + QIcon icon_; +}; + +class TrayIcon : public QSystemTrayIcon { + Q_OBJECT + Q_PROPERTY(QString iconSource READ iconSource WRITE setIconSource NOTIFY + iconSourceChanged) + Q_PROPERTY(int notificationCount READ notificationCount WRITE + setNotificationCount NOTIFY notificationCountChanged) + public: + TrayIcon(QObject* parent = nullptr); + + QString iconSource() { return m_iconSource; } + void setIconSource(const QString& source); + + int notificationCount() { return m_notificationCount; } + void setNotificationCount(int count); + + signals: + void notificationCountChanged(); + void iconSourceChanged(); + + void showWindow(); + + private: + QString m_iconSource; + int m_notificationCount = 0; + + QAction* viewAction_; + QAction* quitAction_; + + MsgCountComposedIcon* icon_; +}; + +#endif // TRAYICON_H