update to 0.4 preview
[novacoin.git] / src / qt / notificator.cpp
1 #include "notificator.h"
2
3 #include <QMetaType>
4 #include <QVariant>
5 #include <QIcon>
6 #include <QApplication>
7 #include <QStyle>
8 #include <QByteArray>
9 #include <QSystemTrayIcon>
10 #include <QMessageBox>
11 #include <QTemporaryFile>
12 #include <QImageWriter>
13
14 #ifdef USE_DBUS
15 #include <QtDBus/QtDBus>
16 #include <stdint.h>
17 #endif
18
19 #ifdef Q_OS_MAC
20 #include <ApplicationServices/ApplicationServices.h>
21 extern bool qt_mac_execute_apple_script(const QString &script, AEDesc *ret);
22 #endif
23
24 // https://wiki.ubuntu.com/NotificationDevelopmentGuidelines recommends at least 128
25 const int FREEDESKTOP_NOTIFICATION_ICON_SIZE = 128;
26
27 Notificator::Notificator(const QString &programName, QSystemTrayIcon *trayicon, QWidget *parent):
28     QObject(parent),
29     parent(parent),
30     programName(programName),
31     mode(None),
32     trayIcon(trayicon)
33 #ifdef USE_DBUS
34     ,interface(0)
35 #endif
36 {
37     if(trayicon && trayicon->supportsMessages())
38     {
39         mode = QSystemTray;
40     }
41 #ifdef USE_DBUS
42     interface = new QDBusInterface("org.freedesktop.Notifications",
43           "/org/freedesktop/Notifications", "org.freedesktop.Notifications");
44     if(interface->isValid())
45     {
46         mode = Freedesktop;
47     }
48 #endif
49 #ifdef Q_OS_MAC
50     // Check if Growl is installed (based on Qt's tray icon implementation)
51     CFURLRef cfurl;
52     OSStatus status = LSGetApplicationForInfo(kLSUnknownType, kLSUnknownCreator, CFSTR("growlTicket"), kLSRolesAll, 0, &cfurl);
53     if (status != kLSApplicationNotFoundErr) {
54         CFBundleRef bundle = CFBundleCreate(0, cfurl);
55         if (CFStringCompare(CFBundleGetIdentifier(bundle), CFSTR("com.Growl.GrowlHelperApp"), kCFCompareCaseInsensitive | kCFCompareBackwards) == kCFCompareEqualTo) {
56             if (CFStringHasSuffix(CFURLGetString(cfurl), CFSTR("/Growl.app/")))
57                 mode = Growl13;
58             else
59                 mode = Growl12;
60         }
61         CFRelease(cfurl);
62         CFRelease(bundle);
63     }
64 #endif
65 }
66
67 Notificator::~Notificator()
68 {
69 #ifdef USE_DBUS
70     delete interface;
71 #endif
72 }
73
74 #ifdef USE_DBUS
75
76 // Loosely based on http://www.qtcentre.org/archive/index.php/t-25879.html
77 class FreedesktopImage
78 {
79 public:
80     FreedesktopImage() {}
81     FreedesktopImage(const QImage &img);
82
83     static int metaType();
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.constBits());
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] = data[ptr] >> 16; // R
124         image[ptr*BYTES_PER_PIXEL+1] = data[ptr] >> 8;  // G
125         image[ptr*BYTES_PER_PIXEL+2] = data[ptr];       // B
126         image[ptr*BYTES_PER_PIXEL+3] = 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 int FreedesktopImage::metaType()
147 {
148     return qDBusRegisterMetaType<FreedesktopImage>();
149 }
150
151 QVariant FreedesktopImage::toVariant(const QImage &img)
152 {
153     FreedesktopImage fimg(img);
154     return QVariant(FreedesktopImage::metaType(), &fimg);
155 }
156
157 void Notificator::notifyDBus(Class cls, const QString &title, const QString &text, const QIcon &icon, int millisTimeout)
158 {
159     Q_UNUSED(cls);
160     // Arguments for DBus call:
161     QList<QVariant> args;
162
163     // Program Name:
164     args.append(programName);
165
166     // Unique ID of this notification type:
167     args.append(0U);
168
169     // Application Icon, empty string
170     args.append(QString());
171
172     // Summary
173     args.append(title);
174
175     // Body
176     args.append(text);
177
178     // Actions (none, actions are deprecated)
179     QStringList actions;
180     args.append(actions);
181
182     // Hints
183     QVariantMap hints;
184
185     // If no icon specified, set icon based on class
186     QIcon tmpicon;
187     if(icon.isNull())
188     {
189         QStyle::StandardPixmap sicon = QStyle::SP_MessageBoxQuestion;
190         switch(cls)
191         {
192         case Information: sicon = QStyle::SP_MessageBoxInformation; break;
193         case Warning: sicon = QStyle::SP_MessageBoxWarning; break;
194         case Critical: sicon = QStyle::SP_MessageBoxCritical; break;
195         default: break;
196         }
197         tmpicon = QApplication::style()->standardIcon(sicon);
198     }
199     else
200     {
201         tmpicon = icon;
202     }
203     hints["icon_data"] = FreedesktopImage::toVariant(tmpicon.pixmap(FREEDESKTOP_NOTIFICATION_ICON_SIZE).toImage());
204     args.append(hints);
205
206     // Timeout (in msec)
207     args.append(millisTimeout);
208
209     // "Fire and forget"
210     interface->callWithArgumentList(QDBus::NoBlock, "Notify", args);
211 }
212 #endif
213
214 void Notificator::notifySystray(Class cls, const QString &title, const QString &text, const QIcon &icon, int millisTimeout)
215 {
216     Q_UNUSED(icon);
217     QSystemTrayIcon::MessageIcon sicon = QSystemTrayIcon::NoIcon;
218     switch(cls) // Set icon based on class
219     {
220     case Information: sicon = QSystemTrayIcon::Information; break;
221     case Warning: sicon = QSystemTrayIcon::Warning; break;
222     case Critical: sicon = QSystemTrayIcon::Critical; break;
223     }
224     trayIcon->showMessage(title, text, sicon, millisTimeout);
225 }
226
227 // Based on Qt's tray icon implementation
228 #ifdef Q_OS_MAC
229 void Notificator::notifyGrowl(Class cls, const QString &title, const QString &text, const QIcon &icon)
230 {
231     const QString script(
232         "tell application \"%5\"\n"
233         "  set the allNotificationsList to {\"Notification\"}\n" // -- Make a list of all the notification types (all)
234         "  set the enabledNotificationsList to {\"Notification\"}\n" // -- Make a list of the notifications (enabled)
235         "  register as application \"%1\" all notifications allNotificationsList default notifications enabledNotificationsList\n" // -- Register our script with Growl
236         "  notify with name \"Notification\" title \"%2\" description \"%3\" application name \"%1\"%4\n" // -- Send a Notification
237         "end tell"
238     );
239
240     QString notificationApp(QApplication::applicationName());
241     if (notificationApp.isEmpty())
242         notificationApp = "Application";
243
244     QPixmap notificationIconPixmap;
245     if (icon.isNull()) { // If no icon specified, set icon based on class
246         QStyle::StandardPixmap sicon = QStyle::SP_MessageBoxQuestion;
247         switch (cls)
248         {
249         case Information: sicon = QStyle::SP_MessageBoxInformation; break;
250         case Warning: sicon = QStyle::SP_MessageBoxWarning; break;
251         case Critical: sicon = QStyle::SP_MessageBoxCritical; break;
252         }
253         notificationIconPixmap = QApplication::style()->standardPixmap(sicon);
254     }
255     else {
256         QSize size = icon.actualSize(QSize(48, 48));
257         notificationIconPixmap = icon.pixmap(size);
258     }
259
260     QString notificationIcon;
261     QTemporaryFile notificationIconFile;
262     if (!notificationIconPixmap.isNull() && notificationIconFile.open()) {
263         QImageWriter writer(&notificationIconFile, "PNG");
264         if (writer.write(notificationIconPixmap.toImage()))
265             notificationIcon = QString(" image from location \"file://%1\"").arg(notificationIconFile.fileName());
266     }
267
268     QString quotedTitle(title), quotedText(text);
269     quotedTitle.replace("\\", "\\\\").replace("\"", "\\");
270     quotedText.replace("\\", "\\\\").replace("\"", "\\");
271     QString growlApp(this->mode == Notificator::Growl13 ? "Growl" : "GrowlHelperApp");
272     qt_mac_execute_apple_script(script.arg(notificationApp, quotedTitle, quotedText, notificationIcon, growlApp), 0);
273 }
274 #endif
275
276 void Notificator::notify(Class cls, const QString &title, const QString &text, const QIcon &icon, int millisTimeout)
277 {
278     switch(mode)
279     {
280 #ifdef USE_DBUS
281     case Freedesktop:
282         notifyDBus(cls, title, text, icon, millisTimeout);
283         break;
284 #endif
285     case QSystemTray:
286         notifySystray(cls, title, text, icon, millisTimeout);
287         break;
288 #ifdef Q_OS_MAC
289     case Growl12:
290     case Growl13:
291         notifyGrowl(cls, title, text, icon);
292         break;
293 #endif
294     default:
295         if(cls == Critical)
296         {
297             // Fall back to old fashioned pop-up dialog if critical and no other notification available
298             QMessageBox::critical(parent, title, text, QMessageBox::Ok, QMessageBox::Ok);
299         }
300         break;
301     }
302 }