From eb77f89df4a0455506056f7c80f077a7fcce4b1f Mon Sep 17 00:00:00 2001 From: zhangkun Date: Sat, 14 Mar 2026 14:33:48 +0800 Subject: [PATCH] feat: add hover protection for notification bubbles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added bubble ID tracking to prevent premature closure when hovering over notifications. The BubbleModel now exposes bubbleId role for QML access. When a bubble is hovered, its ID is passed to the notification server which blocks timeout-based closure for that specific bubble. This prevents notifications from disappearing while users are interacting with them. Log: Notification bubbles now remain visible when hovered over, preventing accidental closure feat: 为通知气泡添加悬停保护功能 在 BubbleModel 中添加了 bubbleId 角色供 QML 访问。当气泡被悬停时,其 ID 会传递给通知服务器,服务器会阻止该特定气泡的超时关闭。这防止了用户与通知 交互时通知意外消失的问题。 Log: 通知气泡在悬停时保持可见,防止意外关闭 PMS: BUG-352577 --- panels/notification/bubble/bubblemodel.cpp | 31 ----------------- panels/notification/bubble/bubblemodel.h | 10 ------ panels/notification/bubble/bubblepanel.cpp | 4 +++ panels/notification/bubble/bubblepanel.h | 3 +- panels/notification/bubble/package/Bubble.qml | 8 ----- panels/notification/bubble/package/main.qml | 18 ++++++++++ .../server/notificationmanager.cpp | 33 ++++++++++++++++++- .../notification/server/notificationmanager.h | 4 ++- .../server/notifyserverapplet.cpp | 7 +++- .../notification/server/notifyserverapplet.h | 3 +- 10 files changed, 67 insertions(+), 54 deletions(-) diff --git a/panels/notification/bubble/bubblemodel.cpp b/panels/notification/bubble/bubblemodel.cpp index 1bcc6ee82..ef8682033 100644 --- a/panels/notification/bubble/bubblemodel.cpp +++ b/panels/notification/bubble/bubblemodel.cpp @@ -82,8 +82,6 @@ void BubbleModel::clear() qDeleteAll(m_bubbles); m_bubbles.clear(); endResetModel(); - m_delayBubbles.clear(); - m_delayRemovedBubble = NotifyEntity::InvalidId; updateLevel(); m_updateTimeTipTimer->stop(); @@ -127,16 +125,8 @@ void BubbleModel::remove(const BubbleItem *bubble) BubbleItem *BubbleModel::removeById(qint64 id) { - if (id == m_delayRemovedBubble) { - // Delayed remove - if (!m_delayBubbles.contains(id)) { - m_delayBubbles.append(id); - } - return nullptr; - } for (const auto &item : m_bubbles) { if (item->id() == id) { - m_delayBubbles.removeAll(id); remove(m_bubbles.indexOf(item)); return item; } @@ -252,27 +242,6 @@ void BubbleModel::updateBubbleCount(int count) updateLevel(); } -qint64 BubbleModel::delayRemovedBubble() const -{ - return m_delayRemovedBubble; -} - -void BubbleModel::setDelayRemovedBubble(qint64 newDelayRemovedBubble) -{ - if (m_delayRemovedBubble == newDelayRemovedBubble) - return; - const auto oldDelayRemovedBubble = m_delayRemovedBubble; - if (m_delayBubbles.contains(oldDelayRemovedBubble)) { - // Remove last delayed bubble. - QTimer::singleShot(DelayRemovBubbleTime, this, [this, oldDelayRemovedBubble]() { - removeById(oldDelayRemovedBubble); - }); - } - - m_delayRemovedBubble = newDelayRemovedBubble; - emit delayRemovedBubbleChanged(); -} - void BubbleModel::clearInvalidBubbles() { for (int i = m_bubbles.count() - 1; i >= 0; i--) { diff --git a/panels/notification/bubble/bubblemodel.h b/panels/notification/bubble/bubblemodel.h index 5977775ff..24891c97f 100644 --- a/panels/notification/bubble/bubblemodel.h +++ b/panels/notification/bubble/bubblemodel.h @@ -17,7 +17,6 @@ class BubbleItem; class BubbleModel : public QAbstractListModel { Q_OBJECT - Q_PROPERTY(qint64 delayRemovedBubble READ delayRemovedBubble WRITE setDelayRemovedBubble NOTIFY delayRemovedBubbleChanged FINAL) public: enum { AppName = Qt::UserRole + 1, @@ -61,14 +60,8 @@ class BubbleModel : public QAbstractListModel int displayRowCount() const; int overlayCount() const; - qint64 delayRemovedBubble() const; - void setDelayRemovedBubble(qint64 newDelayRemovedBubble); - void clearInvalidBubbles(); -signals: - void delayRemovedBubbleChanged(); - private: void updateBubbleCount(int count); int replaceBubbleIndex(const BubbleItem *bubble) const; @@ -82,9 +75,6 @@ class BubbleModel : public QAbstractListModel int BubbleMaxCount{3}; int m_contentRowCount{6}; const int OverlayMaxCount{2}; - QList m_delayBubbles; - qint64 m_delayRemovedBubble{NotifyEntity::InvalidId}; - const int DelayRemovBubbleTime{1000}; }; } diff --git a/panels/notification/bubble/bubblepanel.cpp b/panels/notification/bubble/bubblepanel.cpp index 8c0edc912..df180214b 100644 --- a/panels/notification/bubble/bubblepanel.cpp +++ b/panels/notification/bubble/bubblepanel.cpp @@ -206,6 +206,10 @@ void BubblePanel::setEnabled(bool newEnabled) setVisible(!isEmpty && enabled()); } +void BubblePanel::setHoveredId(qint64 id) +{ + QMetaObject::invokeMethod(m_notificationServer, "setBlockClosedId", Qt::DirectConnection, Q_ARG(qint64, id)); +} } #include "bubblepanel.moc" diff --git a/panels/notification/bubble/bubblepanel.h b/panels/notification/bubble/bubblepanel.h index 5aa84a528..c6770220f 100644 --- a/panels/notification/bubble/bubblepanel.h +++ b/panels/notification/bubble/bubblepanel.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,6 +39,7 @@ public Q_SLOTS: void close(int bubbleIndex, int reason); void delayProcess(int bubbleIndex); void setEnabled(bool newEnabled); + void setHoveredId(qint64 id); Q_SIGNALS: void visibleChanged(); diff --git a/panels/notification/bubble/package/Bubble.qml b/panels/notification/bubble/package/Bubble.qml index 10f3e3875..bad3bb46e 100644 --- a/panels/notification/bubble/package/Bubble.qml +++ b/panels/notification/bubble/package/Bubble.qml @@ -12,14 +12,6 @@ Control { id: control height: loader.height property var bubble - onHoveredChanged: function () { - if (control.hovered) { - Applet.bubbles.delayRemovedBubble = bubble.id - } else { - Applet.bubbles.delayRemovedBubble = 0 - } - } - Loader { id: loader width: control.width diff --git a/panels/notification/bubble/package/main.qml b/panels/notification/bubble/package/main.qml index 47a0e7de7..d25dabf91 100644 --- a/panels/notification/bubble/package/main.qml +++ b/panels/notification/bubble/package/main.qml @@ -132,5 +132,23 @@ Window { width: 360 bubble: model } + + HoverHandler { + onPointChanged: { + const local = point.position + let hoveredItem = bubbleView.itemAt(local.x, local.y) + if (hoveredItem && hoveredItem.bubble) { + Applet.setHoveredId(hoveredItem.bubble.id) + } else { + Applet.setHoveredId(0) + } + } + + onHoveredChanged: { + if (!hovered) { + Applet.setHoveredId(0) + } + } + } } } diff --git a/panels/notification/server/notificationmanager.cpp b/panels/notification/server/notificationmanager.cpp index ab2b9d313..ce3d16f94 100644 --- a/panels/notification/server/notificationmanager.cpp +++ b/panels/notification/server/notificationmanager.cpp @@ -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 @@ -37,6 +37,7 @@ namespace notification { static const uint NoReplacesId = 0; static const int DefaultTimeOutMSecs = 5000; +static const int BlockItemTimeout = 1000; static const QString NotificationsDBusService = "org.freedesktop.Notifications"; static const QString NotificationsDBusPath = "/org/freedesktop/Notifications"; static const QString DDENotifyDBusServer = "org.deepin.dde.Notification1"; @@ -347,6 +348,30 @@ QVariant NotificationManager::GetSystemInfo(uint configItem) return m_setting->systemValue(static_cast(configItem)); } +void NotificationManager::setBlockClosedId(qint64 id) +{ + if (id == m_blockClosedId) { + return; + } + + if(m_blockClosedId != NotifyEntity::InvalidId) { + auto findIter = std::find_if(m_pendingTimeoutEntities.begin(), m_pendingTimeoutEntities.end(), [this](const NotifyEntity &entity) { + return entity.id() == m_blockClosedId; + }); + + const auto current = QDateTime::currentMSecsSinceEpoch(); + if (findIter != m_pendingTimeoutEntities.end()) { + if (current > findIter.key() - BlockItemTimeout) { + qDebug(notifyLog) << "Delay close bubble id:" << m_blockClosedId << "for the new block bubble id:" << id; + m_pendingTimeoutEntities.insert(current + BlockItemTimeout, findIter.value()); + m_pendingTimeoutEntities.erase(findIter); + } + } + } + m_blockClosedId = id; + onHandingPendingEntities(); +} + bool NotificationManager::isDoNotDisturb() const { if (!m_setting->systemValue(NotificationSetting::DNDMode).toBool()) @@ -673,6 +698,12 @@ void NotificationManager::onHandingPendingEntities() continue; } + if (item.id() == m_blockClosedId) { + qDebug(notifyLog) << "bubble id:" << item.bubbleId() << "entity id:" << item.id(); + m_pendingTimeoutEntities.insert(current, item); + continue; + } + qDebug(notifyLog) << "Expired for the notification " << item.id() << item.appName(); notificationClosed(item.id(), item.bubbleId(), NotifyEntity::Expired); } diff --git a/panels/notification/server/notificationmanager.h b/panels/notification/server/notificationmanager.h index 2bd838e87..790ab7f59 100644 --- a/panels/notification/server/notificationmanager.h +++ b/panels/notification/server/notificationmanager.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 @@ -65,6 +65,7 @@ public Q_SLOTS: void SetSystemInfo(uint configItem, const QVariant &value); QVariant GetSystemInfo(uint configItem); + void setBlockClosedId(qint64 id); private: bool isDoNotDisturb() const; bool recordNotification(NotifyEntity &entity); @@ -97,6 +98,7 @@ private slots: QStringList m_systemApps; QMap m_appNamesMap; int m_cleanupDays = 7; + qint64 m_blockClosedId = 0; }; } // notification diff --git a/panels/notification/server/notifyserverapplet.cpp b/panels/notification/server/notifyserverapplet.cpp index fb81829b5..b4de43dfc 100644 --- a/panels/notification/server/notifyserverapplet.cpp +++ b/panels/notification/server/notifyserverapplet.cpp @@ -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 @@ -104,6 +104,11 @@ void NotifyServerApplet::removeExpiredNotifications() m_manager->removeExpiredNotifications(); } +void NotifyServerApplet::setBlockClosedId(qint64 id) +{ + m_manager->setBlockClosedId(id); +} + D_APPLET_CLASS(NotifyServerApplet) } diff --git a/panels/notification/server/notifyserverapplet.h b/panels/notification/server/notifyserverapplet.h index 9704b3231..20975e91d 100644 --- a/panels/notification/server/notifyserverapplet.h +++ b/panels/notification/server/notifyserverapplet.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 @@ -31,6 +31,7 @@ public Q_SLOTS: void removeNotifications(const QString &appName); void removeNotifications(); void removeExpiredNotifications(); + void setBlockClosedId(qint64 id); private: NotificationManager *m_manager = nullptr;