notificator.cpp raw
1 // Copyright (c) 2011-2022 The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #include <limenka-build-config.h> // IWYU pragma: keep
6
7 #include <qt/notificator.h>
8
9 #include <QApplication>
10 #include <QByteArray>
11 #include <QImageWriter>
12 #include <QMessageBox>
13 #include <QMetaType>
14 #include <QStyle>
15 #include <QSystemTrayIcon>
16 #include <QTemporaryFile>
17 #include <QVariant>
18 #ifdef USE_DBUS
19 #include <QDBusMetaType>
20 #include <QtDBus>
21 #include <stdint.h>
22 #endif
23 #ifdef Q_OS_MACOS
24 #include <qt/macnotificationhandler.h>
25 #endif
26
27
28 #ifdef USE_DBUS
29 // https://wiki.ubuntu.com/NotificationDevelopmentGuidelines recommends at least 128
30 const int FREEDESKTOP_NOTIFICATION_ICON_SIZE = 128;
31
32 void DBusInitThread::run() {
33 auto interface = new QDBusInterface("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications");
34 if (!interface->isValid()) {
35 delete interface;
36 return;
37 }
38 interface->moveToThread(m_notificator.thread());
39 m_notificator.interface = interface;
40 m_notificator.mode = Notificator::Freedesktop;
41 }
42
43 #endif
44
45 Notificator::Notificator(const QString &_programName, QSystemTrayIcon *_trayIcon, QWidget *_parent) :
46 QObject(_parent),
47 parent(_parent),
48 programName(_programName),
49 trayIcon(_trayIcon)
50 {
51 if(_trayIcon && _trayIcon->supportsMessages())
52 {
53 mode = QSystemTray;
54 }
55 #ifdef USE_DBUS
56 m_dbus_init_thread = new DBusInitThread(*this);
57 m_dbus_init_thread->start();
58 #endif
59 #ifdef Q_OS_MACOS
60 // check if users OS has support for NSUserNotification
61 if( MacNotificationHandler::instance()->hasUserNotificationCenterSupport()) {
62 mode = UserNotificationCenter;
63 }
64 #endif
65 }
66
67 Notificator::~Notificator()
68 {
69 #ifdef USE_DBUS
70 m_dbus_init_thread->wait();
71 delete m_dbus_init_thread;
72 delete interface;
73 #endif
74 }
75
76 #ifdef USE_DBUS
77
78 // Loosely based on https://www.qtcentre.org/archive/index.php/t-25879.html
79 class FreedesktopImage
80 {
81 public:
82 FreedesktopImage() = default;
83 explicit FreedesktopImage(const QImage &img);
84
85 // Image to variant that can be marshalled over DBus
86 static QVariant toVariant(const QImage &img);
87
88 private:
89 int width, height, stride;
90 bool hasAlpha;
91 int channels;
92 int bitsPerSample;
93 QByteArray image;
94
95 friend QDBusArgument &operator<<(QDBusArgument &a, const FreedesktopImage &i);
96 friend const QDBusArgument &operator>>(const QDBusArgument &a, FreedesktopImage &i);
97 };
98
99 Q_DECLARE_METATYPE(FreedesktopImage);
100
101 // Image configuration settings
102 const int CHANNELS = 4;
103 const int BYTES_PER_PIXEL = 4;
104 const int BITS_PER_SAMPLE = 8;
105
106 FreedesktopImage::FreedesktopImage(const QImage &img):
107 width(img.width()),
108 height(img.height()),
109 stride(img.width() * BYTES_PER_PIXEL),
110 hasAlpha(true),
111 channels(CHANNELS),
112 bitsPerSample(BITS_PER_SAMPLE)
113 {
114 // Convert 00xAARRGGBB to RGBA bytewise (endian-independent) format
115 QImage tmp = img.convertToFormat(QImage::Format_ARGB32);
116 const uint32_t *data = reinterpret_cast<const uint32_t*>(tmp.bits());
117
118 unsigned int num_pixels = width * height;
119 image.resize(num_pixels * BYTES_PER_PIXEL);
120
121 for(unsigned int ptr = 0; ptr < num_pixels; ++ptr)
122 {
123 image[ptr * BYTES_PER_PIXEL + 0] = char(data[ptr] >> 16); // R
124 image[ptr * BYTES_PER_PIXEL + 1] = char(data[ptr] >> 8); // G
125 image[ptr * BYTES_PER_PIXEL + 2] = char(data[ptr]); // B
126 image[ptr * BYTES_PER_PIXEL + 3] = char(data[ptr] >> 24); // A
127 }
128 }
129
130 QDBusArgument &operator<<(QDBusArgument &a, const FreedesktopImage &i)
131 {
132 a.beginStructure();
133 a << i.width << i.height << i.stride << i.hasAlpha << i.bitsPerSample << i.channels << i.image;
134 a.endStructure();
135 return a;
136 }
137
138 const QDBusArgument &operator>>(const QDBusArgument &a, FreedesktopImage &i)
139 {
140 a.beginStructure();
141 a >> i.width >> i.height >> i.stride >> i.hasAlpha >> i.bitsPerSample >> i.channels >> i.image;
142 a.endStructure();
143 return a;
144 }
145
146 QVariant FreedesktopImage::toVariant(const QImage &img)
147 {
148 FreedesktopImage fimg(img);
149 return QVariant(qDBusRegisterMetaType<FreedesktopImage>(), &fimg);
150 }
151
152 void Notificator::notifyDBus(Class cls, const QString &title, const QString &text, const QIcon &icon, int millisTimeout)
153 {
154 // https://developer.gnome.org/notification-spec/
155 // Arguments for DBus "Notify" call:
156 QList<QVariant> args;
157
158 // Program Name:
159 args.append(programName);
160
161 // Replaces ID; A value of 0 means that this notification won't replace any existing notifications:
162 args.append(0U);
163
164 // Application Icon, empty string
165 args.append(QString());
166
167 // Summary
168 args.append(title);
169
170 // Body
171 args.append(text);
172
173 // Actions (none, actions are deprecated)
174 QStringList actions;
175 args.append(actions);
176
177 // Hints
178 QVariantMap hints;
179
180 // If no icon specified, set icon based on class
181 QIcon tmpicon;
182 if(icon.isNull())
183 {
184 QStyle::StandardPixmap sicon = QStyle::SP_MessageBoxQuestion;
185 switch(cls)
186 {
187 case Information: sicon = QStyle::SP_MessageBoxInformation; break;
188 case Warning: sicon = QStyle::SP_MessageBoxWarning; break;
189 case Critical: sicon = QStyle::SP_MessageBoxCritical; break;
190 default: break;
191 }
192 tmpicon = QApplication::style()->standardIcon(sicon);
193 }
194 else
195 {
196 tmpicon = icon;
197 }
198 hints["icon_data"] = FreedesktopImage::toVariant(tmpicon.pixmap(FREEDESKTOP_NOTIFICATION_ICON_SIZE).toImage());
199 args.append(hints);
200
201 // Timeout (in msec)
202 args.append(millisTimeout);
203
204 // "Fire and forget"
205 interface->callWithArgumentList(QDBus::NoBlock, "Notify", args);
206 }
207 #endif
208
209 void Notificator::notifySystray(Class cls, const QString &title, const QString &text, int millisTimeout)
210 {
211 QSystemTrayIcon::MessageIcon sicon = QSystemTrayIcon::NoIcon;
212 switch(cls) // Set icon based on class
213 {
214 case Information: sicon = QSystemTrayIcon::Information; break;
215 case Warning: sicon = QSystemTrayIcon::Warning; break;
216 case Critical: sicon = QSystemTrayIcon::Critical; break;
217 }
218 trayIcon->showMessage(title, text, sicon, millisTimeout);
219 }
220
221 #ifdef Q_OS_MACOS
222 void Notificator::notifyMacUserNotificationCenter(const QString &title, const QString &text)
223 {
224 // icon is not supported by the user notification center yet. OSX will use the app icon.
225 MacNotificationHandler::instance()->showNotification(title, text);
226 }
227 #endif
228
229 void Notificator::notify(Class cls, const QString &title, const QString &text, const QIcon &icon, int millisTimeout)
230 {
231 switch (Mode(mode)) {
232 #ifdef USE_DBUS
233 case Freedesktop:
234 notifyDBus(cls, title, text, icon, millisTimeout);
235 break;
236 #endif
237 case QSystemTray:
238 notifySystray(cls, title, text, millisTimeout);
239 break;
240 #ifdef Q_OS_MACOS
241 case UserNotificationCenter:
242 notifyMacUserNotificationCenter(title, text);
243 break;
244 #endif
245 default:
246 if(cls == Critical)
247 {
248 // Fall back to old fashioned pop-up dialog if critical and no other notification available
249 QMessageBox::critical(parent, title, text, QMessageBox::Ok, QMessageBox::Ok);
250 }
251 break;
252 }
253 }
254