diff --git a/debian/control b/debian/control index bb4c39932..7e0985d5c 100644 --- a/debian/control +++ b/debian/control @@ -38,7 +38,7 @@ Description: DDE Shell devel library Package: dde-shell Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, libdde-shell( =${binary:Version}), - qml6-module-qtquick-layouts, qml6-module-qtquick-window, + qml6-module-qtquick-layouts, qml6-module-qtquick-window, libqt6svg6, libdtk6declarative, qml6-module-qtquick-controls2-styles-chameleon Multi-Arch: same Description: An wrapper for developed based on dde-shell plugin system diff --git a/debian/dde-shell.install b/debian/dde-shell.install index 8e2605cf8..632c806cf 100644 --- a/debian/dde-shell.install +++ b/debian/dde-shell.install @@ -1,3 +1,5 @@ usr/bin/* usr/lib/*/dde-shell/org.deepin.ds.osd* usr/share/dde-shell/org.deepin.ds.osd*/ +usr/lib/*/dde-shell/org.deepin.ds.notification* +usr/share/dde-shell/org.deepin.ds.notification*/ diff --git a/panels/CMakeLists.txt b/panels/CMakeLists.txt index 55fcb3470..df7e5352d 100644 --- a/panels/CMakeLists.txt +++ b/panels/CMakeLists.txt @@ -3,3 +3,4 @@ # SPDX-License-Identifier: GPL-3.0-or-later add_subdirectory(osd) +add_subdirectory(notification) diff --git a/panels/notification/CMakeLists.txt b/panels/notification/CMakeLists.txt new file mode 100644 index 000000000..76bccf0f4 --- /dev/null +++ b/panels/notification/CMakeLists.txt @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +add_library(notificationpanel SHARED + notificationpanel.cpp + notificationpanel.h + notificationinterproxy.cpp + notificationinterproxy.h + bubblemodel.cpp + bubblemodel.h + test.sh +) + +target_link_libraries(notificationpanel PRIVATE + dde-shell-frame + Qt${QT_MAJOR_VERSION}::DBus +) + +ds_install_package(PACKAGE org.deepin.ds.notification TARGET notificationpanel) diff --git a/panels/notification/bubblemodel.cpp b/panels/notification/bubblemodel.cpp new file mode 100644 index 000000000..2f538c071 --- /dev/null +++ b/panels/notification/bubblemodel.cpp @@ -0,0 +1,521 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "bubblemodel.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +DS_BEGIN_NAMESPACE +namespace notification { + +Q_DECLARE_LOGGING_CATEGORY(notificationLog) + +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() << width << height << rowStride << hasAlpha << bitsPerSample << channels; + +#define SANITY_CHECK(condition) \ + if (!(condition)) { \ + qWarning() << "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() << "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; +} + +static QImage decodeImageFromBase64(const QString &arg) +{ + if (arg.startsWith("data:image/")) { + // iconPath is a string representing an inline image. + QStringList strs = arg.split("base64,"); + if (strs.length() == 2) { + QByteArray data = QByteArray::fromBase64(strs.at(1).toLatin1()); + return QImage::fromData(data); + } + } + return QImage(); +} + +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) +{ + static const QStringList HintsOrder { + "desktop-entry", + "image-data", + "image-path", + "image_path", + "icon_data" + }; + + QImage img; + QString imageData(appIcon); + for (auto hint : HintsOrder) { + const auto &source = hints[hint]; + if (source.isNull()) + continue; + if (source.canConvert()) { + img = decodeImageFromDBusArgument(source.value()); + if (!img.isNull()) + break; + } + imageData = source.toString(); + } + if (img.isNull()) { + img = decodeImageFromBase64(imageData); + } + if (!img.isNull()) { + QTemporaryFile file("notification_icon"); + img.save(file.fileName()); + return file.fileName(); + } + QIcon icon = decodeIconFromPath(imageData, appName); + if (icon.isNull()) { + qCWarning(notificationLog) << "Can't get icon for notification, appName:" << appName; + } + return icon.name(); +} + +BubbleItem::BubbleItem() +{ + +} + +BubbleItem::BubbleItem(const QString &text, const QString &title, const QString &iconName) + : m_text(text) + , m_title(title) + , m_iconName(iconName) + , m_ctime(QString::number(QDateTime::currentMSecsSinceEpoch())) +{ + +} + +QString BubbleItem::text() const +{ + return displayText(); +} + +QString BubbleItem::title() const +{ + return m_title; +} + +QString BubbleItem::iconName() const +{ + return imagePathOfNotification(m_hints, m_iconName, m_appName); +} + +QString BubbleItem::appName() const +{ + return m_appName; +} + +int BubbleItem::level() const +{ + return m_level; +} + +int BubbleItem::id() const +{ + return m_id; +} + +int BubbleItem::replaceId() const +{ + return m_replaceId; +} + +void BubbleItem::setLevel(int newLevel) +{ + if (m_level == newLevel) + return; + m_level = newLevel; + emit levelChanged(); +} + +void BubbleItem::setParams(const QString &appName, int id, const QStringList &actions, const QVariantMap hints, int replaceId, const int timeout, const QVariantMap bubbleParams) +{ + m_appName = appName; + m_id = id; + m_actions = actions; + m_hints = hints; + m_replaceId = replaceId; + m_timeout = timeout; + m_extraParams = bubbleParams; + if (m_timeout >= 0) { + auto timer = new QTimer(this); + timer->setSingleShot(true); + timer->setInterval(m_timeout == 0 ? TimeOutInterval : m_timeout); + QObject::connect(timer, &QTimer::timeout, this, &BubbleItem::timeout); + timer->start(); + } +} + +QVariantMap BubbleItem::toMap() const +{ + QVariantMap res; + res["id"] = m_id; + res["replaceId"] = m_replaceId; + res["appName"] = m_appName; + res["appIcon"] = m_iconName; + res["summary"] = m_title; + res["body"] = m_text; + res["actions"] = m_actions; + res["hints"] = m_hints; + res["ctime"] = m_ctime; + res["extraParams"] = m_extraParams; + return res; +} + +QString BubbleItem::defaultActionText() const +{ + const auto index = defaultActionTextIndex(); + if (index < 0) + return QString(); + return m_actions[index]; +} + +QString BubbleItem::defaultActionId() const +{ + const auto index = defaultActionIdIndex(); + if (index < 0) + return QString(); + return m_actions[index]; +} + +QString BubbleItem::firstActionText() const +{ + if (!hasDisplayAction()) + return QString(); + return displayActions()[1]; +} + +QString BubbleItem::firstActionId() const +{ + if (!hasDisplayAction()) + return QString(); + return displayActions()[0]; +} + +QStringList BubbleItem::actionTexts() const +{ + QStringList res; + const auto tmp = displayActions(); + for (int i = 3; i < tmp.count(); i += 2) + res << tmp[i]; + return res; +} + +QStringList BubbleItem::actionIds() const +{ + QStringList res; + const auto tmp = displayActions(); + for (int i = 2; i < tmp.count(); i += 2) + res << tmp[i]; + return res; +} + +int BubbleItem::defaultActionIdIndex() const +{ + return m_actions.indexOf("default"); +} + +int BubbleItem::defaultActionTextIndex() const +{ + const auto index = defaultActionTextIndex(); + if (index >= 0) + return index + 1; + return -1; +} + +QStringList BubbleItem::displayActions() const +{ + const auto defaultIndex = defaultActionIdIndex(); + if (defaultIndex >= 0) { + auto tmp = m_actions; + tmp.remove(defaultIndex, 2); + return tmp; + } + return m_actions; +} + +QString BubbleItem::displayText() const +{ + return m_extraParams["isShowPreview"].toBool() ? m_text : tr("1 new message"); +} + +bool BubbleItem::hasDisplayAction() const +{ + const auto tmp = displayActions(); + return tmp.count() >= 2; +} + +bool BubbleItem::hasDefaultAction() const +{ + return defaultActionIdIndex() >= 0; +} + +BubbleModel::BubbleModel(QObject *parent) + : QAbstractListModel(parent) +{ + +} + +BubbleModel::~BubbleModel() +{ + qDeleteAll(m_bubbles); + m_bubbles.clear(); +} + +void BubbleModel::push(BubbleItem *bubble) +{ + bool more = displayRowCount() >= BubbleMaxCount; + if (more) { + beginRemoveRows(QModelIndex(), BubbleMaxCount - 1, BubbleMaxCount - 1); + endRemoveRows(); + } + beginInsertRows(QModelIndex(), 0, 0); + m_bubbles.prepend(bubble); + endInsertRows(); + + updateLevel(); +} + +bool BubbleModel::isReplaceBubble(BubbleItem *bubble) const +{ + return replaceBubbleIndex(bubble) >= 0; +} + +BubbleItem *BubbleModel::replaceBubble(BubbleItem *bubble) +{ + Q_ASSERT(isReplaceBubble(bubble)); + const auto replaceIndex = replaceBubbleIndex(bubble); + const auto oldBubble = m_bubbles[replaceIndex]; + m_bubbles.replace(replaceIndex, bubble); + Q_EMIT dataChanged(index(replaceIndex), index(replaceIndex)); + + return oldBubble; +} + +void BubbleModel::clear() +{ + if (m_bubbles.count() <= 0) + return; + beginRemoveRows(QModelIndex(), 0, m_bubbles.count() - 1); + m_bubbles.clear(); + endResetModel(); + + updateLevel(); +} + +QList BubbleModel::items() const +{ + return m_bubbles; +} + +void BubbleModel::remove(int index) +{ + if (index < 0 || index >= displayRowCount()) + return; + + beginRemoveRows(QModelIndex(), index, index); + auto bubble = m_bubbles.takeAt(index); + bubble->deleteLater(); + endRemoveRows(); + + if (m_bubbles.count() >= BubbleMaxCount) { + beginInsertRows(QModelIndex(), displayRowCount() - 1, displayRowCount() - 1); + endInsertRows(); + updateLevel(); + } +} + +void BubbleModel::remove(BubbleItem *bubble) +{ + const auto index = m_bubbles.indexOf(bubble); + if (index >= 0) { + remove(index); + } +} + +int BubbleModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return displayRowCount(); +} + +QVariant BubbleModel::data(const QModelIndex &index, int role) const +{ + const int row = index.row(); + if (row >= m_bubbles.size() || !index.isValid()) + return {}; + + switch (role) { + case BubbleModel::Text: + return m_bubbles[row]->text(); + case BubbleModel::Title: + return m_bubbles[row]->title(); + case BubbleModel::IconName: + return m_bubbles[row]->iconName(); + case BubbleModel::Level: + return m_bubbles[row]->level(); + case BubbleModel::OverlayCount: + return overlayCount(); + case BubbleModel::hasDefaultAction: + return m_bubbles[row]->hasDefaultAction(); + case BubbleModel::hasDisplayAction: + return m_bubbles[row]->hasDisplayAction(); + case BubbleModel::FirstActionText: + return m_bubbles[row]->firstActionText(); + case BubbleModel::FirstActionId: + return m_bubbles[row]->firstActionId(); + case BubbleModel::ActionTexts: + return m_bubbles[row]->actionTexts(); + case BubbleModel::ActionIds: + return m_bubbles[row]->actionIds(); + default: + break; + } + return {}; +} + +QHash BubbleModel::roleNames() const +{ + QHash mapRoleNames; + mapRoleNames[BubbleModel::Text] = "text"; + mapRoleNames[BubbleModel::Title] = "title"; + mapRoleNames[BubbleModel::IconName] = "iconName"; + mapRoleNames[BubbleModel::Level] = "level"; + mapRoleNames[BubbleModel::OverlayCount] = "overlayCount"; + mapRoleNames[BubbleModel::hasDefaultAction] = "hasDefaultAction"; + mapRoleNames[BubbleModel::hasDisplayAction] = "hasDisplayAction"; + mapRoleNames[BubbleModel::FirstActionText] = "firstActionText"; + mapRoleNames[BubbleModel::FirstActionId] = "firstActionId"; + mapRoleNames[BubbleModel::ActionTexts] = "actionTexts"; + mapRoleNames[BubbleModel::ActionIds] = "actionIds"; + return mapRoleNames; +} + +int BubbleModel::displayRowCount() const +{ + return qMin(m_bubbles.count(), BubbleMaxCount); +} + +int BubbleModel::overlayCount() const +{ + return qMin(m_bubbles.count() - displayRowCount(), OverlayMaxCount); +} + +int BubbleModel::replaceBubbleIndex(BubbleItem *bubble) const +{ + if (bubble->replaceId() != NoReplaceId) { + for (int i = 0; i < displayRowCount(); i++) { + auto item = m_bubbles[i]; + if (item->appName() != item->appName()) + continue; + + const bool firstItem = item->replaceId() == NoReplaceId && item->id() == bubble->replaceId(); + const bool laterItem = item->replaceId() == bubble->replaceId(); + if (firstItem || laterItem) { + return i; + } + } + } + return -1; +} + +void BubbleModel::updateLevel() +{ + if (m_bubbles.isEmpty()) + return; + + for (int i = 0; i < displayRowCount(); i++) { + auto item = m_bubbles.at(i); + item->setLevel(i <= 1 ? 1 : 1 + overlayCount()); + } + Q_EMIT dataChanged(index(0), index(displayRowCount() - 1), {BubbleModel::Level}); +} + +} +DS_END_NAMESPACE diff --git a/panels/notification/bubblemodel.h b/panels/notification/bubblemodel.h new file mode 100644 index 000000000..f7302e4ff --- /dev/null +++ b/panels/notification/bubblemodel.h @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "dsglobal.h" + +#include + +DS_BEGIN_NAMESPACE +namespace notification { + +class BubbleItem : public QObject +{ + Q_OBJECT +public: + explicit BubbleItem(); + explicit BubbleItem(const QString &text, const QString &title, const QString &iconName); + + QString text() const; + QString title() const; + QString iconName() const; + QString appName() const; + int level() const; + int id() const; + int replaceId() const; + + void setLevel(int newLevel); + void setParams(const QString &appName, int id, const QStringList &actions, + const QVariantMap hints, int replaceId, const int timeout, + const QVariantMap bubbleParams); + + QVariantMap toMap() const; + + bool hasDisplayAction() const; + bool hasDefaultAction() const; + QString defaultActionText() const; + QString defaultActionId() const; + QString firstActionText() const; + QString firstActionId() const; + QStringList actionTexts() const; + QStringList actionIds() const; + +Q_SIGNALS: + void levelChanged(); + void timeout(); + +private: + int defaultActionIdIndex() const; + int defaultActionTextIndex() const; + QStringList displayActions() const; + QString displayText() const; + +public: + QString m_text; + QString m_title; + QString m_iconName; + QString m_appName; + int m_id = 0; + QStringList m_actions; + QVariantMap m_hints; + int m_replaceId; + int m_timeout = 0; + QString m_ctime; + QVariantMap m_extraParams; + +private: + int m_level = 0; + const int TimeOutInterval{5000}; +}; + +class BubbleModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum { + Text = Qt::UserRole + 1, + Title, + IconName, + Level, + OverlayCount, + hasDefaultAction, + hasDisplayAction, + FirstActionText, + FirstActionId, + DefaultActionId, + ActionTexts, + ActionIds, + } BubbleRule; + + explicit BubbleModel(QObject *parent = nullptr); + ~BubbleModel(); + + void push(BubbleItem *bubble); + BubbleItem *replaceBubble(BubbleItem *bubble); + bool isReplaceBubble(BubbleItem *bubble) const; + void clear(); + QList items() const; + Q_INVOKABLE void remove(int index); + void remove(BubbleItem *bubble); + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + int displayRowCount() const; + int overlayCount() const; + +private: + int replaceBubbleIndex(BubbleItem *bubble) const; + void updateLevel(); + QList m_bubbles; + const int BubbleMaxCount{3}; + const int OverlayMaxCount{2}; + const int NoReplaceId{0}; +}; + +} +DS_END_NAMESPACE diff --git a/panels/notification/notificationinterproxy.cpp b/panels/notification/notificationinterproxy.cpp new file mode 100644 index 000000000..b9170bf62 --- /dev/null +++ b/panels/notification/notificationinterproxy.cpp @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "notificationinterproxy.h" + +#include + +#include + +DS_BEGIN_NAMESPACE +namespace notification { + +static DDBusSender notificationInter() +{ + return DDBusSender() + .service("org.deepin.dde.Notification1") + .path("/org/deepin/dde/Notification1") + .interface("org.deepin.dde.Notification1"); +} + +NotificationProxy::NotificationProxy(QObject *parent) + : QObject(parent) +{ + auto bus = QDBusConnection::sessionBus(); + bool res = bus.connect("org.deepin.dde.Notification1", + "/org/deepin/dde/Notification1", + "org.deepin.dde.Notification1", + "ShowBubble", + this, SLOT(ShowBubble(const QString &, uint, const QString &, const QString &, const QString &, const QStringList &, const QVariantMap &, int, const QVariantMap &))); + m_valid = res; + if (!isValid()) { + qWarning() << "Failed to connect Notification's ShowBubble signal" << bus.lastError(); + } +} + +bool NotificationProxy::isValid() const +{ + return m_valid; +} + +bool NotificationProxy::replaceNotificationBubble(bool replace) +{ + auto reply = notificationInter().method("ReplaceBubble").arg(replace).call(); + reply.waitForFinished(); + return !reply.isError(); +} + +void NotificationProxy::handleBubbleEnd(int type, int id) +{ + return handleBubbleEnd(type, id, {}, {}); +} + +void NotificationProxy::handleBubbleEnd(int type, int id, const QVariantMap &bubbleParams, const QVariantMap &selectedHints) +{ + notificationInter().method("HandleBubbleEnd").arg(static_cast(type)).arg(static_cast(id)).arg(bubbleParams).arg(selectedHints).call(); +} + +} +DS_END_NAMESPACE diff --git a/panels/notification/notificationinterproxy.h b/panels/notification/notificationinterproxy.h new file mode 100644 index 000000000..bf55eb241 --- /dev/null +++ b/panels/notification/notificationinterproxy.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "dsglobal.h" + +#include + +DS_BEGIN_NAMESPACE +namespace notification { + +class NotificationProxy : public QObject +{ + Q_OBJECT +public: + explicit NotificationProxy(QObject *parent = nullptr); + + enum ClosedReason { + Expired = 1, + Dismissed = 2, + Closed = 3, + Unknown = 4, + Action = 5, + NotProcessedYet, + }; + + bool isValid() const; + + bool replaceNotificationBubble(bool replace); + void handleBubbleEnd(int type, int id); + void handleBubbleEnd(int type, int id, const QVariantMap &bubbleParams, const QVariantMap &selectedHints); + +Q_SIGNALS: + void ShowBubble(const QString &appName, uint replacesId, + const QString &appIcon, const QString &summary, + const QString &body, const QStringList &actions, + const QVariantMap hints, int expireTimeout, + const QVariantMap bubbleParams); + +private: + bool m_valid = false; +}; + +} +DS_END_NAMESPACE diff --git a/panels/notification/notificationpanel.cpp b/panels/notification/notificationpanel.cpp new file mode 100644 index 000000000..bd2501ecc --- /dev/null +++ b/panels/notification/notificationpanel.cpp @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "notificationpanel.h" + +#include "bubblemodel.h" +#include "notificationinterproxy.h" +#include "pluginfactory.h" + +#include + +DS_BEGIN_NAMESPACE +namespace notification { + +Q_LOGGING_CATEGORY(notificationLog, "dde.shell.notification") + +NotificationPanel::NotificationPanel(QObject *parent) + : DPanel(parent) + , m_bubbles(new BubbleModel(this)) +{ +} + +NotificationPanel::~NotificationPanel() +{ + if (m_interproxy && m_interproxy->isValid()) { + qCInfo(notificationLog) << "Cancle ReplaceBubble of osd's service."; + if (!m_interproxy->replaceNotificationBubble(false)) { + qCWarning(notificationLog) << "Failed to cancle ReplaceBubble of osd's service."; + } + } +} + +bool NotificationPanel::load() +{ + return DPanel::load(); +} + +bool NotificationPanel::init() +{ + DPanel::init(); + + m_interproxy = new NotificationProxy(this); + if (!m_interproxy->isValid()) { + qCWarning(notificationLog) << "Failed to fetch notification service's handler."; + return false; + } + + qCInfo(notificationLog) << "Intercept ReplaceBubble of osd's service."; + if (!m_interproxy->replaceNotificationBubble(true)) { + return false; + } + + QObject::connect(m_interproxy, &NotificationProxy::ShowBubble, this, &NotificationPanel::onShowBubble); + QObject::connect(m_bubbles, &BubbleModel::rowsInserted, this, &NotificationPanel::onBubbleCountChanged); + QObject::connect(m_bubbles, &BubbleModel::rowsRemoved, this, &NotificationPanel::onBubbleCountChanged); + + return true; +} + +bool NotificationPanel::visible() const +{ + return m_visible; +} + +void NotificationPanel::onShowBubble(const QString &appName, uint replaceId, + const QString &appIcon, const QString &summary, + const QString &body, const QStringList &actions, + const QVariantMap hints, int expireTimeout, + const QVariantMap bubbleParams) +{ + qDebug() << "Received bubble" << appName << replaceId << appIcon << summary << body + << actions << hints << expireTimeout << bubbleParams; + const auto id = bubbleParams["id"].toUInt(); + auto bubble = new BubbleItem(summary, + body, + appIcon); + bubble->setParams(appName, id, actions, hints, replaceId, expireTimeout, bubbleParams); + + QObject::connect(bubble, &BubbleItem::timeout, this, &NotificationPanel::onBubbleTimeout); + if (m_bubbles->isReplaceBubble(bubble)) { + auto oldBubble = m_bubbles->replaceBubble(bubble); + m_interproxy->handleBubbleEnd(NotificationProxy::NotProcessedYet, oldBubble->id(), oldBubble->toMap(), {}); + oldBubble->deleteLater(); + } else { + m_bubbles->push(bubble); + } +} + +void NotificationPanel::onBubbleTimeout() +{ + auto bubble = qobject_cast(sender()); + if (!bubble) + return; + + m_interproxy->handleBubbleEnd(NotificationProxy::Expired, bubble->id()); + m_interproxy->handleBubbleEnd(NotificationProxy::NotProcessedYet, bubble->id(), bubble->toMap(), {}); + m_bubbles->remove(bubble); +} + +BubbleItem *NotificationPanel::bubbleItem(int index) +{ + if (index < 0 || index >= m_bubbles->items().count()) + return nullptr; + return m_bubbles->items().at(index); +} + +void NotificationPanel::setVisible(const bool visible) +{ + if (visible == m_visible) + return; + m_visible = visible; + Q_EMIT visibleChanged(); +} + +BubbleModel *NotificationPanel::bubbles() const +{ + return m_bubbles; +} + +void NotificationPanel::defaultActionInvoke(int bubbleIndex) +{ + auto bubble = bubbleItem(bubbleIndex); + if (!bubble) + return; + + QVariantMap selectedHints; + selectedHints["actionId"] = bubble->defaultActionId(); + m_interproxy->handleBubbleEnd(NotificationProxy::Action, bubble->id(), bubble->toMap(), selectedHints); + m_bubbles->remove(bubbleIndex); +} + +void NotificationPanel::actionInvoke(int bubbleIndex, const QString &actionId) +{ + auto bubble = bubbleItem(bubbleIndex); + if (!bubble) + return; + + QVariantMap selectedHints; + selectedHints["actionId"] = actionId; + QVariantMap bubbleParams; + selectedHints["replaceId"] = bubble->m_replaceId; + m_interproxy->handleBubbleEnd(NotificationProxy::Action, bubble->id(), bubbleParams, selectedHints); + m_bubbles->remove(bubbleIndex); +} + +void NotificationPanel::close(int bubbleIndex) +{ + auto bubble = bubbleItem(bubbleIndex); + if (!bubble) + return; + + m_interproxy->handleBubbleEnd(NotificationProxy::Dismissed, bubble->id(), {}, {}); + m_bubbles->remove(bubbleIndex); +} + +void NotificationPanel::delayProcess(int bubbleIndex) +{ + auto bubble = bubbleItem(bubbleIndex); + if (!bubble) + return; + + m_interproxy->handleBubbleEnd(NotificationProxy::Dismissed, bubble->id(), {}, {}); + m_interproxy->handleBubbleEnd(NotificationProxy::NotProcessedYet, bubble->id(), bubble->toMap(), {}); + m_bubbles->remove(bubbleIndex); +} + +void NotificationPanel::onBubbleCountChanged() +{ + bool isEmpty = m_bubbles->items().isEmpty(); + setVisible(!isEmpty); +} + +D_APPLET_CLASS(NotificationPanel) + +} +DS_END_NAMESPACE + +#include "notificationpanel.moc" diff --git a/panels/notification/notificationpanel.h b/panels/notification/notificationpanel.h new file mode 100644 index 000000000..10dda8839 --- /dev/null +++ b/panels/notification/notificationpanel.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "panel.h" +#include + +DS_BEGIN_NAMESPACE +namespace notification { + +class BubbleItem; +class BubbleModel; +class NotificationProxy; +class NotificationPanel : public DPanel +{ + Q_OBJECT + Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged FINAL) + Q_PROPERTY(BubbleModel *bubbles READ bubbles CONSTANT FINAL) +public: + explicit NotificationPanel(QObject *parent = nullptr); + ~NotificationPanel(); + + virtual bool load() override; + virtual bool init() override; + + bool visible() const; + BubbleModel *bubbles() const; + +public Q_SLOTS: + void defaultActionInvoke(int bubbleIndex); + void actionInvoke(int bubbleIndex, const QString &actionId); + void close(int bubbleIndex); + void delayProcess(int bubbleIndex); + +Q_SIGNALS: + void visibleChanged(); + +private Q_SLOTS: + void onBubbleCountChanged(); + void onShowBubble(const QString &appName, uint replaceId, + const QString &appIcon, const QString &summary, + const QString &body, const QStringList &actions, + const QVariantMap hints, int expireTimeout, + const QVariantMap bubbleParams); + + void onBubbleTimeout(); +private: + void setVisible(const bool visible); + BubbleItem *bubbleItem(int index); + +private: + bool m_visible = false; + BubbleModel *m_bubbles = nullptr; + NotificationProxy *m_interproxy = nullptr; +}; + +} +DS_END_NAMESPACE diff --git a/panels/notification/package/Bubble.qml b/panels/notification/package/Bubble.qml new file mode 100644 index 000000000..ebb198430 --- /dev/null +++ b/panels/notification/package/Bubble.qml @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import org.deepin.ds 1.0 +import org.deepin.dtk 1.0 as D + +D.Control { + id: control + property var bubble + + contentItem: Loader { + sourceComponent: bubble.level <= 1 ? normalCom : overlayCom + } + Component { + id: normalCom + NormalBubble { + bubble: control.bubble + } + } + Component { + id: overlayCom + OverlayBubble { + bubble: control.bubble + } + } + + z: bubble.level <= 1 ? 0 : 1 - bubble.level +} diff --git a/panels/notification/package/BubbleAction.qml b/panels/notification/package/BubbleAction.qml new file mode 100644 index 000000000..8922ec8fe --- /dev/null +++ b/panels/notification/package/BubbleAction.qml @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.deepin.dtk 1.0 as D + +RowLayout { + property var bubble + signal actionInvoked(var actionId) + + D.Button { + Layout.preferredWidth: 60 + text: bubble.firstActionText + onClicked: { + actionInvoked(bubble.firstActionId) + } + } + + Loader { + active: bubble.actionTexts.length > 0 + sourceComponent: bubble.actionTexts.length <= 1 ? singleAction : multiActions + } + Component { + id: singleAction + D.Button { + width: 60 + text: bubble.actionTexts[0] + onClicked: { + actionInvoked(bubble.actionIds[index]) + } + } + } + Component { + id: multiActions + ComboBox { + width: 80 + model: bubble.actionTexts + delegate: Button { + width: 60 + text: modelData + onClicked: { + actionInvoked(bubble.actionIds[index]) + } + } + } + } +} diff --git a/panels/notification/package/BubbleContent.qml b/panels/notification/package/BubbleContent.qml new file mode 100644 index 000000000..38c2e14c7 --- /dev/null +++ b/panels/notification/package/BubbleContent.qml @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.deepin.ds 1.0 +import org.deepin.dtk 1.0 as D + +D.Control { + property var bubble + + MouseArea { + anchors.fill: parent + + onClicked: { + if (!bubble.hasDefaultAction) + return + + console.log("default action", bubble.index) + Applet.defaultActionInvoke(bubble.index) + } + property bool longPressed + onPressAndHold: { + longPressed = true + } + onPositionChanged: { + if (longPressed) { + longPressed = false + console.log("delay process", bubble.index) + Applet.delayProcess(bubble.index) + } + } + } + contentItem: RowLayout { + + D.QtIcon { + Layout.leftMargin: 10 + sourceSize: Qt.size(40, 40) + name: bubble.iconName + } + + ColumnLayout { + spacing: 0 + Layout.topMargin: 10 + Layout.bottomMargin: 10 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.maximumWidth: 500 + Layout.minimumHeight: 40 + Text { + visible: bubble.title !== "" + Layout.alignment: Qt.AlignLeft + elide: Text.ElideRight + text: bubble.title + Layout.fillWidth: true + maximumLineCount: 1 + font: D.DTK.fontManager.t6 + } + + Text { + visible: bubble.text !== "" + Layout.alignment: Qt.AlignLeft + text: bubble.text + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + font: D.DTK.fontManager.t7 + textFormat: Text.PlainText + } + } + + Loader { + Layout.alignment: Qt.AlignRight + active: bubble.hasDisplayAction + sourceComponent: BubbleAction { + bubble: control.bubble + onActionInvoked: function(actionId) { + console.log("action", actionId, bubble.index) + Applet.actionInvoke(bubble.index, actionId) + } + } + } + + D.ActionButton { + icon.name: "window_close" + Layout.alignment: Qt.AlignRight + onClicked: { + console.log("close", bubble.index) + Applet.close(bubble.index) + } + } + } +} diff --git a/panels/notification/package/NormalBubble.qml b/panels/notification/package/NormalBubble.qml new file mode 100644 index 000000000..7a58f4787 --- /dev/null +++ b/panels/notification/package/NormalBubble.qml @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import org.deepin.ds 1.0 +import org.deepin.dtk 1.0 as D + +D.Control { + id: control + property var bubble + + contentItem: BubbleContent { + bubble: control.bubble + } + + z: bubble.level <= 1 ? 0 : 1 - bubble.level + + background: Rectangle { + implicitWidth: 600 + radius: 18 + opacity: { + if (bubble.level === 1) + return 0.8 + return 1 + } + } +} diff --git a/panels/notification/package/OverlayBubble.qml b/panels/notification/package/OverlayBubble.qml new file mode 100644 index 000000000..fe4057671 --- /dev/null +++ b/panels/notification/package/OverlayBubble.qml @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.deepin.ds 1.0 +import org.deepin.dtk 1.0 as D + +D.Control { + id: control + property var bubble + + contentItem: ColumnLayout { + spacing: 0 + NormalBubble { + id: bubbleContent + bubble: control.bubble + } + Repeater { + model: bubble.overlayCount + Rectangle { + Layout.topMargin: -30 + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: bubbleContent.width - (index + 1) * 40 + Layout.preferredHeight: 50 + radius: 18 + opacity: 0.8 + z: 1 - bubble.level - (index + 1) + } + } + } + + z: bubble.level <= 1 ? 0 : 1 - bubble.level + + background: Rectangle { + implicitWidth: 600 + radius: 18 + opacity: 1 + color: "transparent" + } +} diff --git a/panels/notification/package/main.qml b/panels/notification/package/main.qml new file mode 100644 index 000000000..996fe3cab --- /dev/null +++ b/panels/notification/package/main.qml @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Window 2.15 +import QtQuick.Layouts 1.15 + +import org.deepin.ds 1.0 +import org.deepin.dtk 1.0 as D + +Window { + id: root + visible: Applet.visible + width: 600 + height: Math.max(10, bubbleView.height) + DLayerShellWindow.topMargin: 10 + DLayerShellWindow.leftMargin: 800 + DLayerShellWindow.layer: DLayerShellWindow.LayerOverlay + DLayerShellWindow.anchors: DLayerShellWindow.AnchorTop + + ListView { + id: bubbleView + width: root.width + height: contentHeight + spacing: 10 + model: Applet.bubbles + interactive: false + + add: Transition { + NumberAnimation { properties: "y"; from: -100; duration: 500 } + } + delegate: Bubble { + bubble: model + } + } + color: "transparent" +} diff --git a/panels/notification/package/metadata.json b/panels/notification/package/metadata.json new file mode 100644 index 000000000..35e7889d7 --- /dev/null +++ b/panels/notification/package/metadata.json @@ -0,0 +1,8 @@ +{ + "Plugin": { + "Version": "1.0", + "Id": "org.deepin.ds.notification", + "Url": "main.qml", + "ContainmentType": "Panel" + } +} diff --git a/panels/notification/test.sh b/panels/notification/test.sh new file mode 100755 index 000000000..8e024d4cb --- /dev/null +++ b/panels/notification/test.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +#busctl --user monitor org.deepin.dde.Notification1 + +#dbus-send --session --type=method_call --dest=org.deepin.dde.Notification1 /org/deepin/dde/Notification1 org.deepin.dde.Notification1.ReplaceBubble boolean:false + +notify-send 1 -a dde-control-center -t 3000 +notify-send 2 -a dde-control-center -t 4000 +notify-send 3 -a dde-control-center -t 5000 +notify-send 4 -a dde-control-center -t 5000 + +notify-send 3 -a element + +sleep 3 + +notify-send "Memory Consumption (%):" "$(ps axch -o cmd:15,%mem --sort=-%mem | head -n 30)" + +notify-send "long body long body long body long body long body long body long body long body long body" "summary" + +dunstify -a dde-control-center -i dde-control-center body summary + +NOTIFICATION_ID=$(dunstify -p -a dde-control-center -i dde-control-center body summary) +dunstify -a dde-control-center -i dde-control-center body summary-replace -r ${NOTIFICATION_ID}