Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions panels/notification/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ target_include_directories(ds-notification-shared PUBLIC
)
target_link_libraries(ds-notification-shared PUBLIC
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Sql
Dtk${DTK_VERSION_MAJOR}::Core
ICU::uc
Expand Down
143 changes: 1 addition & 142 deletions panels/notification/bubble/bubbleitem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,138 +21,6 @@ Q_DECLARE_LOGGING_CATEGORY(notifyLog)

namespace notification {

static inline void copyLineRGB32(QRgb *dst, const char *src, int width)
{
const char *end = src + width * 3;
for (; src != end; ++dst, src += 3) {
*dst = qRgb(src[0], src[1], src[2]);
}
}

static inline void copyLineARGB32(QRgb *dst, const char *src, int width)
{
const char *end = src + width * 4;
for (; src != end; ++dst, src += 4) {
*dst = qRgba(src[0], src[1], src[2], src[3]);
}
}

static QImage decodeImageFromDBusArgument(const QDBusArgument &arg)
{
int width, height, rowStride, hasAlpha, bitsPerSample, channels;
QByteArray pixels;
char *ptr;
char *end;

arg.beginStructure();
arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels;
arg.endStructure();
//qDebug(notifyLog) << width << height << rowStride << hasAlpha << bitsPerSample << channels;

#define SANITY_CHECK(condition) \
if (!(condition)) { \
qWarning(notifyLog) << "Sanity check failed on" << #condition; \
return QImage(); \
}

SANITY_CHECK(width > 0);
SANITY_CHECK(width < 2048);
SANITY_CHECK(height > 0);
SANITY_CHECK(height < 2048);
SANITY_CHECK(rowStride > 0);

#undef SANITY_CHECK

QImage::Format format = QImage::Format_Invalid;
void (*fcn)(QRgb *, const char *, int) = nullptr;
if (bitsPerSample == 8) {
if (channels == 4) {
format = QImage::Format_ARGB32;
fcn = copyLineARGB32;
} else if (channels == 3) {
format = QImage::Format_RGB32;
fcn = copyLineRGB32;
}
}
if (format == QImage::Format_Invalid) {
qWarning(notifyLog) << "Unsupported image format (hasAlpha:" << hasAlpha << "bitsPerSample:" << bitsPerSample << "channels:" << channels << ")";
return QImage();
}

QImage image(width, height, format);
ptr = pixels.data();
end = ptr + pixels.length();
for (int y = 0; y < height; ++y, ptr += rowStride) {
if (ptr + channels * width > end) {
qWarning(notifyLog) << "Image data is incomplete. y:" << y << "height:" << height;
break;
}
fcn((QRgb *)image.scanLine(y), ptr, width);
}

return image;
}

static QString decodeImageToBase64(const QImage &image, const char *format = "PNG")
{
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, format);

return QString("data:image/%1;base64,%2").arg(QString::fromLatin1(format).toLower()).arg(QString::fromLatin1(ba.toBase64()));
}

[[maybe_unused]] static QIcon decodeIconFromPath(const QString &arg, const QString &fallback)
{
DGUI_USE_NAMESPACE;
const QUrl url(arg);
const auto iconUrl = url.isLocalFile() ? url.toLocalFile() : url.url();
QIcon icon = DIconTheme::findQIcon(iconUrl);
if (!icon.isNull()) {
return icon;
}
return DIconTheme::findQIcon(fallback, DIconTheme::findQIcon("application-x-desktop"));
}

static QString imagePathOfNotification(const QVariantMap &hints, const QString &appIcon, const QString &appName)
{
Q_UNUSED(appName)
static const QStringList HintsOrder {
"desktop-entry",
"image-data",
"icon_data"
};

QImage img;
QString imageData(appIcon);
for (const auto &hint : HintsOrder) {
const auto &source = hints[hint];
if (source.isNull())
continue;
if (source.canConvert<QDBusArgument>()) {
img = decodeImageFromDBusArgument(source.value<QDBusArgument>());
if (!img.isNull())
break;
}
imageData = source.toString();
}
if (img.isNull()) {
// check if imageData is a base64 image data.
QRegularExpression dataUriPattern("^data:image/[a-zA-Z0-9+\\-]+;base64,");
QRegularExpressionMatch match = dataUriPattern.match(imageData);
if (match.hasMatch()) {
return imageData;
}
} else {
return decodeImageToBase64(img);
}

// ui can fallback to application-x-desktop icon.
return {};
}


BubbleItem::BubbleItem(QObject *parent)
: QObject(parent)
, m_timeTip(tr("just now"))
Expand Down Expand Up @@ -195,16 +63,7 @@ QString BubbleItem::appName() const

QString BubbleItem::appIcon() const
{
// image-data / icon_data hints carry the notification-specific image
const QString iconFromHints = imagePathOfNotification(m_entity.hints(), m_entity.appIcon(), m_entity.appName());
if (!iconFromHints.isEmpty())
return iconFromHints;

if (!m_entity.appIcon().isEmpty()) {
return m_entity.appIcon();
}

return {};
return m_entity.appIconResolved();
}

QString BubbleItem::summary() const
Expand Down
2 changes: 1 addition & 1 deletion panels/notification/center/notifymodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ QVariant NotifyModel::data(const QModelIndex &index, int role) const
} else if (role == NotifyRole::NotifyAppName) {
return notify->appName();
} else if (role == NotifyRole::NotifyIconName) {
return notify->entity().appIcon();
return notify->entity().appIconResolved();
} else if (role == NotifyRole::NotifyTitle) {
return notify->entity().summary();
} else if (role == NotifyRole::NotifyContent) {
Expand Down
4 changes: 2 additions & 2 deletions panels/notification/center/notifystagingmodel.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2024-2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

Expand Down Expand Up @@ -197,7 +197,7 @@ QVariant NotifyStagingModel::data(const QModelIndex &index, int role) const
} else if (role == NotifyRole::NotifyAppName) {
return notify->appName();
} else if (role == NotifyRole::NotifyIconName) {
return notify->entity().appIcon();
return notify->entity().appIconResolved();
} else if (role == NotifyRole::NotifyTitle) {
return notify->entity().summary();
} else if (role == NotifyRole::NotifyContent) {
Expand Down
134 changes: 134 additions & 0 deletions panels/notification/common/notifyentity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
#include "notifyentity.h"

#include <QDateTime>
#include <QLocale>

Check warning on line 8 in panels/notification/common/notifyentity.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QLocale> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QStringList>

Check warning on line 9 in panels/notification/common/notifyentity.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QStringList> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QLoggingCategory>

Check warning on line 10 in panels/notification/common/notifyentity.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QLoggingCategory> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QImage>

Check warning on line 11 in panels/notification/common/notifyentity.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QImage> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QBuffer>

Check warning on line 12 in panels/notification/common/notifyentity.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QBuffer> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QDBusArgument>

Check warning on line 13 in panels/notification/common/notifyentity.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDBusArgument> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QRegularExpression>

Check warning on line 14 in panels/notification/common/notifyentity.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QRegularExpression> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <unicode/reldatefmt.h>

Check warning on line 16 in panels/notification/common/notifyentity.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <unicode/reldatefmt.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <unicode/smpdtfmt.h>

Check warning on line 17 in panels/notification/common/notifyentity.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <unicode/smpdtfmt.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <memory>

Expand Down Expand Up @@ -292,6 +296,136 @@
return QString();
}

static inline void copyLineRGB32(QRgb *dst, const char *src, int width)
{
const char *end = src + width * 3;
for (; src != end; ++dst, src += 3) {
*dst = qRgb(static_cast<uchar>(src[0]), static_cast<uchar>(src[1]), static_cast<uchar>(src[2]));
}
}

static inline void copyLineARGB32(QRgb *dst, const char *src, int width)
{
const char *end = src + width * 4;
for (; src != end; ++dst, src += 4) {
*dst = qRgba(static_cast<uchar>(src[0]), static_cast<uchar>(src[1]), static_cast<uchar>(src[2]), static_cast<uchar>(src[3]));
}
}

static QImage decodeImageFromDBusArgument(const QDBusArgument &arg)
{
int width, height, rowStride, hasAlpha, bitsPerSample, channels;
QByteArray pixels;
char *ptr;
char *end;

Check warning on line 320 in panels/notification/common/notifyentity.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Variable 'end' can be declared as pointer to const

arg.beginStructure();
arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels;
arg.endStructure();

#define SANITY_CHECK(condition) \
if (!(condition)) { \
qWarning(notifyLog) << "Sanity check failed on" << #condition; \
return QImage(); \
}

SANITY_CHECK(width > 0);
SANITY_CHECK(width < 2048);
SANITY_CHECK(height > 0);
SANITY_CHECK(height < 2048);
SANITY_CHECK(rowStride > 0);

#undef SANITY_CHECK

QImage::Format format = QImage::Format_Invalid;
void (*fcn)(QRgb *, const char *, int) = nullptr;
if (bitsPerSample == 8) {
if (channels == 4) {
format = QImage::Format_ARGB32;
fcn = copyLineARGB32;
} else if (channels == 3) {
format = QImage::Format_RGB32;
fcn = copyLineRGB32;
}
}
if (format == QImage::Format_Invalid) {
qWarning(notifyLog) << "Unsupported image format (hasAlpha:" << hasAlpha << "bitsPerSample:" << bitsPerSample << "channels:" << channels << ")";
return QImage();
}

QImage image(width, height, format);
ptr = pixels.data();
end = ptr + pixels.length();
for (int y = 0; y < height; ++y, ptr += rowStride) {
if (ptr + channels * width > end) {
qWarning(notifyLog) << "Image data is incomplete. y:" << y << "height:" << height;
break;
}
fcn(reinterpret_cast<QRgb *>(image.scanLine(y)), ptr, width);
}

return image;
}

static QString decodeImageToBase64(const QImage &image, const char *format = "PNG")
{
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, format);

return QString("data:image/%1;base64,%2").arg(QString::fromLatin1(format).toLower()).arg(QString::fromLatin1(ba.toBase64()));
}

static QString imagePathOfNotification(const QVariantMap &hints, const QString &appIcon)
{
static const QStringList HintsOrder {
"desktop-entry",
"image-data",
"icon_data"
};

QImage img;
QString imageData(appIcon);
for (const auto &hint : HintsOrder) {
const auto &source = hints[hint];
if (source.isNull())
continue;
if (source.canConvert<QDBusArgument>()) {
img = decodeImageFromDBusArgument(source.value<QDBusArgument>());
if (!img.isNull())
break;
}
imageData = source.toString();
}
if (img.isNull()) {
// check if imageData is a base64 image data.
QRegularExpression dataUriPattern("^data:image/[a-zA-Z0-9+\\-]+;base64,");
QRegularExpressionMatch match = dataUriPattern.match(imageData);
if (match.hasMatch()) {
return imageData;
}
} else {
return decodeImageToBase64(img);
}

// ui can fallback to application-x-desktop icon.
return {};
}

QString NotifyEntity::appIconResolved() const
{
const QString iconFromHints = imagePathOfNotification(d->hints, d->appIcon);
if (!iconFromHints.isEmpty())
return iconFromHints;

if (!d->appIcon.isEmpty()) {
return d->appIcon;
}

return {};
}

QString NotifyEntity::convertHintsToString(const QVariantMap &map)
{
QString text;
Expand Down
4 changes: 4 additions & 0 deletions panels/notification/common/notifyentity.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ class NotifyEntity

QString bodyIcon() const;

// Resolves the app icon considering image-data/icon_data hints from the notification spec.
// Returns a base64 data URI if image-data/icon_data hint is present, otherwise returns appIcon.
QString appIconResolved() const;

// Formats a creation time (ms since epoch) as a locale-aware relative
// time string. Returns empty string if less than 1 minute or invalid.
static QString formatRelativeTime(qint64 ctimeMs);
Expand Down
Loading