Skip to content

feat: add hover protection for notification bubbles#1502

Open
mhduiy wants to merge 1 commit intolinuxdeepin:masterfrom
mhduiy:noti
Open

feat: add hover protection for notification bubbles#1502
mhduiy wants to merge 1 commit intolinuxdeepin:masterfrom
mhduiy:noti

Conversation

@mhduiy
Copy link
Contributor

@mhduiy mhduiy commented Mar 14, 2026

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

Summary by Sourcery

Add hover-based protection to notification bubbles to prevent them from closing while the user is interacting with them.

New Features:

  • Forward hovered bubble IDs from QML to the notification server so that specific notifications are protected from timeout-based closure while hovered.

Enhancements:

  • Extend notification timeout handling to defer expiration of the currently hovered notification bubble.

Chores:

  • Update SPDX copyright years for notification-related components to cover 2024–2026.

@deepin-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: mhduiy

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 14, 2026

Reviewer's Guide

Implements hover-based protection for notification bubbles by tracking the currently hovered bubble id from QML through the bubble panel and applet into the NotificationManager, which defers timeout-based closure for that specific notification entity while it is hovered, plus minor metadata updates.

Sequence diagram for hover-based notification bubble protection

sequenceDiagram
    actor User
    participant BubbleQml as Bubble_QML
    participant BubblePanel
    participant NotifyServerApplet
    participant NotificationManager
    participant TimeoutHandler as Timeout_processing

    User ->> BubbleQml: hover enter on bubble
    BubbleQml ->> BubblePanel: setHoveredId(bubble.id)
    BubblePanel ->> NotifyServerApplet: setBlockClosedId(id)
    NotifyServerApplet ->> NotificationManager: setBlockClosedId(id)
    NotificationManager ->> NotificationManager: update m_blockClosedId

    loop Existing blocked bubble handling
        TimeoutHandler ->> NotificationManager: setBlockClosedId(new_id)
        NotificationManager ->> NotificationManager: find previous m_blockClosedId in m_pendingTimeoutEntities
        alt previous blocked entity is pending and expired
            NotificationManager ->> NotificationManager: reinsert previous entity with current+1000ms
        end
        NotificationManager ->> NotificationManager: m_blockClosedId = new_id
        NotificationManager ->> NotificationManager: onHandingPendingEntities()
    end

    loop Periodic timeout processing
        TimeoutHandler ->> NotificationManager: onHandingPendingEntities()
        NotificationManager ->> NotificationManager: iterate m_pendingTimeoutEntities
        alt entity.id == m_blockClosedId
            NotificationManager ->> NotificationManager: reinsert entity with current+1000ms
        else entity expired and not hovered
            NotificationManager ->> NotificationManager: notificationClosed(entity.id, entity.bubbleId, Expired)
        end
    end
Loading

Updated class diagram for notification hover blocking

classDiagram
    class BubblePanel {
        +setEnabled(newEnabled bool) void
        +setHoveredId(id qint64) void
        +close(bubbleIndex int, reason int) void
        +delayProcess(bubbleIndex int) void
    }

    class NotifyServerApplet {
        +removeNotifications(appName QString) void
        +removeNotifications() void
        +removeExpiredNotifications() void
        +setBlockClosedId(id qint64) void
        -m_manager NotificationManager*
    }

    class NotificationManager {
        +SetSystemInfo(configItem uint, value QVariant) void
        +GetSystemInfo(configItem uint) QVariant
        +setBlockClosedId(id qint64) void
        -isDoNotDisturb() bool
        -recordNotification(entity NotifyEntity) bool
        -onHandingPendingEntities() void
        -m_blockClosedId qint64
    }

    class NotifyEntity {
        +id() qint64
        +bubbleId() qint64
        +appName() QString
        <<enum>> InvalidId
    }

    BubblePanel --> NotifyServerApplet : calls_setBlockClosedId
    NotifyServerApplet --> NotificationManager : forwards_setBlockClosedId
    NotificationManager ..> NotifyEntity : manages_pending_timeout_entities
Loading

File-Level Changes

Change Details Files
Add server-side tracking of a 'blocked from closing' notification id and use it to defer timeout-based closure while hovered.
  • Introduce setBlockClosedId slot in NotificationManager and store current blocked id in m_blockClosedId.
  • When a new block id is set, detect if the previously blocked entity is pending timeout and, if overdue, reschedule it 1s into the future instead of closing it.
  • In onHandingPendingEntities, skip closing entities whose id matches m_blockClosedId and requeue them 1s later to keep them alive while hovered.
panels/notification/server/notificationmanager.cpp
panels/notification/server/notificationmanager.h
Plumb hovered bubble id from QML bubble UI down to the notification manager so the server knows which bubble is currently protected from timeout.
  • Add setHoveredId slot to BubblePanel that forwards the given id to the notification server via QMetaObject::invokeMethod("setBlockClosedId").
  • Expose setBlockClosedId slot on NotifyServerApplet and delegate to underlying NotificationManager.
  • Update Bubble.qml hover handler to call Applet.setHoveredId with the current bubble id when hovered and 0 when unhovered, replacing the previous delayRemovedBubble mechanism.
panels/notification/bubble/bubblepanel.cpp
panels/notification/bubble/bubblepanel.h
panels/notification/server/notifyserverapplet.cpp
panels/notification/server/notifyserverapplet.h
panels/notification/bubble/package/Bubble.qml
Refresh SPDX copyright year ranges across touched notification-related files.
  • Update SPDX-FileCopyrightText year range from '2024' to '2024 - 2026' in all modified notification server, applet, and bubble panel files.
panels/notification/server/notificationmanager.cpp
panels/notification/server/notifyserverapplet.cpp
panels/notification/bubble/bubblepanel.cpp
panels/notification/server/notificationmanager.h
panels/notification/bubble/bubblepanel.h
panels/notification/server/notifyserverapplet.h

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In NotificationManager::setBlockClosedId, the std::find_if lambda compares against m_blockClosedId instead of the incoming id parameter, which means it will search using the old value rather than the new one and likely not behave as intended when changing the hovered bubble.
  • The std::find_if predicate in setBlockClosedId takes a const NotifyEntity & argument, but m_pendingTimeoutEntities appears to be a keyed container (e.g. QMap<qint64, NotifyEntity>), so the lambda parameter should be adjusted (or replaced with a simple iterator loop) to match the actual element type and avoid compilation or runtime issues when accessing entity.id() and findIter.key()/value().
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `NotificationManager::setBlockClosedId`, the `std::find_if` lambda compares against `m_blockClosedId` instead of the incoming `id` parameter, which means it will search using the old value rather than the new one and likely not behave as intended when changing the hovered bubble.
- The `std::find_if` predicate in `setBlockClosedId` takes a `const NotifyEntity &` argument, but `m_pendingTimeoutEntities` appears to be a keyed container (e.g. `QMap<qint64, NotifyEntity>`), so the lambda parameter should be adjusted (or replaced with a simple iterator loop) to match the actual element type and avoid compilation or runtime issues when accessing `entity.id()` and `findIter.key()/value()`.

## Individual Comments

### Comment 1
<location path="panels/notification/bubble/bubblepanel.cpp" line_range="209-212" />
<code_context>
     setVisible(!isEmpty && enabled());
 }

+void BubblePanel::setHoveredId(qint64 id)
+{
+    QMetaObject::invokeMethod(m_notificationServer, "setBlockClosedId", Qt::DirectConnection, Q_ARG(qint64, id));
+}
 }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using `Qt::DirectConnection` here could be unsafe if `m_notificationServer` lives in another thread.

`QMetaObject::invokeMethod` with `Qt::DirectConnection` will run `setBlockClosedId` in the caller’s thread. If `BubblePanel` and `m_notificationServer` ever end up in different threads, this will break Qt’s threading rules and can cause race conditions.

If cross-thread calls are possible (now or later), prefer the default connection type or `Qt::QueuedConnection`. If they are guaranteed to share a thread, either call the slot directly or add a short comment documenting that assumption to avoid future misuse.

```suggestion
void BubblePanel::setHoveredId(qint64 id)
{
    // Use a queued connection to be safe if BubblePanel and m_notificationServer live in different threads.
    // This avoids violating Qt's threading rules if this ever becomes a cross-thread call.
    QMetaObject::invokeMethod(
        m_notificationServer,
        "setBlockClosedId",
        Qt::QueuedConnection,
        Q_ARG(qint64, id));
}
```
</issue_to_address>

### Comment 2
<location path="panels/notification/server/notificationmanager.cpp" line_range="350" />
<code_context>
     return m_setting->systemValue(static_cast<NotificationSetting::SystemConfigItem>(configItem));
 }

+void NotificationManager::setBlockClosedId(qint64 id)
+{
+    auto findIter = std::find_if(m_pendingTimeoutEntities.begin(), m_pendingTimeoutEntities.end(), [this](const NotifyEntity &entity) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider clarifying which id is used in the timeout search and extracting the common 1000ms postponement logic into a helper to make the behavior more explicit and maintainable.

You can keep the feature as‑is but reduce the cognitive load a bit by (1) avoiding hidden state in the `find_if` lambda and (2) centralising the “postpone by N ms” logic.

### 1. Make it clear which id `find_if` is using

Right now the lambda closes over `this` and uses `m_blockClosedId` while the function takes a new `id` parameter. This is exactly the “which id is this using?” confusion the other reviewer mentioned.

You can make the intent explicit by capturing the previous id in a local and capturing that:

```cpp
void NotificationManager::setBlockClosedId(qint64 id)
{
    const auto previousBlockedId = m_blockClosedId;
    const auto current = QDateTime::currentMSecsSinceEpoch();

    auto findIter = std::find_if(m_pendingTimeoutEntities.begin(),
                                 m_pendingTimeoutEntities.end(),
                                 [previousBlockedId](const NotifyEntity &entity) {
                                     return entity.id() == previousBlockedId;
                                 });

    if (previousBlockedId != NotifyEntity::InvalidId
        && findIter != m_pendingTimeoutEntities.end()
        && current > findIter.key()) {

        qDebug(notifyLog) << "Block close bubble id:" << previousBlockedId
                          << "for the new block bubble id:" << id;

        m_pendingTimeoutEntities.insert(current + 1000, findIter.value());
        m_pendingTimeoutEntities.erase(findIter);
    }

    m_blockClosedId = id;
    onHandingPendingEntities();
}
```

This keeps all behaviour intact but removes the implicit dependency on mutable member state inside the lambda.

### 2. Centralise the “postpone by 1000ms” behaviour

You have the same “insert with `+ 1000`” logic in two places. A tiny helper (or local lambda) will make the intent obvious and avoid subtle divergence later:

```cpp
// Member helper (header + cpp) if you prefer:
void NotificationManager::postponeTimeout(const NotifyEntity &entity,
                                          qint64 baseTimeMs,
                                          qint64 delayMs)
{
    m_pendingTimeoutEntities.insert(baseTimeMs + delayMs, entity);
}
```

Use it in both call sites:

```cpp
void NotificationManager::setBlockClosedId(qint64 id)
{
    const auto previousBlockedId = m_blockClosedId;
    const auto current = QDateTime::currentMSecsSinceEpoch();

    auto findIter = std::find_if(m_pendingTimeoutEntities.begin(),
                                 m_pendingTimeoutEntities.end(),
                                 [previousBlockedId](const NotifyEntity &entity) {
                                     return entity.id() == previousBlockedId;
                                 });

    if (previousBlockedId != NotifyEntity::InvalidId
        && findIter != m_pendingTimeoutEntities.end()
        && current > findIter.key()) {

        qDebug(notifyLog) << "Block close bubble id:" << previousBlockedId
                          << "for the new block bubble id:" << id;

        postponeTimeout(findIter.value(), current, 1000);
        m_pendingTimeoutEntities.erase(findIter);
    }

    m_blockClosedId = id;
    onHandingPendingEntities();
}
```

```cpp
for (const auto &item : timeoutEntities) {
    if (!item.isValid()) {
        // ...
        continue;
    }

    if (item.id() == m_blockClosedId) {
        qDebug(notifyLog) << "bubble id:" << item.bubbleId()
                          << "entity id:" << item.id();
        postponeTimeout(item, current, 1000);
        continue;
    }

    qDebug(notifyLog) << "Expired for the notification " << item.id()
                      << item.appName();
    notificationClosed(item.id(), item.bubbleId(), NotifyEntity::Expired);
}
```

This keeps:

- the “reschedule previous blocked entity if already expired” behaviour, and  
- the “keep postponing the currently blocked entity by 1s on each timeout pass”

but makes the postponement rule explicit and removes duplicated “`current + 1000`” logic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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
@deepin-ci-robot
Copy link

deepin pr auto review

这段代码主要实现了通知气泡在鼠标悬停时暂停自动关闭的功能。以下是对代码的详细审查意见,包括语法逻辑、代码质量、代码性能和代码安全四个方面:

1. 语法逻辑

问题:NotificationManager::setBlockClosedId 中的逻辑错误
notificationmanager.cppsetBlockClosedId 函数中:

auto findIter = std::find_if(m_pendingTimeoutEntities.begin(), m_pendingTimeoutEntities.end(), [this](const NotifyEntity &entity) {
    return entity.id() == m_blockClosedId;
});

这里使用了 std::find_if,但 m_pendingTimeoutEntities 是一个 QMap(根据 inserterase 的用法推断)。QMap 的迭代器解引用后是 QPair<Key, Value>,而不是 NotifyEntity。此外,findIter.key()findIter.value() 的使用也证实了它是 QMap
std::find_if 遍历的是 map 的节点(pair),需要访问 .value() 才能获取 NotifyEntity

改进建议:
修改 lambda 表达式以正确处理 QMap 的迭代器,或者直接使用 QMap 的查找机制(如果 key 不是时间戳)。由于 key 是时间戳,无法通过 id 直接查找,因此必须遍历。

// 修改后的查找逻辑
auto findIter = std::find_if(m_pendingTimeoutEntities.begin(), m_pendingTimeoutEntities.end(), [this](const QMap<qint64, NotifyEntity>::iterator::value_type &item) {
    return item.value().id() == m_blockClosedId;
});

或者更简洁地使用 C++ 结构化绑定(如果编译器支持):

auto findIter = std::find_if(m_pendingTimeoutEntities.begin(), m_pendingTimeoutEntities.end(), [this](const auto &item) {
    return item.value().id() == m_blockClosedId;
});

2. 代码质量

问题 1:魔法数字
代码中出现了 10000 这样的魔法数字,降低了代码的可读性和可维护性。

改进建议:
定义常量或枚举来代替这些数字。

// 在类定义中或头文件中
static constexpr qint64 INVALID_ID = 0;
static constexpr qint64 HOVER_EXTENSION_MS = 1000;

问题 2:版权年份更新
代码中将版权年份从 2024 更新为 2024 - 2026。这通常是正确的做法,但需要确认是否确实有计划维护到 2026 年,或者这只是一个批量修改。如果是后者,建议只修改实际发生变更的文件。

3. 代码性能

问题:线性查找
setBlockClosedId 中,使用 std::find_ifQMap 进行线性查找,时间复杂度为 O(N)。如果通知数量很多,可能会影响性能。

改进建议:
考虑维护一个额外的 QHash<qint64, qint64>(ID -> Timestamp)来加速查找,或者重新设计数据结构。如果通知数量通常很少(例如少于 20 个),当前实现可能可以接受。

4. 代码安全

问题 1:线程安全
setBlockClosedId 通过 Qt::DirectConnection 调用,这意味着它在调用者的线程中执行。如果 m_notificationServer 属于不同的线程,且 m_pendingTimeoutEntities 不是线程安全的,这会导致数据竞争。

改进建议:

  • 确认 m_notificationServer 和调用者(QML 引擎)是否在同一线程。如果是,DirectConnection 是安全的。
  • 如果不在同一线程,应使用 Qt::QueuedConnection 并确保 m_pendingTimeoutEntities 的访问是互斥的(使用 QMutex)。

问题 2:潜在的空指针解引用
虽然代码中没有显式解引用空指针,但 m_notificationServerm_manager 的初始化需要确保在调用 setBlockClosedId 之前完成。

改进建议:
setBlockClosedId 中添加断言或空指针检查。

void BubblePanel::setHoveredId(qint64 id)
{
    if (m_notificationServer) {
        QMetaObject::invokeMethod(m_notificationServer, "setBlockClosedId", Qt::DirectConnection, Q_ARG(qint64, id));
    }
}

5. 其他建议

问题:日志输出
代码中有一些 qDebug 输出,这在调试时很有用,但在生产环境中可能会产生大量日志。

改进建议:
使用 qCDebug 并配合分类日志,以便在发布版本中禁用。

qCDebug(notifyLog) << "Block close bubble id:" << m_blockClosedId << "for the new block bubble id:" << id;

总结

这段代码实现了基本功能,但在逻辑正确性(std::find_if 的使用)、代码质量(魔法数字)、性能(线性查找)和安全性(线程安全)方面有改进空间。建议优先修复 std::find_if 的逻辑错误,并考虑添加空指针检查和线程安全措施。

@deepin-ci-robot
Copy link

deepin pr auto review

这段代码主要实现了通知气泡在鼠标悬停时阻止其自动关闭的功能,通过引入 setBlockClosedId 方法并在 NotificationManager 中管理待处理的超时实体来实现。

以下是对这段代码的详细审查,包括语法逻辑、代码质量、代码性能和代码安全方面的改进意见:

1. 语法逻辑与功能实现

审查点:setBlockClosedId 函数逻辑

notificationmanager.cppsetBlockClosedId 函数中:

void NotificationManager::setBlockClosedId(qint64 id)
{
    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(m_blockClosedId != NotifyEntity::InvalidId && findIter != m_pendingTimeoutEntities.end() && current > findIter.key()) {
        // ...
    }
    m_blockClosedId = id;
    onHandingPendingEntities();
}
  • 问题std::find_if 的使用方式与 m_pendingTimeoutEntities 的类型(推测为 QMap<qint64, NotifyEntity>)不匹配。
    • std::find_if 接受的是迭代器范围,并对范围内的元素(Value类型)应用谓词。
    • 如果 m_pendingTimeoutEntitiesQMap,其迭代器解引用后是 QPair(Key, Value)。谓词中 entity.id() 可能是正确的,但 findIter.key() 的使用暗示了 findIter 是一个 QMap 迭代器,这没问题。
    • 更严重的逻辑问题:代码意图是找到"上一个被阻塞的气泡"并更新其超时时间。但是,如果 onHandingPendingEntities 刚刚处理过该气泡(因为它超时了),它可能已经从 m_pendingTimeoutEntities 中移除了。此时 findIter 会等于 end()
    • 如果 findIter == end(),意味着旧的气泡已经因为超时被处理(关闭)了,此时就不应该再尝试更新它。目前的逻辑 if (findIter != ... end()) 处理了这种情况,这是好的。
    • 潜在 Bugcurrent > findIter.key() 这个条件。如果用户在气泡即将超时前(例如还剩 10ms)悬停,current 可能已经大于 findIter.key()。此时代码将其重新插入并增加 1000ms。但紧接着 m_blockClosedId = id 更新了 ID。如果 id 是新的气泡 ID,那么旧气泡的 ID 丢失,但它还在队列里。如果 id 是 0(鼠标移出),那么旧气泡被重新加入队列,但 m_blockClosedId 变为 0,这可能导致它下次被正常处理。逻辑稍显复杂,需要确保边界情况(快速切换悬停)处理正确。

审查点:onHandingPendingEntities 中的逻辑

if (item.id() == m_blockClosedId) {
    qDebug(notifyLog) << "bubble id:" << item.bubbleId() << "entity id:" << item.id();
    m_pendingTimeoutEntities.insert(current + 1000, item);
    continue;
}
  • 逻辑:当发现当前处理的实体 ID 等于被阻塞的 ID 时,将其超时时间推迟 1000ms。
  • 问题item 是从 m_pendingTimeoutEntities 中取出的,这里执行 insert 插入一个新的条目,然后 continue 跳过后续的关闭逻辑。
    • 内存/性能问题insert 会拷贝 NotifyEntity。如果 NotifyEntity 包含较多数据,频繁拷贝会影响性能。
    • 迭代器失效:虽然 Qt 的 QMap 在插入时通常不会导致迭代器失效(除非是同一个节点),但在遍历容器时修改容器结构(插入)通常是不良实践,尽管在这里是安全的(因为是 continue,且插入的是未来的 Key)。
    • 逻辑漏洞:如果用户一直悬停,这个气泡会每秒被重新插入一次。这会导致气泡在队列中不断"重生"。如果用户悬停了 10 秒,队列里会有多个该气泡的条目(如果之前的还没被清理)。虽然 ID 相同,但 Key(时间戳)不同。当鼠标移开(setBlockClosedId(0)),这些条目可能会依次触发关闭逻辑,导致多次尝试关闭同一个通知。

2. 代码质量

  • 魔法数字:代码中出现了 current + 1000。建议将其定义为常量,例如 const qint64 HOVER_EXTENSION_TIME = 1000;,增加代码可读性。
  • 日志输出qDebug 输出较多,建议确认是否都必要,过多的日志会影响性能。
  • 命名规范setBlockClosedId 命名清晰,但 setHoveredId (QML/C++) 和 setBlockClosedId (C++/C++) 概念转换稍显生硬,不过可以接受。

3. 代码性能

  • QMap 的查找与插入QMap 的查找和插入时间复杂度是 O(log n)。在通知量大的情况下,频繁操作可能成为瓶颈。不过通常通知数量不多,影响不大。
  • 数据拷贝:如上所述,insert(item) 涉及数据拷贝。建议使用 std::move 或确保 NotifyEntity 的拷贝构造函数高效。
  • QMetaObject::invokeMethod:在 bubblepanel.cpp 中使用了 Qt::DirectConnection。这通常是高效的(直接调用函数),但如果跨线程调用会导致崩溃。请确保 BubblePanelm_notificationServer 在同一线程。如果是跨线程,应该使用 Qt::QueuedConnection

4. 代码安全

  • 线程安全

    • m_blockClosedId 是一个成员变量,被 setBlockClosedId 写入,被 onHandingPendingEntities 读取。
    • m_pendingTimeoutEntities 也是被多处修改。
    • 如果 NotificationManager 运行在独立线程(例如作为 DBus 服务),而 BubblePanel (UI) 运行在主线程,那么 setBlockClosedId 的调用涉及跨线程。
    • QMetaObject::invokeMethod(..., Qt::DirectConnection, ...) 强制在同一线程直接调用。如果对象不在同一线程,这是不安全的。必须确认线程模型。如果是跨线程,必须使用 Qt::QueuedConnection 并确保 m_blockClosedIdm_pendingTimeoutEntities 的访问受互斥锁保护(或者确保所有操作都在同一线程通过事件循环串行化)。
    • QMap 本身不是线程安全的。
  • 空指针检查:代码中未对 m_notificationServer 进行空指针检查,虽然通常依赖框架保证,但防御性编程建议增加检查。

改进建议

  1. 修复逻辑漏洞(重复条目问题)
    onHandingPendingEntities 中,不应该简单地 insert 新条目。应该更新现有条目的 Key,或者使用更复杂的数据结构来跟踪"被阻塞"的状态,而不是依赖重新插入。

    • 改进方案:在 setBlockClosedId 中,找到对应的实体后,直接修改其 Key(如果 QMap 支持,或者先 erase 再 insert)。在 onHandingPendingEntities 中,如果发现是阻塞 ID,直接跳过处理(continue),但不要重新插入。重新插入的逻辑应该由定时器或 setBlockClosedId 本身来触发(例如,当鼠标移开时,设置一个最终的超时时间)。
    • 或者:在 onHandingPendingEntities 中,如果是阻塞 ID,将其移到一个单独的 m_hoveredEntities 容器中。当 setBlockClosedId 被调用(无论是更新 ID 还是设为 0),再决定是放回 m_pendingTimeoutEntities 还是处理关闭。
  2. 使用常量

    // 在类定义中或匿名命名空间中
    static const qint64 HOVER_EXTENSION_TIME_MS = 1000;
  3. 线程安全确认与修复

    • 检查 BubblePanelNotificationManager 的线程依附性。
    • 如果跨线程:
      • Qt::DirectConnection 改为 Qt::QueuedConnection
      • NotificationManager 中使用 QMutex 保护 m_blockClosedIdm_pendingTimeoutEntities 的访问。
  4. 优化 setBlockClosedId 查找逻辑
    确保查找逻辑与容器类型匹配,并处理 findIter == end() 的情况(目前的代码处理了,但需确认逻辑是否符合预期)。

  5. 代码重构建议
    将"阻塞关闭"的逻辑封装得更清晰。目前的实现分散在 setBlockClosedIdonHandingPendingEntities 中,且依赖修改容器结构(insert/erase)来维持状态,容易出错。

修正后的代码示例(部分逻辑)

针对 notificationmanager.cpp 的逻辑改进建议:

void NotificationManager::setBlockClosedId(qint64 id)
{
    // 如果设置了新的阻塞ID,且之前有旧的阻塞ID,需要处理旧ID的"恢复"逻辑
    if (m_blockClosedId != NotifyEntity::InvalidId && m_blockClosedId != id) {
        auto findIter = std::find_if(m_pendingTimeoutEntities.begin(), m_pendingTimeoutEntities.end(), 
            [this](const NotifyEntity &entity) { return entity.id() == m_blockClosedId; });
        
        if (findIter != m_pendingTimeoutEntities.end()) {
            // 找到了旧的阻塞实体,将其重新加入队列,设置新的超时时间(例如当前时间 + 默认超时)
            // 注意:这里需要根据实际业务逻辑决定是恢复默认超时还是立即处理
            // 如果是切换到另一个气泡悬停,可能不需要做任何事,因为新的 setBlockClosedId 会保护新的气泡
            // 但如果 id == 0 (鼠标移出),则需要让它尽快关闭或恢复倒计时
            
            // 简化逻辑:如果切换到 0,我们将其超时设为当前时间+一小段时间以便立即处理
            // 或者直接从队列中移除并触发关闭逻辑(取决于需求:鼠标移开后是立即关闭还是等待剩余时间)
            // 假设需求是等待剩余时间或立即关闭,这里不做重新插入,而是让它在下次 onHandingPendingEntities 中自然处理
        }
    }
    
    m_blockClosedId = id;
    
    // 如果需要立即触发处理(例如为了响应鼠标移出)
    if (id == 0) {
        onHandingPendingEntities();
    }
}

void NotificationManager::onHandingPendingEntities()
{
    const auto current = QDateTime::currentMSecsSinceEpoch();
    
    // 使用可修改的迭代器或先收集Key再处理,避免迭代器失效问题(虽然这里只是continue)
    // 注意:不要在遍历时直接 insert 到同一个 QMap,这可能导致逻辑混乱
    // 更好的做法是:如果是阻塞ID,跳过。真正的延期逻辑由定时器或其他机制触发,或者在这里只做"跳过",
    // 而在 setBlockClosedId 中更新时间戳(如果 QMap 允许修改 Key,或者 erase 后 insert)
    
    for (auto it = m_pendingTimeoutEntities.begin(); it != m_pendingTimeoutEntities.end(); ) {
        // 检查是否超时
        if (current < it.key()) {
            ++it;
            continue;
        }

        NotifyEntity item = it.value();
        qint64 key = it.key();
        
        // 移除当前项,防止内存泄漏或重复处理
        it = m_pendingTimeoutEntities.erase(it);

        if (item.id() == m_blockClosedId) {
            // 如果是当前被阻塞的ID,重新插入到未来时间
            // 这里使用 move 语义减少拷贝
            m_pendingTimeoutEntities.insert(current + HOVER_EXTENSION_TIME_MS, std::move(item));
            continue;
        }

        // 正常关闭逻辑
        notificationClosed(item.id(), item.bubbleId(), NotifyEntity::Expired);
    }
}

总结
该代码实现了基本功能,但在逻辑严密性(防止重复条目、处理快速切换)、代码质量(去除魔法数字)和线程安全(确认 DirectConnection 的安全性)方面有改进空间。建议重点修复 onHandingPendingEntities 中直接修改正在遍历的容器可能导致的状态不一致问题。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants