Improved Mac experience; QDoubleSpinBox for BitcoinAmountField
[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_WS_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_WS_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         CFRelease(cfurl);
56         if (CFStringCompare(CFBundleGetIdentifier(bundle), CFSTR("com.Growl.GrowlHelperApp"), kCFCompareCaseInsensitive | kCFCompareBackwards) == kCFCompareEqualTo) {
57             mode = Growl;
58         }
59         CFRelease(bundle);
60     }
61 #endif
62 }
63
64 Notificator::~Notificator()
65 {
66 #ifdef USE_DBUS
67     delete interface;
68 #endif
69 }
70
71 #ifdef USE_DBUS
72
73 // Loosely based on http://www.qtcentre.org/archive/index.php/t-25879.html
74 class FreedesktopImage
75 {
76 public:
77     FreedesktopImage() {}
78     FreedesktopImage(const QImage &img);
79
80     static int metaType();
81
82     // Image to variant that can be marshaled over DBus
83     static QVariant toVariant(const QImage &img);
84
85 private:
86     int width, height, stride;
87     bool hasAlpha;
88     int channels;
89     int bitsPerSample;
90     QByteArray image;
91
92     friend QDBusArgument &operator<<(QDBusArgument &a, const FreedesktopImage &i);
93     friend const QDBusArgument &operator>>(const QDBusArgument &a, FreedesktopImage &i);
94 };
95
96 Q_DECLARE_METATYPE(FreedesktopImage);
97
98 // Image configuration settings
99 const int CHANNELS = 4;
100 const int BYTES_PER_PIXEL = 4;
101 const int BITS_PER_SAMPLE = 8;
102
103 FreedesktopImage::FreedesktopImage(const QImage &img):
104     width(img.width()),
105     height(img.height()),
106     stride(img.width() * BYTES_PER_PIXEL),
107     hasAlpha(true),
108     channels(CHANNELS),
109     bitsPerSample(BITS_PER_SAMPLE)
110 {
111     // Convert 00xAARRGGBB to RGBA bytewise (endian-independent) format
112     QImage tmp = img.convertToFormat(QImage::Format_ARGB32);
113     const uint32_t *data = reinterpret_cast<const uint32_t*>(tmp.constBits());
114
115     unsigned int num_pixels = width * height;
116     image.resize(num_pixels * BYTES_PER_PIXEL);
117
118     for(unsigned int ptr = 0; ptr < num_pixels; ++ptr)
119     {
120         image[ptr*BYTES_PER_PIXEL+0] = data[ptr] >> 16; // R
121         image[ptr*BYTES_PER_PIXEL+1] = data[ptr] >> 8;  // G
122         image[ptr*BYTES_PER_PIXEL+2] = data[ptr];       // B
123         image[ptr*BYTES_PER_PIXEL+3] = data[ptr] >> 24; // A
124     }
125 }
126
127 QDBusArgument &operator<<(QDBusArgument &a, const FreedesktopImage &i)
128 {
129     a.beginStructure();
130     a << i.width << i.height << i.stride << i.hasAlpha << i.bitsPerSample << i.channels << i.image;
131     a.endStructure();
132     return a;
133 }
134
135 const QDBusArgument &operator>>(const QDBusArgument &a, FreedesktopImage &i)
136 {
137     a.beginStructure();
138     a >> i.width >> i.height >> i.stride >> i.hasAlpha >> i.bitsPerSample >> i.channels >> i.image;
139     a.endStructure();
140     return a;
141 }
142
143 int FreedesktopImage::metaType()
144 {
145     return qDBusRegisterMetaType<FreedesktopImage>();
146 }
147
148 QVariant FreedesktopImage::toVariant(const QImage &img)
149 {
150     FreedesktopImage fimg(img);
151     return QVariant(FreedesktopImage::metaType(), &fimg);
152 }
153
154 void Notificator::notifyDBus(Class cls, const QString &title, const QString &text, const QIcon &icon, int millisTimeout)
155 {
156     Q_UNUSED(cls);
157     // Arguments for DBus call:
158     QList<QVariant> args;
159
160     // Program Name:
161     args.append(programName);
162
163     // Unique ID of this notification type:
164     args.append(0U);
165
166     // Application Icon, empty string
167     args.append(QString());
168
169     // Summary
170     args.append(title);
171
172     // Body
173     args.append(text);
174
175     // Actions (none, actions are deprecated)
176     QStringList actions;
177     args.append(actions);
178
179     // Hints
180     QVariantMap hints;
181
182     // If no icon specified, set icon based on class
183     QIcon tmpicon;
184     if(icon.isNull())
185     {
186         QStyle::StandardPixmap sicon = QStyle::SP_MessageBoxQuestion;
187         switch(cls)
188         {
189         case Information: sicon = QStyle::SP_MessageBoxInformation; break;
190         case Warning: sicon = QStyle::SP_MessageBoxWarning; break;
191         case Critical: sicon = QStyle::SP_MessageBoxCritical; break;
192         default: break;
193         }
194         tmpicon = QApplication::style()->standardIcon(sicon);
195     }
196     else
197     {
198         tmpicon = icon;
199     }
200     hints["icon_data"] = FreedesktopImage::toVariant(tmpicon.pixmap(FREEDESKTOP_NOTIFICATION_ICON_SIZE).toImage());
201     args.append(hints);
202
203     // Timeout (in msec)
204     args.append(millisTimeout);
205
206     // "Fire and forget"
207     interface->callWithArgumentList(QDBus::NoBlock, "Notify", args);
208 }
209 #endif
210
211 void Notificator::notifySystray(Class cls, const QString &title, const QString &text, const QIcon &icon, int millisTimeout)
212 {
213     Q_UNUSED(icon);
214     QSystemTrayIcon::MessageIcon sicon = QSystemTrayIcon::NoIcon;
215     switch(cls) // Set icon based on class
216     {
217     case Information: sicon = QSystemTrayIcon::Information; break;
218     case Warning: sicon = QSystemTrayIcon::Warning; break;
219     case Critical: sicon = QSystemTrayIcon::Critical; break;
220     }
221     trayIcon->showMessage(title, text, sicon, millisTimeout);
222 }
223
224 // Based on Qt's tray icon implementation
225 #ifdef Q_WS_MAC
226 void Notificator::notifyGrowl(Class cls, const QString &title, const QString &text, const QIcon &icon)
227 {
228     const QString script(
229         "tell application \"GrowlHelperApp\"\n"
230         "  set the allNotificationsList to {\"Notification\"}\n" // -- Make a list of all the notification types (all)
231         "  set the enabledNotificationsList to {\"Notification\"}\n" // -- Make a list of the notifications (enabled)
232         "  register as application \"%1\" all notifications allNotificationsList default notifications enabledNotificationsList\n" // -- Register our script with Growl
233         "  notify with name \"Notification\" title \"%2\" description \"%3\" application name \"%1\"%4\n" // -- Send a Notification
234         "end tell"
235     );
236
237     QString notificationApp(QApplication::applicationName());
238     if (notificationApp.isEmpty())
239         notificationApp = "Application";
240
241     QPixmap notificationIconPixmap;
242     if (icon.isNull()) { // If no icon specified, set icon based on class
243         QStyle::StandardPixmap sicon = QStyle::SP_MessageBoxQuestion;
244         switch (cls)
245         {
246         case Information: sicon = QStyle::SP_MessageBoxInformation; break;
247         case Warning: sicon = QStyle::SP_MessageBoxWarning; break;
248         case Critical: sicon = QStyle::SP_MessageBoxCritical; break;
249         }
250         notificationIconPixmap = QApplication::style()->standardPixmap(sicon);
251     }
252     else {
253         QSize size = icon.actualSize(QSize(48, 48));
254         notificationIconPixmap = icon.pixmap(size);
255     }
256
257     QString notificationIcon;
258     QTemporaryFile notificationIconFile;
259     if (!notificationIconPixmap.isNull() && notificationIconFile.open()) {
260         QImageWriter writer(&notificationIconFile, "PNG");
261         if (writer.write(notificationIconPixmap.toImage()))
262             notificationIcon = QString(" image from location \"file://%1\"").arg(notificationIconFile.fileName());
263     }
264
265     QString quotedTitle(title), quotedText(text);
266     quotedTitle.replace("\\", "\\\\").replace("\"", "\\");
267     quotedText.replace("\\", "\\\\").replace("\"", "\\");
268     qt_mac_execute_apple_script(script.arg(notificationApp, quotedTitle, quotedText, notificationIcon), 0);
269 }
270 #endif
271
272 void Notificator::notify(Class cls, const QString &title, const QString &text, const QIcon &icon, int millisTimeout)
273 {
274     switch(mode)
275     {
276 #ifdef USE_DBUS
277     case Freedesktop:
278         notifyDBus(cls, title, text, icon, millisTimeout);
279         break;
280 #endif
281     case QSystemTray:
282         notifySystray(cls, title, text, icon, millisTimeout);
283         break;
284 #ifdef Q_WS_MAC
285     case Growl:
286         notifyGrowl(cls, title, text, icon);
287         break;
288 #endif
289     default:
290         if(cls == Critical)
291         {
292             // Fall back to old fashioned popup dialog if critical and no other notification available
293             QMessageBox::critical(parent, title, text, QMessageBox::Ok, QMessageBox::Ok);
294         }
295         break;
296     }
297 }