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