433 lines
11 KiB
C++
433 lines
11 KiB
C++
/*
|
||
* Copyright (C) 2023, KylinSoft Co., Ltd.
|
||
*
|
||
* 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 3 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.
|
||
*
|
||
* You should have received a copy of the GNU General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
*
|
||
* Authors: iaom <zhangpengfei@kylinos.cn>
|
||
*/
|
||
|
||
#include "popup-notification.h"
|
||
#include <QDBusArgument>
|
||
#include <QDebug>
|
||
#include <QImageReader>
|
||
namespace UkuiNotification {
|
||
class PopupNotificationPrivate
|
||
{
|
||
public:
|
||
PopupNotificationPrivate();
|
||
~PopupNotificationPrivate();
|
||
QImage parseImageHint(const QDBusArgument &arg);
|
||
void loadImageFromPath(const QString &path);
|
||
QSize maximumImageSize();
|
||
|
||
uint m_id = 0;
|
||
QString m_applicationName;
|
||
QString m_applicationIconName;
|
||
QString m_summary;
|
||
QString m_body;
|
||
QList<QPair<QString, QString>> m_actions;
|
||
bool m_hasDefaultAction = false;
|
||
QString m_defaultActionLabel;
|
||
QVariantMap m_hints = QVariantMap();
|
||
QString m_category;
|
||
QImage m_image;
|
||
QString m_icon;
|
||
QDateTime m_createdTime;
|
||
QString m_desktopEntry;
|
||
PopupNotification::Urgency m_urgency = PopupNotification::NormalUrgency;
|
||
int m_timeout = -1;
|
||
bool m_resident = false;
|
||
bool m_transient = false;
|
||
QString m_soundFile;
|
||
QString m_soundName;
|
||
bool m_suppressSound = false;
|
||
QString m_display;
|
||
QStringList m_actionState;
|
||
bool m_noFold = false;
|
||
int m_popupTimeout = 6000; //ms
|
||
};
|
||
}
|
||
using namespace UkuiNotification;
|
||
|
||
PopupNotificationPrivate::PopupNotificationPrivate()
|
||
{
|
||
}
|
||
|
||
PopupNotificationPrivate::~PopupNotificationPrivate() = default;
|
||
|
||
QImage PopupNotificationPrivate::parseImageHint(const QDBusArgument &arg)
|
||
{
|
||
//copy from plasma-workspace
|
||
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();
|
||
|
||
auto 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]);
|
||
}
|
||
};
|
||
|
||
auto 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]);
|
||
}
|
||
};
|
||
|
||
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() << "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() << "Image data is incomplete. y:" << y << "height:" << height;
|
||
break;
|
||
}
|
||
fcn((QRgb *)image.scanLine(y), ptr, width);
|
||
}
|
||
|
||
return image;
|
||
}
|
||
|
||
void PopupNotificationPrivate::loadImageFromPath(const QString &path)
|
||
{
|
||
//copy from plasma-workspace
|
||
QUrl imageUrl;
|
||
if (path.startsWith(QLatin1Char('/'))) {
|
||
imageUrl = QUrl::fromLocalFile(path);
|
||
} else if (path.contains(QLatin1Char('/'))) {
|
||
imageUrl = QUrl(path);
|
||
|
||
if (!imageUrl.isLocalFile()) {
|
||
qDebug() << "Refused to load image from" << path << "which isn't a valid local location.";
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!imageUrl.isValid()) {
|
||
m_icon = path;
|
||
return;
|
||
}
|
||
|
||
QImageReader reader(imageUrl.toLocalFile());
|
||
reader.setAutoTransform(true);
|
||
|
||
const QSize imageSize = reader.size();
|
||
if (imageSize.isValid() && (imageSize.width() > maximumImageSize().width() || imageSize.height() > maximumImageSize().height())) {
|
||
const QSize thumbnailSize = imageSize.scaled(maximumImageSize(), Qt::KeepAspectRatio);
|
||
reader.setScaledSize(thumbnailSize);
|
||
}
|
||
|
||
m_image = reader.read();
|
||
}
|
||
|
||
QSize PopupNotificationPrivate::maximumImageSize()
|
||
{
|
||
return QSize(256, 256);
|
||
}
|
||
|
||
PopupNotification::PopupNotification(uint id) : d(new PopupNotificationPrivate())
|
||
{
|
||
d->m_id = id;
|
||
}
|
||
|
||
PopupNotification::PopupNotification(const PopupNotification &other) : d(new PopupNotificationPrivate(*other.d))
|
||
{
|
||
}
|
||
|
||
PopupNotification &PopupNotification::operator=( const PopupNotification &other)
|
||
{
|
||
*d = *other.d;
|
||
return *this;
|
||
}
|
||
|
||
PopupNotification &PopupNotification::operator=(PopupNotification &&other) Q_DECL_NOEXCEPT
|
||
{
|
||
d = other.d;
|
||
other.d = nullptr;
|
||
return *this;
|
||
}
|
||
PopupNotification::~PopupNotification()
|
||
{
|
||
if(d) {
|
||
delete d;
|
||
d = nullptr;
|
||
}
|
||
}
|
||
|
||
uint PopupNotification::id() const
|
||
{
|
||
return d->m_id;
|
||
}
|
||
|
||
QString PopupNotification::applicationName() const
|
||
{
|
||
return d->m_applicationName;
|
||
}
|
||
|
||
void PopupNotification::setApplicationName(const QString &applicationName)
|
||
{
|
||
d->m_applicationName = applicationName;
|
||
}
|
||
|
||
QString PopupNotification::applicationIconName() const
|
||
{
|
||
return d->m_applicationIconName;
|
||
}
|
||
|
||
void PopupNotification::setApplicationIconName(const QString &applicationIconName)
|
||
{
|
||
d->m_applicationIconName = applicationIconName;
|
||
}
|
||
|
||
QString PopupNotification::summary() const
|
||
{
|
||
return d->m_summary;
|
||
}
|
||
|
||
void PopupNotification::setSummary(const QString &summary)
|
||
{
|
||
d->m_summary = summary;
|
||
}
|
||
|
||
QString PopupNotification::body() const
|
||
{
|
||
return d->m_body;
|
||
}
|
||
|
||
void PopupNotification::setBody(const QString &body)
|
||
{
|
||
d->m_body = body;
|
||
}
|
||
|
||
bool PopupNotification::hasDefaultAction() const
|
||
{
|
||
return d->m_hasDefaultAction;
|
||
}
|
||
|
||
QString PopupNotification::defaultActionLabel()
|
||
{
|
||
return d->m_defaultActionLabel;
|
||
}
|
||
|
||
void PopupNotification::setActions(const QStringList &actions)
|
||
{
|
||
if (actions.count() % 2 != 0) {
|
||
qWarning() << "List of actions must contain an even number of items, tried to set actions to" << actions;
|
||
return;
|
||
}
|
||
|
||
d->m_hasDefaultAction = false;
|
||
|
||
for (int i = 0; i < actions.count(); i += 2) {
|
||
const QString &key = actions.at(i);
|
||
const QString &label = actions.at(i + 1);
|
||
|
||
if (!d->m_hasDefaultAction && key == QLatin1String("default")) {
|
||
d->m_hasDefaultAction = true;
|
||
d->m_defaultActionLabel = label;
|
||
}
|
||
d->m_actions.append({key, label});
|
||
}
|
||
}
|
||
|
||
QList<QPair<QString, QString>> PopupNotification::actions() const
|
||
{
|
||
return d->m_actions;
|
||
}
|
||
|
||
QVariantMap PopupNotification::hints() const
|
||
{
|
||
return d->m_hints;
|
||
}
|
||
|
||
void PopupNotification::setHints(const QVariantMap &hints)
|
||
{
|
||
d->m_hints = hints;
|
||
d->m_desktopEntry = hints.value(QStringLiteral("desktop-entry")).toString();
|
||
bool ok;
|
||
int urgency = hints.value(QStringLiteral("urgency")).toInt(&ok);
|
||
if(ok) {
|
||
switch (urgency) {
|
||
default:
|
||
case 0: //低等级,不弹窗
|
||
setUrgency(Urgency::LowUrgency);
|
||
d->m_popupTimeout = 0;
|
||
break;
|
||
case 1: //中等级,弹窗时间默认6秒
|
||
setUrgency(Urgency::NormalUrgency);
|
||
d->m_popupTimeout = 6000;
|
||
break;
|
||
case 2: //高等级,默认弹窗常驻,不可折叠
|
||
setUrgency(Urgency::CriticalUrgency);
|
||
d->m_popupTimeout = -1;
|
||
d->m_noFold = true;
|
||
break;
|
||
}
|
||
}
|
||
d->m_createdTime = QDateTime::fromString(hints.value(QStringLiteral("x-ukui-createdTime")).toString());
|
||
d->m_resident = hints.value(QStringLiteral("resident")).toBool();
|
||
d->m_transient = hints.value(QStringLiteral("transient")).toBool();
|
||
d->m_category = hints.value(QStringLiteral("category")).toString();
|
||
|
||
auto end = hints.end();
|
||
auto it = hints.find(QStringLiteral("image-data"));
|
||
if (it != end) {
|
||
d->m_image = d->parseImageHint(it->value<QDBusArgument>());
|
||
if (!d->m_image.isNull()) {
|
||
const QSize max = d->maximumImageSize();
|
||
if (d->m_image.size().width() > max.width() || d->m_image.size().height() > max.height()) {
|
||
d->m_image = d->m_image.scaled(max, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||
}
|
||
}
|
||
}
|
||
if (d->m_image.isNull()) {
|
||
it = hints.find(QStringLiteral("image-path"));
|
||
|
||
if (it != end) {
|
||
d->loadImageFromPath(it->toString());
|
||
}
|
||
}
|
||
|
||
d->m_soundFile = hints.value(QStringLiteral("sound-file")).toString();
|
||
d->m_soundName = hints.value(QStringLiteral("sound-name")).toString();
|
||
d->m_suppressSound = hints.value(QStringLiteral("suppress-sound")).toBool();
|
||
d->m_display = hints.value(QStringLiteral("x-ukui-display")).toString();
|
||
d->m_actionState = hints.value(QStringLiteral("x-ukui-action-state")).toStringList();
|
||
|
||
QVariant tmp = hints.value(QStringLiteral("x-ukui-no-fold"));
|
||
if(!tmp.isNull()) {
|
||
d->m_noFold = tmp.toBool();
|
||
}
|
||
tmp = hints.value(QStringLiteral("x-ukui-popup-timeout"));
|
||
if(!tmp.isNull()) {
|
||
d->m_popupTimeout = tmp.toInt();
|
||
}
|
||
}
|
||
|
||
int PopupNotification::timeout() const
|
||
{
|
||
return d->m_timeout;
|
||
}
|
||
|
||
void PopupNotification::setTimeout(int timeout)
|
||
{
|
||
d->m_timeout = timeout;
|
||
}
|
||
|
||
QDateTime PopupNotification::createdTime() const
|
||
{
|
||
return d->m_createdTime;
|
||
}
|
||
|
||
bool PopupNotification::enableActionIcons() const
|
||
{
|
||
//TODO 可以支持action-icon
|
||
return false;
|
||
}
|
||
|
||
QString PopupNotification::category() const
|
||
{
|
||
return d->m_category;
|
||
}
|
||
|
||
QString PopupNotification::desktopEntry() const
|
||
{
|
||
return d->m_desktopEntry;
|
||
}
|
||
|
||
QImage PopupNotification::image() const
|
||
{
|
||
return d->m_image;
|
||
}
|
||
|
||
QString PopupNotification::icon() const
|
||
{
|
||
return d->m_icon;
|
||
}
|
||
|
||
bool PopupNotification::resident() const
|
||
{
|
||
return d->m_resident;
|
||
}
|
||
|
||
QString PopupNotification::soundFile() const
|
||
{
|
||
return d->m_soundFile;
|
||
}
|
||
QString PopupNotification::soundName() const
|
||
{
|
||
return d->m_soundName;
|
||
}
|
||
|
||
bool PopupNotification::suppressSound() const
|
||
{
|
||
return d->m_suppressSound;
|
||
}
|
||
|
||
bool PopupNotification::transient() const
|
||
{
|
||
return d->m_transient;
|
||
}
|
||
|
||
QString PopupNotification::display() const
|
||
{
|
||
return d->m_display;
|
||
}
|
||
|
||
void PopupNotification::setUrgency(PopupNotification::Urgency urgency)
|
||
{
|
||
d->m_urgency = urgency;
|
||
}
|
||
|
||
PopupNotification::Urgency PopupNotification::urgency() const
|
||
{
|
||
return d->m_urgency;
|
||
}
|
||
|
||
QStringList PopupNotification::actionState() const {
|
||
return d->m_actionState;
|
||
}
|
||
|
||
bool PopupNotification::noFold() const {
|
||
return d->m_noFold;
|
||
}
|
||
|
||
int PopupNotification::popupTimeout() const {
|
||
return d->m_popupTimeout;
|
||
}
|