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