Bläddra i källkod

Enable share to Talk and Email. Display correct icon. Added unit tests.

Signed-off-by: allexzander <blackslayer4@gmail.com>
allexzander 4 år sedan
förälder
incheckning
a3fc812539

+ 1 - 1
.drone.yml

@@ -149,4 +149,4 @@ trigger:
     - master
   event:
     - pull_request
-    - push
+    - push

+ 1 - 0
src/gui/CMakeLists.txt

@@ -104,6 +104,7 @@ set(client_SRCS
     elidedlabel.cpp
     headerbanner.cpp
     iconjob.cpp
+    iconutils.cpp
     remotewipe.cpp
     tray/ActivityData.cpp
     tray/ActivityListModel.cpp

+ 92 - 0
src/gui/iconutils.cpp

@@ -0,0 +1,92 @@
+#include "iconutils.h"
+
+#include <theme.h>
+
+#include <QFile>
+#include <QPainter>
+#include <QPixmapCache>
+#include <QSvgRenderer>
+
+namespace OCC {
+namespace Ui {
+namespace IconUtils {
+QPixmap pixmapForBackground(const QString &fileName, const QColor &backgroundColor)
+{
+    Q_ASSERT(!fileName.isEmpty());
+
+    // some icons are present in white or black only, so, we need to check both when needed
+    const auto iconBaseColors = QStringList({ QStringLiteral("black"), QStringLiteral("white") });
+
+    const QString pixmapColor = backgroundColor.isValid() && !Theme::isDarkColor(backgroundColor) ? "black" : "white";
+
+    const QString cacheKey = fileName + QLatin1Char(',') + pixmapColor;
+
+    QPixmap cachedPixmap;
+
+    if (!QPixmapCache::find(cacheKey, &cachedPixmap)) {
+        if (iconBaseColors.contains(pixmapColor)) {
+            cachedPixmap = QPixmap::fromImage(QImage(QString(Theme::themePrefix) + pixmapColor + QLatin1Char('/') + fileName));
+            QPixmapCache::insert(cacheKey, cachedPixmap);
+            return cachedPixmap;
+        }
+
+        const auto drawSvgWithCustomFillColor = [](const QString &sourceSvgPath, const QString &fillColor) {
+            QSvgRenderer svgRenderer;
+
+            if (!svgRenderer.load(sourceSvgPath)) {
+                return QPixmap();
+            }
+
+            // render source image
+            QImage svgImage(svgRenderer.defaultSize(), QImage::Format_ARGB32);
+            {
+                QPainter svgImagePainter(&svgImage);
+                svgImage.fill(Qt::GlobalColor::transparent);
+                svgRenderer.render(&svgImagePainter);
+            }
+
+            // draw target image with custom fillColor
+            QImage image(svgRenderer.defaultSize(), QImage::Format_ARGB32);
+            image.fill(QColor(fillColor));
+            {
+                QPainter imagePainter(&image);
+                imagePainter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
+                imagePainter.drawImage(0, 0, svgImage);
+            }
+
+            return QPixmap::fromImage(image);
+        };
+
+        // find the first matching svg among base colors, if any
+        const QString sourceSvg = [&]() {
+            for (const auto &color : iconBaseColors) {
+                const QString baseSVG(QString(Theme::themePrefix) + color + QLatin1Char('/') + fileName);
+
+                if (QFile(baseSVG).exists()) {
+                    return baseSVG;
+                }
+            }
+            return QString();
+        }();
+
+        Q_ASSERT(!sourceSvg.isEmpty());
+        if (sourceSvg.isEmpty()) {
+            qWarning("Failed to find base svg for %s", qPrintable(cacheKey));
+            return {};
+        }
+
+        cachedPixmap = drawSvgWithCustomFillColor(sourceSvg, pixmapColor);
+        QPixmapCache::insert(cacheKey, cachedPixmap);
+
+        Q_ASSERT(!cachedPixmap.isNull());
+        if (cachedPixmap.isNull()) {
+            qWarning("Failed to load pixmap for %s", qPrintable(cacheKey));
+            return {};
+        }
+    }
+
+    return cachedPixmap;
+}
+}
+}
+}

+ 28 - 0
src/gui/iconutils.h

@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#ifndef ICONUTILS_H
+#define ICONUTILS_H
+
+#include <QColor>
+#include <QPixmap>
+
+namespace OCC {
+namespace Ui {
+namespace IconUtils {
+QPixmap pixmapForBackground(const QString &fileName, const QColor &backgroundColor);
+}
+}
+}
+#endif // ICONUTILS_H

+ 7 - 2
src/gui/sharemanager.cpp

@@ -296,7 +296,7 @@ UserGroupShare::UserGroupShare(AccountPtr account,
     , _note(note)
     , _expireDate(expireDate)
 {
-    Q_ASSERT(shareType == TypeUser || shareType == TypeGroup || shareType == TypeEmail);
+    Q_ASSERT(shareType == TypeUser || shareType == TypeGroup || shareType == TypeEmail || shareType == TypeRoom);
     Q_ASSERT(shareWith);
 }
 
@@ -326,6 +326,11 @@ QDate UserGroupShare::getExpireDate() const
 
 void UserGroupShare::setExpireDate(const QDate &date)
 {
+    if (_expireDate == date) {
+        emit expireDateSet();
+        return;
+    }
+
     auto *job = new OcsShareJob(_account);
     connect(job, &OcsShareJob::shareJobFinished, this, &UserGroupShare::slotExpireDateSet);
     connect(job, &OcsJob::ocsError, this, &UserGroupShare::slotOcsError);
@@ -461,7 +466,7 @@ void ShareManager::slotSharesFetched(const QJsonDocument &reply)
 
         if (shareType == Share::TypeLink) {
             newShare = parseLinkShare(data);
-        } else if (shareType == Share::TypeGroup || shareType == Share::TypeUser || shareType == Share::TypeEmail) {
+        } else if (shareType == Share::TypeGroup || shareType == Share::TypeUser || shareType == Share::TypeEmail || shareType == Share::TypeRoom) {
             newShare = parseUserGroupShare(data);
         } else {
             newShare = parseShare(data);

+ 83 - 29
src/gui/shareusergroupwidget.cpp

@@ -27,6 +27,7 @@
 #include "thumbnailjob.h"
 #include "sharemanager.h"
 #include "theme.h"
+#include "iconutils.h"
 
 #include "QProgressIndicator.h"
 #include <QBuffer>
@@ -46,6 +47,7 @@
 #include <QColor>
 #include <QPainter>
 #include <QListWidget>
+#include <QSvgRenderer>
 
 #include <cstring>
 
@@ -247,7 +249,7 @@ void ShareUserGroupWidget::slotSharesFetched(const QList<QSharedPointer<Share>>
         }
 
 
-        Q_ASSERT(share->getShareType() == Share::TypeUser || share->getShareType() == Share::TypeGroup || share->getShareType() == Share::TypeEmail);
+        Q_ASSERT(share->getShareType() == Share::TypeUser || share->getShareType() == Share::TypeGroup || share->getShareType() == Share::TypeEmail || share->getShareType() == Share::TypeRoom);
         auto userGroupShare = qSharedPointerDynamicCast<UserGroupShare>(share);
         auto *s = new ShareUserLine(_account, userGroupShare, _maxSharingPermissions, _isFile, _parentScrollArea);
         connect(s, &ShareUserLine::resizeRequested, this, &ShareUserGroupWidget::slotAdjustScrollWidgetSize);
@@ -501,7 +503,6 @@ ShareUserLine::ShareUserLine(AccountPtr account,
     _ui->permissionsEdit->setEnabled(enabled);
     connect(_ui->permissionsEdit, &QAbstractButton::clicked, this, &ShareUserLine::slotEditPermissionsChanged);
     connect(_ui->noteConfirmButton, &QAbstractButton::clicked, this, &ShareUserLine::onNoteConfirmButtonClicked);
-    connect(_ui->confirmExpirationDate, &QAbstractButton::clicked, this, &ShareUserLine::setExpireDate);
     connect(_ui->calendar, &QDateTimeEdit::dateChanged, this, &ShareUserLine::setExpireDate);
 
     connect(_share.data(), &UserGroupShare::noteSet, this, &ShareUserLine::disableProgessIndicatorAnimation);
@@ -521,10 +522,9 @@ ShareUserLine::ShareUserLine(AccountPtr account,
 
     showNoteOptions(false);
 
-    // email shares do not support notes and expiration dates
-    const bool isNoteAndExpirationDateSupported = _share->getShareType() != Share::ShareType::TypeEmail;
+    const bool isNoteSupported = _share->getShareType() != Share::ShareType::TypeEmail && _share->getShareType() != Share::ShareType::TypeRoom;
 
-    if (isNoteAndExpirationDateSupported) {
+    if (isNoteSupported) {
         _noteLinkAction = new QAction(tr("Note to recipient"));
         _noteLinkAction->setCheckable(true);
         menu->addAction(_noteLinkAction);
@@ -537,7 +537,9 @@ ShareUserLine::ShareUserLine(AccountPtr account,
 
     showExpireDateOptions(false);
 
-    if (isNoteAndExpirationDateSupported) {
+    const bool isExpirationDateSupported = _share->getShareType() != Share::ShareType::TypeEmail;
+
+    if (isExpirationDateSupported) {
         // email shares do not support expiration dates
         _expirationDateLinkAction = new QAction(tr("Set expiration date"));
         _expirationDateLinkAction->setCheckable(true);
@@ -545,9 +547,8 @@ ShareUserLine::ShareUserLine(AccountPtr account,
         connect(_expirationDateLinkAction, &QAction::triggered, this, &ShareUserLine::toggleExpireDateOptions);
         const auto expireDate = _share->getExpireDate().isValid() ? share.data()->getExpireDate() : QDate();
         if (!expireDate.isNull()) {
-            _ui->calendar->setDate(expireDate);
             _expirationDateLinkAction->setChecked(true);
-            showExpireDateOptions(true);
+            showExpireDateOptions(true, expireDate);
         }
     }
 
@@ -646,15 +647,28 @@ void ShareUserLine::loadAvatar()
     _ui->avatar->setMaximumWidth(avatarSize);
     _ui->avatar->setAlignment(Qt::AlignCenter);
 
+    setDefaultAvatar(avatarSize);
+
+    /* Start the network job to fetch the avatar data.
+     *
+     * Currently only regular users can have avatars.
+     */
+    if (_share->getShareWith()->type() == Sharee::User) {
+        auto *job = new AvatarJob(_share->account(), _share->getShareWith()->shareWith(), avatarSize, this);
+        connect(job, &AvatarJob::avatarPixmap, this, &ShareUserLine::slotAvatarLoaded);
+        job->start();
+    }
+}
+
+void ShareUserLine::setDefaultAvatar(int avatarSize)
+{
     /* Create the fallback avatar.
      *
      * This will be shown until the avatar image data arrives.
      */
-    const QByteArray hash = QCryptographicHash::hash(_ui->sharedWith->text().toUtf8(), QCryptographicHash::Md5);
-    double hue = static_cast<quint8>(hash[0]) / 255.;
 
     // See core/js/placeholder.js for details on colors and styling
-    const QColor bg = QColor::fromHslF(hue, 0.7, 0.68);
+    const auto backgroundColor = backgroundColorForShareeType(_share->getShareWith()->type());
     const QString style = QString(R"(* {
         color: #fff;
         background-color: %1;
@@ -662,21 +676,19 @@ void ShareUserLine::loadAvatar()
         text-align: center;
         line-height: %2px;
         font-size: %2px;
-    })").arg(bg.name(), QString::number(avatarSize / 2));
+    })").arg(backgroundColor.name(), QString::number(avatarSize / 2));
     _ui->avatar->setStyleSheet(style);
 
-    // The avatar label is the first character of the user name.
-    const QString text = _share->getShareWith()->displayName();
-    _ui->avatar->setText(text.at(0).toUpper());
+    const auto pixmap = pixmapForShareeType(_share->getShareWith()->type(), backgroundColor);
 
-    /* Start the network job to fetch the avatar data.
-     *
-     * Currently only regular users can have avatars.
-     */
-    if (_share->getShareWith()->type() == Sharee::User) {
-        auto *job = new AvatarJob(_share->account(), _share->getShareWith()->shareWith(), avatarSize, this);
-        connect(job, &AvatarJob::avatarPixmap, this, &ShareUserLine::slotAvatarLoaded);
-        job->start();
+    if (!pixmap.isNull()) {
+        _ui->avatar->setPixmap(pixmap);
+    } else {
+        qCDebug(lcSharing) << "pixmap is null for share type: " << _share->getShareWith()->type();
+
+        // The avatar label is the first character of the user name.
+        const auto text = _share->getShareWith()->displayName();
+        _ui->avatar->setText(text.at(0).toUpper());
     }
 }
 
@@ -926,13 +938,57 @@ void ShareUserLine::customizeStyle()
     _deleteShareButton->setIcon(deleteicon);
 
     _ui->noteConfirmButton->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg"));
-    _ui->confirmExpirationDate->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg"));
     _ui->progressIndicator->setColor(QGuiApplication::palette().color(QPalette::WindowText));
 
     // make sure to force BackgroundRole to QPalette::WindowText for a lable, because it's parent always has a different role set that applies to children unless customized
     _ui->errorLabel->setBackgroundRole(QPalette::WindowText);
 }
 
+QPixmap ShareUserLine::pixmapForShareeType(Sharee::Type type, const QColor &backgroundColor) const
+{
+    switch (type) {
+    case Sharee::Room:
+        return Ui::IconUtils::pixmapForBackground(QStringLiteral("talk-app.svg"), backgroundColor);
+    case Sharee::Email:
+        return Ui::IconUtils::pixmapForBackground(QStringLiteral("email.svg"), backgroundColor);
+    case Sharee::Group:
+    case Sharee::Federated:
+    case Sharee::Circle:
+    case Sharee::User:
+        break;
+    }
+
+    return {};
+}
+
+QColor ShareUserLine::backgroundColorForShareeType(Sharee::Type type) const
+{
+    switch (type) {
+    case Sharee::Room:
+        return Theme::instance()->wizardHeaderBackgroundColor();
+    case Sharee::Email:
+        return Theme::instance()->wizardHeaderTitleColor();
+    case Sharee::Group:
+    case Sharee::Federated:
+    case Sharee::Circle:
+    case Sharee::User:
+        break;
+    }
+
+    const auto calculateBackgroundBasedOnText = [this]() {
+        const auto hash = QCryptographicHash::hash(_ui->sharedWith->text().toUtf8(), QCryptographicHash::Md5);
+        Q_ASSERT(hash.size() > 0);
+        if (hash.size() == 0) {
+            qCWarning(lcSharing) << "Failed to calculate hash color for share:" << _share->path();
+            return QColor{};
+        }
+        const double hue = static_cast<quint8>(hash[0]) / 255.;
+        return QColor::fromHslF(hue, 0.7, 0.68);
+    };
+
+    return calculateBackgroundBasedOnText();
+}
+
 void ShareUserLine::showNoteOptions(bool show)
 {
     _ui->noteLabel->setVisible(show);
@@ -979,16 +1035,14 @@ void ShareUserLine::toggleExpireDateOptions(bool enable)
     }
 }
 
-void ShareUserLine::showExpireDateOptions(bool show)
+void ShareUserLine::showExpireDateOptions(bool show, const QDate &initialDate)
 {
     _ui->expirationLabel->setVisible(show);
     _ui->calendar->setVisible(show);
-    _ui->confirmExpirationDate->setVisible(show);
 
     if (show) {
-        const QDate date = QDate::currentDate().addDays(1);
-        _ui->calendar->setDate(date);
-        _ui->calendar->setMinimumDate(date);
+        _ui->calendar->setMinimumDate(QDate::currentDate().addDays(1));
+        _ui->calendar->setDate(initialDate.isValid() ? initialDate : _ui->calendar->minimumDate());
         _ui->calendar->setFocus();
     }
 

+ 5 - 1
src/gui/shareusergroupwidget.h

@@ -169,15 +169,19 @@ private slots:
 private:
     void displayPermissions();
     void loadAvatar();
+    void setDefaultAvatar(int avatarSize);
     void customizeStyle();
 
+    QPixmap pixmapForShareeType(Sharee::Type type, const QColor &backgroundColor = QColor()) const;
+    QColor backgroundColorForShareeType(Sharee::Type type) const;
+
   void showNoteOptions(bool show);
   void toggleNoteOptions(bool enable);
   void onNoteConfirmButtonClicked();
   void setNote(const QString &note);
 
   void toggleExpireDateOptions(bool enable);
-  void showExpireDateOptions(bool show);
+  void showExpireDateOptions(bool show, const QDate &initialDate = QDate());
   void setExpireDate();
 
   void togglePasswordSetProgressAnimation(bool show);

+ 0 - 14
src/gui/shareuserline.ui

@@ -273,20 +273,6 @@
        </property>
       </widget>
      </item>
-     <item>
-      <widget class="QToolButton" name="confirmExpirationDate">
-       <property name="text">
-        <string notr="true">…</string>
-       </property>
-       <property name="icon">
-        <iconset resource="../../theme.qrc">
-         <normaloff>:/client/theme/confirm.svg</normaloff>:/client/theme/confirm.svg</iconset>
-       </property>
-       <property name="autoRaise">
-        <bool>true</bool>
-       </property>
-      </widget>
-     </item>
     </layout>
    </item>
    <item>

+ 11 - 13
src/libsync/theme.cpp

@@ -197,7 +197,7 @@ QIcon Theme::themeIcon(const QString &name, bool sysTray) const
             return cached = QIcon::fromTheme(name);
         }
 
-        const auto svgName = QString::fromLatin1(":/client/theme/%1/%2.svg").arg(flavor).arg(name);
+        const QString svgName = QString(Theme::themePrefix) + QString::fromLatin1("%1/%2.svg").arg(flavor).arg(name);
         QSvgRenderer renderer(svgName);
         const auto createPixmapFromSvg = [&renderer] (int size) {
             QImage img(size, size, QImage::Format_ARGB32);
@@ -208,7 +208,7 @@ QIcon Theme::themeIcon(const QString &name, bool sysTray) const
         };
 
         const auto loadPixmap = [flavor, name] (int size) {
-            const auto pixmapName = QString::fromLatin1(":/client/theme/%1/%2-%3.png").arg(flavor).arg(name).arg(size);
+            const QString pixmapName = QString(Theme::themePrefix) + QString::fromLatin1("%1/%2-%3.png").arg(flavor).arg(name).arg(size);
             return QPixmap(pixmapName);
         };
 
@@ -249,8 +249,8 @@ QString Theme::themeImagePath(const QString &name, int size, bool sysTray) const
 
     // branded client may have several sizes of the same icon
     const QString filePath = (useSvg || size <= 0)
-            ? QString::fromLatin1(":/client/theme/%1/%2").arg(flavor).arg(name)
-            : QString::fromLatin1(":/client/theme/%1/%2-%3").arg(flavor).arg(name).arg(size);
+            ? QString(Theme::themePrefix) + QString::fromLatin1("%1/%2").arg(flavor).arg(name)
+            : QString(Theme::themePrefix) + QString::fromLatin1("%1/%2-%3").arg(flavor).arg(name).arg(size);
 
     const QString svgPath = filePath + ".svg";
     if (useSvg) {
@@ -274,8 +274,7 @@ bool Theme::isHidpi(QPaintDevice *dev)
 
 QIcon Theme::uiThemeIcon(const QString &iconName, bool uiHasDarkBg) const
 {
-    QString themeResBasePath = ":/client/theme/";
-    QString iconPath = themeResBasePath + (uiHasDarkBg?"white/":"black/") + iconName;
+    QString iconPath = QString(Theme::themePrefix) + (uiHasDarkBg ? "white/" : "black/") + iconName;
     std::string icnPath = iconPath.toUtf8().constData();
     return QIcon(QPixmap(iconPath));
 }
@@ -303,8 +302,7 @@ QString Theme::hidpiFileName(const QString &iconName, const QColor &backgroundCo
 {
     const auto isDarkBackground = Theme::isDarkColor(backgroundColor);
 
-    const QString themeResBasePath = ":/client/theme/";
-    const QString iconPath = themeResBasePath + (isDarkBackground ? "white/" : "black/") + iconName;
+    const QString iconPath = QString(Theme::themePrefix) + (isDarkBackground ? "white/" : "black/") + iconName;
 
     return Theme::hidpiFileName(iconPath, dev);
 }
@@ -406,7 +404,7 @@ bool Theme::systrayUseMonoIcons() const
 
 bool Theme::monoIconsAvailable() const
 {
-    QString themeDir = QString::fromLatin1(":/client/theme/%1/").arg(Theme::instance()->systrayIconFlavor(true));
+    QString themeDir = QString(Theme::themePrefix) + QString::fromLatin1("%1/").arg(Theme::instance()->systrayIconFlavor(true));
     return QDir(themeDir).exists();
 }
 
@@ -510,7 +508,7 @@ QVariant Theme::customMedia(CustomMediaType type)
         break;
     }
 
-    QString imgPath = QString::fromLatin1(":/client/theme/colored/%1.png").arg(key);
+    QString imgPath = QString(Theme::themePrefix) + QString::fromLatin1("colored/%1.png").arg(key);
     if (QFile::exists(imgPath)) {
         QPixmap pix(imgPath);
         if (pix.isNull()) {
@@ -581,11 +579,11 @@ QColor Theme::wizardHeaderBackgroundColor() const
 QPixmap Theme::wizardApplicationLogo() const
 {
     if (!Theme::isBranded()) {
-        return QPixmap(Theme::hidpiFileName(":/client/theme/colored/wizard-nextcloud.png"));
+        return QPixmap(Theme::hidpiFileName(QString(Theme::themePrefix) + "colored/wizard-nextcloud.png"));
     }
 #ifdef APPLICATION_WIZARD_USE_CUSTOM_LOGO
     const auto useSvg = shouldPreferSvg();
-    const auto logoBasePath = QStringLiteral(":/client/theme/colored/wizard_logo");
+    const QString logoBasePath = QString(Theme::themePrefix) + QStringLiteral("colored/wizard_logo");
     if (useSvg) {
         const auto maxHeight = Theme::isHidpi() ? 200 : 100;
         const auto maxWidth = 2 * maxHeight;
@@ -605,7 +603,7 @@ QPixmap Theme::wizardHeaderLogo() const
 {
 #ifdef APPLICATION_WIZARD_USE_CUSTOM_LOGO
     const auto useSvg = shouldPreferSvg();
-    const auto logoBasePath = QStringLiteral(":/client/theme/colored/wizard_logo");
+    const QString logoBasePath = QString(Theme::themePrefix) + QStringLiteral("colored/wizard_logo");
     if (useSvg) {
         const auto maxHeight = 64;
         const auto maxWidth = 2 * maxHeight;

+ 2 - 0
src/libsync/theme.h

@@ -535,6 +535,8 @@ public:
      */
     virtual bool showVirtualFilesOption() const;
 
+    static constexpr const char *themePrefix = ":/client/theme/";
+
 protected:
 #ifndef TOKEN_AUTH_ONLY
     QIcon themeIcon(const QString &name, bool sysTray = false) const;

+ 1 - 0
test/CMakeLists.txt

@@ -57,6 +57,7 @@ nextcloud_add_test(FolderWatcher)
 nextcloud_add_test(Capabilities)
 nextcloud_add_test(PushNotifications)
 nextcloud_add_test(Theme)
+nextcloud_add_test(IconUtils)
 nextcloud_add_test(NotificationCache)
 
 if( UNIX AND NOT APPLE )

+ 64 - 0
test/testiconutils.cpp

@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include <QTest>
+
+#include "theme.h"
+#include "iconutils.h"
+
+class TestIconUtils : public QObject
+{
+    Q_OBJECT
+
+public:
+    TestIconUtils()
+    {
+        Q_INIT_RESOURCE(resources);
+        Q_INIT_RESOURCE(theme);
+    }
+
+private slots:
+    void testPixmapForBackground()
+    {
+        const QDir blackSvgDir(QString(OCC::Theme::themePrefix) + QStringLiteral("black"));
+        const QStringList blackImages = blackSvgDir.entryList(QStringList("*.svg"));
+
+        const QDir whiteSvgDir(QString(OCC::Theme::themePrefix) + QStringLiteral("white"));
+        const QStringList whiteImages = whiteSvgDir.entryList(QStringList("*.svg"));
+
+        if (blackImages.size() > 0) {
+            // white pixmap for dark background - should not fail
+            QVERIFY(!OCC::Ui::IconUtils::pixmapForBackground(whiteImages.at(0), QColor("blue")).isNull());
+        }
+
+        if (whiteImages.size() > 0) {
+            // black pixmap for bright background - should not fail
+            QVERIFY(!OCC::Ui::IconUtils::pixmapForBackground(blackImages.at(0), QColor("yellow")).isNull());
+        }
+
+        const auto blackImagesExclusive = QSet<QString>(blackImages.begin(), blackImages.end()).subtract(QSet<QString>(whiteImages.begin(), whiteImages.end()));
+        const auto whiteImagesExclusive = QSet<QString>(whiteImages.begin(), whiteImages.end()).subtract(QSet<QString>(blackImages.begin(), blackImages.end()));
+
+        if (blackImagesExclusive != whiteImagesExclusive) {
+            // black pixmap for dark background - should fail as we don't have this image in black
+            QVERIFY(OCC::Ui::IconUtils::pixmapForBackground(blackImagesExclusive.values().at(0), QColor("blue")).isNull());
+
+            // white pixmap for bright background - should fail as we don't have this image in white
+            QVERIFY(OCC::Ui::IconUtils::pixmapForBackground(whiteImagesExclusive.values().at(0), QColor("yellow")).isNull());
+        }
+    }
+};
+
+QTEST_MAIN(TestIconUtils)
+#include "testiconutils.moc"

+ 1 - 0
test/testtheme.cpp

@@ -16,6 +16,7 @@
 
 #include "theme.h"
 #include "themeutils.h"
+#include "iconutils.h"
 
 class TestTheme : public QObject
 {

+ 1 - 0
theme.qrc

@@ -198,5 +198,6 @@
         <file>theme/colored/user-status-invisible.svg</file>
         <file>theme/colored/user-status-away.svg</file>
         <file>theme/colored/user-status-dnd.svg</file>
+        <file>theme/black/email.svg</file>
     </qresource>
 </RCC>

+ 1 - 0
theme.qrc.in

@@ -198,5 +198,6 @@
         <file>theme/colored/user-status-invisible.svg</file>
         <file>theme/colored/user-status-away.svg</file>
         <file>theme/colored/user-status-dnd.svg</file>
+        <file>theme/black/email.svg</file>
     </qresource>
 </RCC>

+ 1 - 0
theme/black/email.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 24 24" width="16px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>