- master
event:
- pull_request
- - push
+ - push
\ No newline at end of file
elidedlabel.cpp
headerbanner.cpp
iconjob.cpp
+ iconutils.cpp
remotewipe.cpp
tray/ActivityData.cpp
tray/ActivityListModel.cpp
--- /dev/null
+#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;
+}
+}
+}
+}
--- /dev/null
+/*
+ * 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
, _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);
}
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);
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);
#include "thumbnailjob.h"
#include "sharemanager.h"
#include "theme.h"
+#include "iconutils.h"
#include "QProgressIndicator.h"
#include <QBuffer>
#include <QColor>
#include <QPainter>
#include <QListWidget>
+#include <QSvgRenderer>
#include <cstring>
}
- 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);
_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);
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);
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);
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);
}
}
_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;
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());
}
}
_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);
}
}
-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();
}
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 ¬e);
void toggleExpireDateOptions(bool enable);
- void showExpireDateOptions(bool show);
+ void showExpireDateOptions(bool show, const QDate &initialDate = QDate());
void setExpireDate();
void togglePasswordSetProgressAnimation(bool show);
</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>
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);
};
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);
};
// 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) {
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));
}
{
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);
}
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();
}
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()) {
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;
{
#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;
*/
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;
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 )
--- /dev/null
+/*
+ * 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"
#include "theme.h"
#include "themeutils.h"
+#include "iconutils.h"
class TestTheme : public QObject
{
<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>
<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>
--- /dev/null
+<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>