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