Skip to content

fix(notification): fix invisible delegates after collapsing expanded …#1476

Open
re2zero wants to merge 1 commit intolinuxdeepin:masterfrom
re2zero:bugfix
Open

fix(notification): fix invisible delegates after collapsing expanded …#1476
re2zero wants to merge 1 commit intolinuxdeepin:masterfrom
re2zero:bugfix

Conversation

@re2zero
Copy link
Contributor

@re2zero re2zero commented Mar 4, 2026

…notifications

Remove emit layoutChanged() which breaks animations and add refresh mechanism to trigger ListView re-render of displaced delegates.

删除emit layoutChanged()避免破坏动画,通过模拟微小滚动触发ListView重新渲染被移位的delegate。

PMS: BUG-351731 BUG-351729
Log: 修复收起超一屏应用通知时下方应用不显示的问题
Influence: 修复后收起应用通知下方应用正常显示,无需手动刷新

Summary by Sourcery

Bug Fixes:

  • Ensure notification delegates below a collapsed or expanded app group become visible without requiring manual refresh.

@deepin-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: re2zero

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 4, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Replaces a disruptive layoutChanged() emit with a lightweight ListView refresh mechanism by simulating a tiny scroll after collapsing/expanding notification app groups, ensuring displaced delegates are re-rendered without breaking animations.

Sequence diagram for refreshed delegates after notification collapse/expand

sequenceDiagram
    actor User
    participant NotifyViewDelegate
    participant NotifyModel
    participant NotifyView_ListView

    User->>NotifyViewDelegate: clickCollapse(appGroup)
    NotifyViewDelegate->>NotifyModel: collapseApp(index)
    NotifyModel-->>NotifyViewDelegate: collapseCompleted
    NotifyViewDelegate->>NotifyView_ListView: refreshAfterCollapse()
    activate NotifyView_ListView
    NotifyView_ListView->>NotifyView_ListView: Qt.callLater(callback)
    Note over NotifyView_ListView: callback executes shortly after
    NotifyView_ListView->>NotifyView_ListView: contentY++ (tiny scroll)
    NotifyView_ListView->>NotifyView_ListView: re-render displaced delegates
    deactivate NotifyView_ListView

    User->>NotifyViewDelegate: clickExpand(appGroup)
    NotifyViewDelegate->>NotifyModel: expandApp(index)
    NotifyModel-->>NotifyViewDelegate: expandCompleted
    NotifyViewDelegate->>NotifyView_ListView: refreshAfterCollapse()
    activate NotifyView_ListView
    NotifyView_ListView->>NotifyView_ListView: Qt.callLater(callback)
    NotifyView_ListView->>NotifyView_ListView: contentY++ (tiny scroll)
    NotifyView_ListView->>NotifyView_ListView: re-render displaced delegates
    deactivate NotifyView_ListView

    NotifyViewDelegate->>NotifyView_ListView: requestFocusOnExpand(targetIndex)
Loading

File-Level Changes

Change Details Files
Add a View-level helper to refresh ListView delegates after collapsing/expanding notification groups by simulating a minimal scroll.
  • Introduce refreshAfterCollapse() on the root view to adjust contentY by 1 in a Qt.callLater callback, forcing the ListView to re-render affected delegates.
  • Document the intent of the refresh helper to clarify its purpose and timing relative to collapse/expand transitions.
panels/notification/center/NotifyView.qml
Invoke the new refresh helper whenever an app notification group is collapsed or expanded so displaced delegates become visible again.
  • Call view.refreshAfterCollapse() immediately after notifyModel.collapseApp() in the collapse handler.
  • Call view.refreshAfterCollapse() immediately after notifyModel.expandApp() in the expand handler, before requesting focus.
panels/notification/center/NotifyViewDelegate.qml
Remove the layoutChanged() signal emission from the model collapse logic to avoid breaking animations.
  • Delete emit layoutChanged() at the end of NotifyModel::collapseApp so that view/layout updates rely on the new refresh mechanism instead of a full layout change.
panels/notification/center/notifymodel.cpp

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 1 issue, and left some high level feedback:

  • The refreshAfterCollapse implementation permanently increments contentY on every call; consider storing the current value and restoring it (or doing a small up/down adjustment in the same callLater) to avoid cumulative drift and unexpected jumps for users who collapse/expand frequently.
  • Since refreshAfterCollapse is also called after expand, you might want to rename it to something more generic (e.g. refreshAfterToggle) and document any assumptions about root.view (that it directly exposes contentY and is a ListView) to make the coupling clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `refreshAfterCollapse` implementation permanently increments `contentY` on every call; consider storing the current value and restoring it (or doing a small up/down adjustment in the same `callLater`) to avoid cumulative drift and unexpected jumps for users who collapse/expand frequently.
- Since `refreshAfterCollapse` is also called after expand, you might want to rename it to something more generic (e.g. `refreshAfterToggle`) and document any assumptions about `root.view` (that it directly exposes `contentY` and is a `ListView`) to make the coupling clearer.

## Individual Comments

### Comment 1
<location path="panels/notification/center/NotifyView.qml" line_range="87-89" />
<code_context>
+        // Refresh delegates after collapse/expand by simulating a tiny scroll
+        // This triggers ListView to re-render displaced delegates that may be invisible
+        // Called immediately after collapseApp/expandApp, before transition starts
+        function refreshAfterCollapse() {
+            Qt.callLater(function() {
+                contentY++
+            })
+        }
</code_context>
<issue_to_address>
**issue (bug_risk):** refreshAfterCollapse permanently shifts scroll position by 1 unit on every call

Because `contentY` is only incremented and never reset, each collapse/expand shifts the viewport down by 1 unit, gradually drifting the view from the user’s expected position. If you just need to trigger a re-render, use a net-zero scroll (e.g., `contentY += 1; contentY -= 1`, possibly via two `Qt.callLater` calls) or a more direct layout invalidation API so the visible scroll position stays stable.
</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.

@re2zero re2zero force-pushed the bugfix branch 3 times, most recently from 6e5e364 to 0aa40d0 Compare March 4, 2026 07:27
…notifications

Remove emit layoutChanged() which breaks animations and add refresh mechanism
to trigger ListView re-render of displaced delegates.

删除emit layoutChanged()避免破坏动画,通过模拟微小滚动触发ListView
重新渲染被移位的delegate。

PMS: BUG-351731 BUG-351729
Log: 修复收起超一屏应用通知时下方应用不显示的问题
Influence: 修复后收起应用通知下方应用正常显示,无需手动刷新
@deepin-ci-robot
Copy link

deepin pr auto review

这份代码变更主要是针对通知中心列表视图(ListView)在折叠/展开应用分组后,某些委托项可能未正确渲染的问题,提供了一种通过模拟微小滚动来触发视图刷新的变通方案。此外,还更新了版权年份。

以下是针对此变更的详细审查意见:

1. 语法逻辑

  • 版权年份更新:
    • SPDX-FileCopyrightText2024 更新为 2024 - 2026 是符合规范的,表明对代码的维护周期预期。
  • refreshAfterCollapse 函数逻辑:
    • 逻辑上,该函数通过先修改 contentY,再利用 Qt.callLater 在下一帧恢复 contentY,从而产生一个"滚动-回滚"的动作。这会强制 ListView 重新计算可见区域并重新渲染委托。
    • 潜在问题:如果 contentY 已经是 0(在顶部),代码执行 contentY++,然后回滚。如果 ListView 的 contentHeight 小于 height(即内容不足以滚动),这种修改可能无效或者导致视图出现不必要的抖动。不过,考虑到这是通知中心,通常内容是可滚动的,该逻辑在大多数场景下是成立的。

2. 代码质量

  • 命名规范:
    • 函数名 refreshAfterCollapse 清晰地表达了其用途,符合命名规范。
  • 注释质量:
    • 添加的注释非常详细,解释了为什么需要这样做("triggers ListView to re-render displaced delegates")以及何时调用("Called immediately after..."),这对于后续维护非常有帮助。
  • 副作用:
    • 这是一个典型的"副作用"修复方案,通过修改滚动位置来触发渲染。虽然解决了渲染问题,但并不是最"干净"的架构方式。然而,在 QML/Qt Quick 中,有时这是处理 ListView 惰性委托加载缓存问题的唯一高效方法。
  • 删除 emit layoutChanged():
    • notifymodel.cpp 中删除了 emit layoutChanged()。这通常是一个好的改进,因为 layoutChanged() 会强制视图完全重建,性能开销很大。如果通过 QML 侧的微小滚动能解决视觉问题,那么移除这个沉重的信号是合理的。

3. 代码性能

  • Qt.callLater 的使用:
    • 使用 Qt.callLater 是正确的,它确保了 contentY 的修改发生在事件循环的下一帧,给 ListView 一个处理第一次变化的机会,然后再恢复。这避免了在同一事件循环中修改两次属性可能导致其中一次被忽略的问题。
  • 性能开销:
    • 相比于 C++ 侧的 layoutChanged()(会导致整个列表重新加载,甚至重新实例化所有 Delegate),QML 侧的微小滚动只会影响当前可见区域附近的委托。因此,这个改动显著提升了性能,尤其是在通知列表很长的时候。
  • 渲染抖动:
    • 由于 contentY 变动了 1 像素,用户可能会察觉到极其微小的视觉抖动,但在快速折叠/展开的操作中,这种抖动通常被动画效果掩盖,肉眼难以察觉。

4. 代码安全

  • 边界检查:
    • 函数中 if (contentY > 0) 的检查防止了在列表顶部时出现负的 contentY,保证了滚动位置的安全性。
  • 稳定性:
    • 移除 C++ 侧的 layoutChanged() 减少了模型与视图之间重同步的频率,降低了因模型数据不一致导致崩溃的风险。

改进建议

虽然当前的实现可以解决问题,但为了更健壮和可维护,建议考虑以下优化:

  1. 考虑使用 forceLayout():

    • Qt Quick 的 ListView 有一个 forceLayout() 方法。虽然它主要用于显式大小变化后,但在某些情况下,调用 ListView.view.forceLayout() 可能比模拟滚动更符合语义。
    • 注意forceLayout() 也有局限性,它可能不会重新创建已经缓存但不可见的 delegate,这正是当前代码试图解决的问题。如果 forceLayout() 无效,则当前的方案是合理的。
  2. 增加状态检查:

    • refreshAfterCollapse 中,可以增加一个检查,确保 ListView 确实处于可滚动状态(contentHeight > height),如果不可滚动,则可能不需要执行刷新逻辑,避免无意义的计算。
    function refreshAfterCollapse() {
        // 如果内容高度小于视图高度,说明不需要滚动,也就不存在"被挤出屏幕的不可见项"的问题
        if (contentHeight <= height) return;
    
        if (contentY > 0) {
            contentY--;
            Qt.callLater(function() { contentY++; });
        } else {
            contentY++;
            Qt.callLater(function() { contentY--; });
        }
    }
  3. 函数命名微调:

    • 既然这个函数既用于折叠也用于展开(从 Delegate 的调用可以看出),函数名 refreshAfterCollapse 略显狭隘。改为 refreshAfterLayoutChangerefreshDelegates 可能更准确。

总结

这是一个有效且高效的修复方案。它用较低成本的 QML 动画帧操作替代了高成本的 C++ 模型重置信号,解决了 ListView 的特定渲染问题。代码逻辑清晰,注释完善。唯一的小建议是优化函数名以适应更广泛的使用场景,并增加对不可滚动列表的防御性检查。

console.log("expand")
let expandIndex = model.index
notifyModel.expandApp(expandIndex)
root.view.refreshAfterCollapse()
Copy link
Contributor

Choose a reason for hiding this comment

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

函数名改一下吧,它跟collapse也没啥关系了,

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a UI issue in the notification center where delegates below a collapsed/expanded app group can become invisible, by avoiding a model layoutChanged() emission (which breaks animations) and adding a QML-side refresh mechanism to force the ListView to re-render displaced delegates.

Changes:

  • Removed emit layoutChanged() from NotifyModel::collapseApp() to avoid breaking ListView transitions/animations.
  • Added a ListView.refreshAfterCollapse() helper in QML that simulates a tiny scroll to trigger delegate re-rendering.
  • Invoked the refresh helper after group collapse/expand actions in the delegate.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
panels/notification/center/notifymodel.cpp Stops emitting layoutChanged() during collapse to preserve animations.
panels/notification/center/NotifyViewDelegate.qml Calls the new view refresh hook after collapse/expand actions.
panels/notification/center/NotifyView.qml Adds refreshAfterCollapse() implementation via small contentY nudge.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +72 to 73
root.view.refreshAfterCollapse()
root.view.requestFocusOnExpand(collapseIndex)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

In these handlers refreshAfterCollapse() is called before requestFocusOnExpand(). Because refreshAfterCollapse schedules a Qt.callLater contentY adjustment, the later adjustment can run after positionViewAtIndex() and shift the final scroll position by 1px. Either call requestFocusOnExpand() first, or change refreshAfterCollapse() to restore to a captured contentY so it can’t interfere with subsequent scrolling.

Suggested change
root.view.refreshAfterCollapse()
root.view.requestFocusOnExpand(collapseIndex)
root.view.requestFocusOnExpand(collapseIndex)
root.view.refreshAfterCollapse()

Copilot uses AI. Check for mistakes.
Comment on lines +215 to 216
root.view.refreshAfterCollapse()
root.view.requestFocusOnExpand(expandIndex + 1)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Same ordering issue as above: refreshAfterCollapse() runs before requestFocusOnExpand(), and its deferred contentY undo can run after positionViewAtIndex(), leaving the view shifted by 1px. Reorder the calls or make refreshAfterCollapse() restore to a captured contentY value.

Suggested change
root.view.refreshAfterCollapse()
root.view.requestFocusOnExpand(expandIndex + 1)
root.view.requestFocusOnExpand(expandIndex + 1)
root.view.refreshAfterCollapse()

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +95
// Refresh delegates after collapse/expand by simulating a tiny scroll
// This triggers ListView to re-render displaced delegates that may be invisible
// Called immediately after collapseApp/expandApp, before transition starts
function refreshAfterCollapse() {
if (contentY > 0) {
contentY--;
Qt.callLater(function() { contentY++; });
} else {
contentY++;
Qt.callLater(function() { contentY--; });
}
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Function name refreshAfterCollapse() is now used for both collapse and expand flows, which makes the intent harder to follow. Consider renaming it to reflect both cases (e.g., refreshAfterGroupToggle / refreshAfterModelChange) so future call sites don’t mislead readers.

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +94
// Refresh delegates after collapse/expand by simulating a tiny scroll
// This triggers ListView to re-render displaced delegates that may be invisible
// Called immediately after collapseApp/expandApp, before transition starts
function refreshAfterCollapse() {
if (contentY > 0) {
contentY--;
Qt.callLater(function() { contentY++; });
} else {
contentY++;
Qt.callLater(function() { contentY--; });
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

refreshAfterCollapse() nudges contentY and then uses Qt.callLater to undo it, but the undo step is relative to whatever contentY is at that later time. If anything else changes contentY between the two steps (e.g., positionViewAtIndex in requestFocusOnExpand), this will leave the ListView offset by 1px and can break SnapToItem / focus positioning. Consider capturing the intended final contentY and restoring exactly to that value (or avoid changing contentY at all by using ListView.forceLayout() if it solves the re-render issue).

Suggested change
// Refresh delegates after collapse/expand by simulating a tiny scroll
// This triggers ListView to re-render displaced delegates that may be invisible
// Called immediately after collapseApp/expandApp, before transition starts
function refreshAfterCollapse() {
if (contentY > 0) {
contentY--;
Qt.callLater(function() { contentY++; });
} else {
contentY++;
Qt.callLater(function() { contentY--; });
}
// Refresh delegates after collapse/expand by forcing a layout pass
// This triggers ListView to re-render displaced delegates that may be invisible
// Called immediately after collapseApp/expandApp, before transition starts
function refreshAfterCollapse() {
forceLayout()

Copilot uses AI. Check for mistakes.
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.

4 participants