From 938f892bec6c6713654d8cd4f5e4fd9c694ea62c Mon Sep 17 00:00:00 2001 From: zhangkun Date: Tue, 21 Apr 2026 20:18:13 +0800 Subject: [PATCH] refactor(notification): move bubble overlay logic to QML delegate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Remove level property and overlay count tracking from BubbleItem and BubbleModel 2. Introduce BubbleDelegate.qml with visual folding effects (scale, y-offset, opacity) based on index 3. Simplify BubbleModel to use m_maxKeep limit instead of separate display/overlay counts 4. Add remove and removeDisplaced transitions for smooth disappearance animations 5. Consolidate NormalBubble.qml and OverlayBubble.qml into single Bubble.qml component 6. Add delayed hide (500ms) in BubblePanel to allow QML animations to complete Log: Refactor notification bubble system to handle overlay visual effects in QML instead of C++ model refactor(notification): 将气泡折叠逻辑移至 QML 代理组件 1. 移除 BubbleItem 和 BubbleModel 中的 level 属性及折叠计数追踪 2. 新增 BubbleDelegate.qml,根据索引实现视觉折叠效果(缩放、Y 偏移、透明度) 3. 简化 BubbleModel,使用 m_maxKeep 限制替代分离的显示/折叠计数 4. 添加 remove 和 removeDisplaced 过渡动画实现平滑消失效果 5. 合并 NormalBubble.qml 和 OverlayBubble.qml 为单一 Bubble.qml 组件 6. BubblePanel 添加 500ms 延迟隐藏,确保 QML 动画播放完成 Log: 重构通知气泡系统,将折叠视觉效果从 C++ 模型移至 QML 处理 PMS: BUG-355029 --- debian/control | 1 + frame/CMakeLists.txt | 2 +- frame/layershell/dlayershellwindow.cpp | 24 ++++ frame/layershell/dlayershellwindow.h | 8 ++ .../layershell/qwaylandlayershellsurface.cpp | 9 +- frame/layershell/x11dlayershellemulation.cpp | 39 +++++++ frame/layershell/x11dlayershellemulation.h | 3 +- panels/notification/bubble/bubbleitem.cpp | 13 --- panels/notification/bubble/bubbleitem.h | 6 +- panels/notification/bubble/bubblemodel.cpp | 107 ++++++++---------- panels/notification/bubble/bubblemodel.h | 14 +-- panels/notification/bubble/bubblepanel.cpp | 11 +- panels/notification/bubble/package/Bubble.qml | 39 ++++--- .../bubble/package/BubbleDelegate.qml | 52 +++++++++ .../bubble/package/NormalBubble.qml | 39 ------- .../bubble/package/OverlayBubble.qml | 41 ------- panels/notification/bubble/package/main.qml | 51 +++++++-- 17 files changed, 268 insertions(+), 191 deletions(-) create mode 100644 panels/notification/bubble/package/BubbleDelegate.qml delete mode 100644 panels/notification/bubble/package/NormalBubble.qml delete mode 100644 panels/notification/bubble/package/OverlayBubble.qml diff --git a/debian/control b/debian/control index 2cc47da58..e25d4c52a 100644 --- a/debian/control +++ b/debian/control @@ -22,6 +22,7 @@ Build-Depends: libxcb-res0-dev, libxcb-util-dev, libxcb1-dev, + libxcb-shape0-dev, libxtst-dev, libyaml-cpp-dev, qml6-module-qtquick-controls2-styles-chameleon, diff --git a/frame/CMakeLists.txt b/frame/CMakeLists.txt index 195a1dc3e..1bfc03365 100644 --- a/frame/CMakeLists.txt +++ b/frame/CMakeLists.txt @@ -118,7 +118,7 @@ target_link_directories(dde-shell-frame INTERFACE if (BUILD_WITH_X11) target_compile_definitions(dde-shell-frame PRIVATE BUILD_WITH_X11) - pkg_check_modules(XCB REQUIRED IMPORTED_TARGET xcb-ewmh xcb-icccm xtst x11) + pkg_check_modules(XCB REQUIRED IMPORTED_TARGET xcb-ewmh xcb-icccm xtst xcb-shape x11) target_sources(dde-shell-frame PRIVATE layershell/x11dlayershellemulation.h layershell/x11dlayershellemulation.cpp) target_link_libraries(dde-shell-frame PRIVATE PkgConfig::XCB) endif(BUILD_WITH_X11) diff --git a/frame/layershell/dlayershellwindow.cpp b/frame/layershell/dlayershellwindow.cpp index 3149e005c..934e91133 100644 --- a/frame/layershell/dlayershellwindow.cpp +++ b/frame/layershell/dlayershellwindow.cpp @@ -43,6 +43,7 @@ class DLayerShellWindowPrivate int preferredWidth = -1; int preferredHeight = -1; bool closeOnDismissed = true; + QRegion inputRegion; }; void DLayerShellWindow::setAnchors(DLayerShellWindow::Anchors anchors) @@ -192,6 +193,29 @@ int DLayerShellWindow::preferredHeight() const return d->preferredHeight; } +void DLayerShellWindow::setInputRegion(const QRegion ®ion) +{ + if (d->inputRegion != region) { + d->inputRegion = region; + Q_EMIT inputRegionChanged(); + } +} + +void DLayerShellWindow::resetInputRegion() +{ + setInputRegion(QRegion()); +} + +QRegion DLayerShellWindow::inputRegion() const +{ + return d->inputRegion; +} + +void DLayerShellWindow::setInputRegionRect(int x, int y, int width, int height) +{ + setInputRegion(QRegion(x, y, width, height)); +} + bool DLayerShellWindow::closeOnDismissed() const { return d->closeOnDismissed; diff --git a/frame/layershell/dlayershellwindow.h b/frame/layershell/dlayershellwindow.h index 1b77ac777..578e94ed2 100644 --- a/frame/layershell/dlayershellwindow.h +++ b/frame/layershell/dlayershellwindow.h @@ -30,6 +30,7 @@ class DS_SHARE DLayerShellWindow : public QObject Q_PROPERTY(ScreenConfiguration screenConfiguration READ screenConfiguration WRITE setScreenConfiguration) Q_PROPERTY(int preferredWidth READ preferredWidth WRITE setPreferredWidth RESET resetPreferredWidth NOTIFY geometryHintsChanged) Q_PROPERTY(int preferredHeight READ preferredHeight WRITE setPreferredHeight RESET resetPreferredHeight NOTIFY geometryHintsChanged) + Q_PROPERTY(QRegion inputRegion READ inputRegion WRITE setInputRegion RESET resetInputRegion NOTIFY inputRegionChanged) Q_PROPERTY(bool closeOnDismissed READ closeOnDismissed WRITE setCloseOnDismissed) @@ -121,6 +122,12 @@ class DS_SHARE DLayerShellWindow : public QObject void resetPreferredHeight(); int preferredHeight() const; + void setInputRegion(const QRegion ®ion); + void resetInputRegion(); + QRegion inputRegion() const; + + Q_INVOKABLE void setInputRegionRect(int x, int y, int width, int height); + /** * Sets a string based identifier for this window. * This may be used by a compositor to determine stacking @@ -156,6 +163,7 @@ class DS_SHARE DLayerShellWindow : public QObject void layerChanged(); void scopeChanged(); void geometryHintsChanged(); + void inputRegionChanged(); private: DLayerShellWindow(QWindow* window); diff --git a/frame/layershell/qwaylandlayershellsurface.cpp b/frame/layershell/qwaylandlayershellsurface.cpp index 8731d7d15..2de637ba8 100644 --- a/frame/layershell/qwaylandlayershellsurface.cpp +++ b/frame/layershell/qwaylandlayershellsurface.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -75,6 +75,13 @@ QWaylandLayerShellSurface::QWaylandLayerShellSurface(QtWayland::zwlr_layer_shell window->waylandSurface()->commit(); }); + auto applyInputRegion = [this, window]() { + window->window()->setMask(m_dlayerShellWindow->inputRegion()); + window->waylandSurface()->commit(); + }; + + connect(m_dlayerShellWindow, &DLayerShellWindow::inputRegionChanged, this, applyInputRegion); + calcAndSetRequestSize(window->surfaceSize()); if (m_requestSize.isValid()) { diff --git a/frame/layershell/x11dlayershellemulation.cpp b/frame/layershell/x11dlayershellemulation.cpp index c7ae84ce4..08d6f76ba 100644 --- a/frame/layershell/x11dlayershellemulation.cpp +++ b/frame/layershell/x11dlayershellemulation.cpp @@ -17,6 +17,7 @@ #include #include #include +#include DS_BEGIN_NAMESPACE @@ -71,6 +72,9 @@ LayerShellEmulation::LayerShellEmulation(QWindow* window, QObject *parent) onScopeChanged(); connect(m_dlayerShellWindow, &DLayerShellWindow::scopeChanged, this, &LayerShellEmulation::onScopeChanged); + onInputRegionChanged(); + connect(m_dlayerShellWindow, &DLayerShellWindow::inputRegionChanged, this, &LayerShellEmulation::onInputRegionChanged); + // connect(m_dlayerShellWindow, &DS_NAMESPACE::DLayerShellWindow::keyboardInteractivityChanged, this, &LayerShellEmulation::onKeyboardInteractivityChanged); } @@ -321,6 +325,41 @@ void LayerShellEmulation::onScopeChanged() qCDebug(layershell) << "Set WM_CLASS for window" << m_window->winId() << " wm_class:" << wmClassData; } +void LayerShellEmulation::onInputRegionChanged() +{ + auto *x11Application = qGuiApp->nativeInterface(); + if (!x11Application || !m_window->winId() || !m_dlayerShellWindow) { + return; + } + + if (m_dlayerShellWindow->inputRegion().isNull()) { + // Reset input region (no shape constraint) + xcb_shape_mask(x11Application->connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, m_window->winId(), 0, 0, XCB_NONE); + xcb_flush(x11Application->connection()); + return; + } + + QRegion region = m_dlayerShellWindow->inputRegion(); + qreal scaleFactor = qGuiApp->devicePixelRatio(); + + QVector rects; + for (const QRect &r : region) { + xcb_rectangle_t rect; + rect.x = r.x() * scaleFactor; + rect.y = r.y() * scaleFactor; + rect.width = r.width() * scaleFactor; + rect.height = r.height() * scaleFactor; + rects.append(rect); + } + + // Set the input shape via XCB + // If rects vector is empty, the window will become completely transparent to input clicks (unclickable) + xcb_shape_rectangles(x11Application->connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, + XCB_CLIP_ORDERING_UNSORTED, m_window->winId(), 0, 0, + rects.size(), rects.data()); + xcb_flush(x11Application->connection()); +} + // void X11Emulation::onKeyboardInteractivityChanged() // { // // kwin no implentation on wayland diff --git a/frame/layershell/x11dlayershellemulation.h b/frame/layershell/x11dlayershellemulation.h index 72aec4d9e..5814f7b8b 100644 --- a/frame/layershell/x11dlayershellemulation.h +++ b/frame/layershell/x11dlayershellemulation.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -28,6 +28,7 @@ private slots: void onPositionChanged(); void onExclusionZoneChanged(); void onScopeChanged(); + void onInputRegionChanged(); // void onKeyboardInteractivityChanged(); private: diff --git a/panels/notification/bubble/bubbleitem.cpp b/panels/notification/bubble/bubbleitem.cpp index a70693c0e..530db1ddc 100644 --- a/panels/notification/bubble/bubbleitem.cpp +++ b/panels/notification/bubble/bubbleitem.cpp @@ -282,19 +282,6 @@ void BubbleItem::updateActions() m_actions = array; } -int BubbleItem::level() const -{ - return m_level; -} - -void BubbleItem::setLevel(int level) -{ - if (m_level == level) - return; - - m_level = level; - emit levelChanged(); -} QString BubbleItem::timeTip() const { diff --git a/panels/notification/bubble/bubbleitem.h b/panels/notification/bubble/bubbleitem.h index ac4cb8d38..1b28f0aa8 100644 --- a/panels/notification/bubble/bubbleitem.h +++ b/panels/notification/bubble/bubbleitem.h @@ -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 @@ -39,8 +39,6 @@ class BubbleItem : public QObject QVariantList actions() const; void updateActions(); - int level() const; - void setLevel(int level); QString timeTip() const; void setTimeTip(const QString &timeTip); @@ -51,7 +49,6 @@ class BubbleItem : public QObject bool isValid() const; signals: - void levelChanged(); void timeTipChanged(); private: @@ -59,7 +56,6 @@ class BubbleItem : public QObject private: NotifyEntity m_entity; - int m_level = 0; int m_urgency = NotifyEntity::Normal; QString m_timeTip; bool m_enablePreview = true; diff --git a/panels/notification/bubble/bubblemodel.cpp b/panels/notification/bubble/bubblemodel.cpp index 455161497..fa0714f42 100644 --- a/panels/notification/bubble/bubblemodel.cpp +++ b/panels/notification/bubble/bubblemodel.cpp @@ -23,18 +23,33 @@ namespace notification { BubbleModel::BubbleModel(QObject *parent) : QAbstractListModel(parent) , m_updateTimeTipTimer(new QTimer(this)) + , m_processPendingTimer(new QTimer(this)) { m_updateTimeTipTimer->setInterval(1000); m_updateTimeTipTimer->setSingleShot(false); - BubbleMaxCount = NotifySetting::instance()->bubbleCount(); + + m_processPendingTimer->setInterval(300); + m_processPendingTimer->setSingleShot(true); + + m_maxKeep = NotifySetting::instance()->bubbleCount() + 2; // max keep folds. connect(m_updateTimeTipTimer, &QTimer::timeout, this, &BubbleModel::updateBubbleTimeTip); + connect(m_processPendingTimer, &QTimer::timeout, this, [this] { + if (!m_pendingBubbles.isEmpty()) { + auto bubble = m_pendingBubbles.dequeue(); + insertBubble(bubble); + m_processPendingTimer->start(); + } + }); + connect(NotifySetting::instance(), &NotifySetting::contentRowCountChanged, this, &BubbleModel::updateContentRowCount); connect(NotifySetting::instance(), &NotifySetting::bubbleCountChanged, this, &BubbleModel::updateBubbleCount); } BubbleModel::~BubbleModel() { + qDeleteAll(m_pendingBubbles); + m_pendingBubbles.clear(); qDeleteAll(m_bubbles); m_bubbles.clear(); } @@ -45,16 +60,28 @@ void BubbleModel::push(BubbleItem *bubble) m_updateTimeTipTimer->start(); } - bool more = displayRowCount() >= BubbleMaxCount; - if (more) { - beginRemoveRows(QModelIndex(), BubbleMaxCount - 1, BubbleMaxCount - 1); + if (m_processPendingTimer->isActive()) { + m_pendingBubbles.enqueue(bubble); + } else { + insertBubble(bubble); + m_processPendingTimer->start(); + } +} + +void BubbleModel::insertBubble(BubbleItem *bubble) +{ + // Retain only enough bubbles to show requested number + folded overlay space. + // This offloads disappearance smoothly to the QML remove transition. + if (m_bubbles.size() >= m_maxKeep) { + beginRemoveRows(QModelIndex(), m_bubbles.size() - 1, m_bubbles.size() - 1); + auto old = m_bubbles.takeLast(); + old->deleteLater(); endRemoveRows(); } + beginInsertRows(QModelIndex(), 0, 0); m_bubbles.prepend(bubble); endInsertRows(); - - updateLevel(); } bool BubbleModel::isReplaceBubble(const BubbleItem *bubble) const @@ -75,6 +102,12 @@ BubbleItem *BubbleModel::replaceBubble(BubbleItem *bubble) void BubbleModel::clear() { + if (m_processPendingTimer) { + m_processPendingTimer->stop(); + } + qDeleteAll(m_pendingBubbles); + m_pendingBubbles.clear(); + if (m_bubbles.count() <= 0) return; beginResetModel(); @@ -82,7 +115,6 @@ void BubbleModel::clear() m_bubbles.clear(); endResetModel(); - updateLevel(); m_updateTimeTipTimer->stop(); } @@ -96,22 +128,11 @@ void BubbleModel::remove(int index) if (index < 0 || index >= m_bubbles.size()) return; - if (index >= rowCount(QModelIndex())) { - auto bubble = m_bubbles.takeAt(index); - bubble->deleteLater(); - 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(const BubbleItem *bubble) @@ -165,16 +186,12 @@ QVariant BubbleModel::data(const QModelIndex &index, int role) const return m_bubbles[row]->summary(); case BubbleModel::IconName: return m_bubbles[row]->appIcon(); - case BubbleModel::Level: - return m_bubbles[row]->level(); case BubbleModel::CTime: return m_bubbles[row]->ctime(); case BubbleModel::TimeTip: return m_bubbles[row]->timeTip(); case BubbleModel::BodyImagePath: return m_bubbles[row]->bodyImagePath(); - case BubbleModel::OverlayCount: - return overlayCount(); case BubbleModel::DefaultAction: return m_bubbles[row]->defaultAction(); case BubbleModel::Actions: @@ -183,6 +200,8 @@ QVariant BubbleModel::data(const QModelIndex &index, int role) const return m_bubbles[row]->urgency(); case BubbleModel::ContentRowCount: return NotifySetting::instance()->contentRowCount(); + case BubbleModel::BubbleCount: + return NotifySetting::instance()->bubbleCount(); default: break; } @@ -197,48 +216,31 @@ QHash BubbleModel::roleNames() const mapRoleNames[BubbleModel::Body] = "body"; mapRoleNames[BubbleModel::Summary] = "summary"; mapRoleNames[BubbleModel::IconName] = "iconName"; - mapRoleNames[BubbleModel::Level] = "level"; mapRoleNames[BubbleModel::CTime] = "ctime"; mapRoleNames[BubbleModel::TimeTip] = "timeTip"; mapRoleNames[BubbleModel::Urgency] = "urgency"; mapRoleNames[BubbleModel::BodyImagePath] = "bodyImagePath"; - mapRoleNames[BubbleModel::OverlayCount] = "overlayCount"; mapRoleNames[BubbleModel::DefaultAction] = "defaultAction"; mapRoleNames[BubbleModel::Actions] = "actions"; mapRoleNames[BubbleModel::ContentRowCount] = "contentRowCount"; + mapRoleNames[BubbleModel::BubbleCount] = "bubbleCount"; return mapRoleNames; } int BubbleModel::displayRowCount() const { - return qMin(m_bubbles.count(), BubbleMaxCount); -} - -int BubbleModel::overlayCount() const -{ - return qMin(m_bubbles.count() - displayRowCount(), OverlayMaxCount); + return m_bubbles.count(); } void BubbleModel::updateBubbleCount(int count) { - if (count == BubbleMaxCount) - return; + m_maxKeep = count + 2; + // We don't dynamically add/remove based on setting here anymore + // to let QML handle Repeater logic fully mapped to the model size - int currentRowCount = rowCount(QModelIndex()); - - if (count < currentRowCount) { - beginRemoveRows(QModelIndex(), count, currentRowCount - 1); - endRemoveRows(); - } else if (count > currentRowCount) { - int maxInsertCount = std::min(count, (int)m_bubbles.size()); - beginInsertRows(QModelIndex(), currentRowCount, maxInsertCount - 1); - endInsertRows(); + if (!m_bubbles.isEmpty()) { + Q_EMIT dataChanged(index(0), index(m_bubbles.size() - 1), {BubbleModel::BubbleCount}); } - - BubbleMaxCount = count; - - layoutChanged(); - updateLevel(); } void BubbleModel::clearInvalidBubbles() @@ -267,19 +269,6 @@ int BubbleModel::replaceBubbleIndex(const BubbleItem *bubble) const return -1; } -void BubbleModel::updateLevel() -{ - if (m_bubbles.isEmpty()) - return; - - int lastBubbleMaxIndex = BubbleMaxCount - 1; - for (int i = 0; i < displayRowCount(); i++) { - auto item = m_bubbles.at(i); - item->setLevel(i == lastBubbleMaxIndex ? 1 + overlayCount() : 1); - } - Q_EMIT dataChanged(index(0), index(displayRowCount() - 1), {BubbleModel::Level}); -} - void BubbleModel::updateBubbleTimeTip() { if (m_bubbles.isEmpty()) { diff --git a/panels/notification/bubble/bubblemodel.h b/panels/notification/bubble/bubblemodel.h index 24891c97f..b9b8f6203 100644 --- a/panels/notification/bubble/bubblemodel.h +++ b/panels/notification/bubble/bubblemodel.h @@ -8,6 +8,7 @@ #include "notifyentity.h" #include +#include class QTimer; @@ -24,15 +25,14 @@ class BubbleModel : public QAbstractListModel Body, Summary, IconName, - Level, CTime, TimeTip, BodyImagePath, - OverlayCount, DefaultAction, Actions, Urgency, - ContentRowCount + ContentRowCount, + BubbleCount } BubbleRole; explicit BubbleModel(QObject *parent = nullptr); @@ -58,23 +58,23 @@ class BubbleModel : public QAbstractListModel QHash roleNames() const override; int displayRowCount() const; - int overlayCount() const; void clearInvalidBubbles(); private: + void insertBubble(BubbleItem *bubble); void updateBubbleCount(int count); int replaceBubbleIndex(const BubbleItem *bubble) const; - void updateLevel(); void updateBubbleTimeTip(); void updateContentRowCount(int rowCount); private: QTimer *m_updateTimeTipTimer = nullptr; + QTimer *m_processPendingTimer = nullptr; QList m_bubbles; - int BubbleMaxCount{3}; + QQueue m_pendingBubbles; + int m_maxKeep{5}; int m_contentRowCount{6}; - const int OverlayMaxCount{2}; }; } diff --git a/panels/notification/bubble/bubblepanel.cpp b/panels/notification/bubble/bubblepanel.cpp index df180214b..469fe67f7 100644 --- a/panels/notification/bubble/bubblepanel.cpp +++ b/panels/notification/bubble/bubblepanel.cpp @@ -111,7 +111,16 @@ void BubblePanel::onNotificationStateChanged(qint64 id, int processedType) void BubblePanel::onBubbleCountChanged() { bool isEmpty = m_bubbles->items().isEmpty(); - setVisible(!isEmpty && enabled()); + if (isEmpty) { + // 延迟发送 false ,好让 QML 中的 remove: Transition 有足够的时间播放完动画 + QTimer::singleShot(500, this, [this]() { + if (m_bubbles->items().isEmpty()) { + setVisible(false); + } + }); + } else { + setVisible(enabled()); + } } void BubblePanel::addBubble(qint64 id) diff --git a/panels/notification/bubble/package/Bubble.qml b/panels/notification/bubble/package/Bubble.qml index bad3bb46e..2019262f3 100644 --- a/panels/notification/bubble/package/Bubble.qml +++ b/panels/notification/bubble/package/Bubble.qml @@ -6,27 +6,34 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import org.deepin.ds 1.0 +import org.deepin.ds.notification 1.0 import org.deepin.dtk 1.0 as D -Control { +NotifyItemContent { id: control - height: loader.height property var bubble - Loader { - id: loader - width: control.width - sourceComponent: bubble.level <= 1 ? normalCom : overlayCom + + width: 360 + appName: bubble.appName + iconName: bubble.iconName + date: bubble.timeTip + actions: bubble.actions + defaultAction: bubble.defaultAction + title: bubble.summary + content: bubble.body + strongInteractive: bubble.urgency === 2 + contentIcon: bubble.bodyImagePath + contentRowCount: bubble.contentRowCount + onRemove: function () { + console.log("remove notify", bubble.appName) + Applet.close(bubble.index, NotifyItem.Closed) } - Component { - id: normalCom - NormalBubble { - bubble: control.bubble - } + onDismiss: function () { + console.log("dismiss notify", bubble.appName) + Applet.close(bubble.index, NotifyItem.Dismissed) } - Component { - id: overlayCom - OverlayBubble { - bubble: control.bubble - } + onActionInvoked: function (actionId) { + console.log("action notify", bubble.appName, actionId) + Applet.invokeAction(bubble.index, actionId) } } diff --git a/panels/notification/bubble/package/BubbleDelegate.qml b/panels/notification/bubble/package/BubbleDelegate.qml new file mode 100644 index 000000000..6c4ba6484 --- /dev/null +++ b/panels/notification/bubble/package/BubbleDelegate.qml @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Item { + id: delegateRoot + width: 360 + property var bubble: model + property int maxCount: 3 + // ListView 的 remove 动画执行的时候,remove Item的index会以负数的方式出现 + property int realIndex: index < 0 ? ListView.view.count + index : index; + + height: bubbleContent.height + z: -realIndex + Bubble { + id: bubbleContent + width: 360 + bubble: delegateRoot.bubble + + transformOrigin: Item.Top + + y: { + // normal bubble dont need to move + if (realIndex < delegateRoot.maxCount) + return 0 + + let spacing = 10 + let peekAmount = 8 + // 根据 realIndex 计算出超出部分的折叠层数(最多折叠3层,再多层保留为了动画淡出) + let levelsFolded = Math.min(realIndex - (delegateRoot.maxCount - 1), 3) + let ret = levelsFolded * (delegateRoot.height + spacing - peekAmount) + return ret + } + + scale: { + if (realIndex < delegateRoot.maxCount) + return 1.0 + + let levelsFolded = Math.min(realIndex - (delegateRoot.maxCount - 1), 3) + return 1.0 - levelsFolded * 0.05 + } + + opacity: realIndex >= (delegateRoot.maxCount + 2) ? 0 : 1.0 + + Behavior on y { NumberAnimation { duration: 600; easing.type: Easing.OutExpo } } + Behavior on scale { NumberAnimation { duration: 600; easing.type: Easing.OutExpo } } + Behavior on opacity { NumberAnimation { duration: 600; easing.type: Easing.OutExpo } } + } +} diff --git a/panels/notification/bubble/package/NormalBubble.qml b/panels/notification/bubble/package/NormalBubble.qml deleted file mode 100644 index 2cbfe81b3..000000000 --- a/panels/notification/bubble/package/NormalBubble.qml +++ /dev/null @@ -1,39 +0,0 @@ -// 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.ds.notification 1.0 -import org.deepin.dtk 1.0 as D - -NotifyItemContent { - id: control - property var bubble - - width: 360 - appName: bubble.appName - iconName: bubble.iconName - date: bubble.timeTip - actions: bubble.actions - defaultAction: bubble.defaultAction - title: bubble.summary - content: bubble.body - strongInteractive: bubble.urgency === 2 - contentIcon: bubble.bodyImagePath - contentRowCount: bubble.contentRowCount - onRemove: function () { - console.log("remove notify", bubble.appName) - Applet.close(bubble.index, NotifyItem.Closed) - } - onDismiss: function () { - console.log("dismiss notify", bubble.appName) - Applet.close(bubble.index, NotifyItem.Dismissed) - } - onActionInvoked: function (actionId) { - console.log("action notify", bubble.appName, actionId) - Applet.invokeAction(bubble.index, actionId) - } -} diff --git a/panels/notification/bubble/package/OverlayBubble.qml b/panels/notification/bubble/package/OverlayBubble.qml deleted file mode 100644 index 500066be5..000000000 --- a/panels/notification/bubble/package/OverlayBubble.qml +++ /dev/null @@ -1,41 +0,0 @@ -// 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.ds.notification 1.0 -import org.deepin.dtk 1.0 as D - -Item { - id: control - property var bubble - readonly property int radius: 12 - - height: bubbleContent.height + indicator.height - OverlapIndicator { - id: indicator - width: parent.width - count: bubble.level - 1 - revert: true - anchors { - top: parent.top - left: parent.left - leftMargin: radius - right: parent.right - rightMargin: radius - } - } - - NormalBubble { - id: bubbleContent - width: parent.width - bubble: control.bubble - anchors { - top: indicator.bottom - } - } -} diff --git a/panels/notification/bubble/package/main.qml b/panels/notification/bubble/package/main.qml index 18dd17787..426576743 100644 --- a/panels/notification/bubble/package/main.qml +++ b/panels/notification/bubble/package/main.qml @@ -68,8 +68,8 @@ Window { } visible: Applet.visible - DLayerShellWindow.preferredWidth: 390 - DLayerShellWindow.preferredHeight: Math.max(10, bubbleView.height + bubbleView.anchors.topMargin + bubbleView.anchors.bottomMargin) + width: 390 + height: root.screen.height DLayerShellWindow.layer: DLayerShellWindow.LayerOverlay DLayerShellWindow.anchors: DLayerShellWindow.AnchorBottom | DLayerShellWindow.AnchorRight DLayerShellWindow.topMargin: windowMargin(0) @@ -96,11 +96,22 @@ Window { anchors { right: parent.right bottom: parent.bottom - bottomMargin: 10 rightMargin: 10 - margins: 30 + bottomMargin: 10 } + function updateInputRegion() { + root.DLayerShellWindow.setInputRegionRect( + Math.ceil(bubbleView.x), + Math.ceil(bubbleView.y), + Math.ceil(bubbleView.width), + Math.ceil(Math.max(10, bubbleView.contentHeight)) + ) + } + onContentHeightChanged: updateInputRegion() + onHeightChanged: updateInputRegion() + onYChanged: updateInputRegion() + spacing: 10 model: Applet.bubbles interactive: false @@ -126,11 +137,37 @@ Window { duration: 600 easing.type: Easing.OutExpo } + PropertyAnimation { + target: addDisplacedTrans.ViewTransition.item + properties: "y" + duration: 600 + easing.type: Easing.OutExpo + } + } + + remove: Transition { + id: removeTrans + ParallelAnimation { + PropertyAnimation { + target: removeTrans.ViewTransition.item + property: "opacity" + to: 0 + duration: 400 + easing.type: Easing.OutCubic + } + } + } + + removeDisplaced: Transition { + PropertyAnimation { + properties: "opacity,y" + duration: 400 + easing.type: Easing.OutExpo + } } - delegate: Bubble { - width: 360 - bubble: model + delegate: BubbleDelegate { + maxCount: model.bubbleCount } HoverHandler {