Skip to content

Conversation

@robertkill
Copy link
Contributor

@robertkill robertkill commented Jan 26, 2026

  1. Added delayed reset timer in FreeSortListView to handle empty model cases
  2. Modified resetViewState() to check for empty model and use timer for delayed reset
  3. Added positionViewAtIndex(0, ListView.Beginning) to ensure proper view positioning
  4. Changed WindowedFrame's onVisibleChanged handler to reset appList both when showing and hiding
  5. The timer ensures reset happens only when model has items, preventing issues with empty lists

Log: Improved launcher view reset behavior when opening and closing

Influence:

  1. Test launcher opening with empty app list
  2. Verify view resets to top when launcher is shown
  3. Test search functionality and clearing on launcher hide
  4. Verify keyboard focus behavior when launcher opens/closes
  5. Test folder popup closes properly when launcher hides
  6. Check smooth scrolling and positioning when resetting view

fix: 改进启动器视图状态重置逻辑

  1. 在 FreeSortListView 中添加延迟重置计时器以处理空模型情况
  2. 修改 resetViewState() 方法检查空模型并使用计时器进行延迟重置
  3. 添加 positionViewAtIndex(0, ListView.Beginning) 确保正确的视图定位
  4. 修改 WindowedFrame 的 onVisibleChanged 处理程序,在显示和隐藏时都重 置 appList
  5. 计时器确保仅在模型有项目时执行重置,避免空列表问题

Log: 改进启动器打开和关闭时的视图重置行为

Influence:

  1. 测试空应用列表时启动器的打开
  2. 验证启动器显示时视图是否重置到顶部
  3. 测试搜索功能和在启动器隐藏时是否清除
  4. 验证启动器打开/关闭时的键盘焦点行为
  5. 测试文件夹弹窗在启动器隐藏时是否正确关闭
  6. 检查重置视图时的平滑滚动和定位

PMS: BUG-348311

Summary by Sourcery

Improve launcher app list view state reset behavior when the launcher is shown or hidden.

Bug Fixes:

  • Ensure app list view resets reliably even when the underlying model is initially empty by deferring the reset until items are available.
  • Reset the app list view when the launcher becomes visible to consistently position the list at the top.

Enhancements:

  • Add a delayed reset mechanism and initial view reset for the free-sort app list to maintain correct positioning and scrolling behavior across launcher open/close cycles.

1. Added delayed reset timer in FreeSortListView to handle empty model
cases
2. Modified resetViewState() to check for empty model and use timer for
delayed reset
3. Added positionViewAtIndex(0, ListView.Beginning) to ensure proper
view positioning
4. Changed WindowedFrame's onVisibleChanged handler to reset appList
both when showing and hiding
5. The timer ensures reset happens only when model has items, preventing
issues with empty lists

Log: Improved launcher view reset behavior when opening and closing

Influence:
1. Test launcher opening with empty app list
2. Verify view resets to top when launcher is shown
3. Test search functionality and clearing on launcher hide
4. Verify keyboard focus behavior when launcher opens/closes
5. Test folder popup closes properly when launcher hides
6. Check smooth scrolling and positioning when resetting view

fix: 改进启动器视图状态重置逻辑

1. 在 FreeSortListView 中添加延迟重置计时器以处理空模型情况
2. 修改 resetViewState() 方法检查空模型并使用计时器进行延迟重置
3. 添加 positionViewAtIndex(0, ListView.Beginning) 确保正确的视图定位
4. 修改 WindowedFrame 的 onVisibleChanged 处理程序,在显示和隐藏时都重
置 appList
5. 计时器确保仅在模型有项目时执行重置,避免空列表问题

Log: 改进启动器打开和关闭时的视图重置行为

Influence:
1. 测试空应用列表时启动器的打开
2. 验证启动器显示时视图是否重置到顶部
3. 测试搜索功能和在启动器隐藏时是否清除
4. 验证启动器打开/关闭时的键盘焦点行为
5. 测试文件夹弹窗在启动器隐藏时是否正确关闭
6. 检查重置视图时的平滑滚动和定位

PMS: BUG-348311
@deepin-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: robertkill

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 Jan 26, 2026

Reviewer's Guide

Refines launcher view reset behavior by introducing a delayed reset mechanism in FreeSortListView for empty models and adjusting WindowedFrame visibility handling so the app list view state is reset both when the launcher is shown and hidden, ensuring consistent positioning, focus, and cleanup.

Sequence diagram for launcher visibility change and app list reset

sequenceDiagram
    actor User
    participant LauncherController
    participant WindowedFrame
    participant AppList
    participant BottomBar
    participant BaseLayer
    participant FolderGridViewPopup

    User->>LauncherController: toggle visible true
    LauncherController-->>WindowedFrame: onVisibleChanged(visible true)
    WindowedFrame->>AppList: resetViewState()
    AppList->>AppList: if model.count === 0
    AppList->>AppList: start delayedResetTimer
    AppList-->>WindowedFrame: return
    AppList->>AppList: when model.count > 0
    AppList->>AppList: stop delayedResetTimer
    AppList->>AppList: perform resetViewState
    AppList->>AppList: currentIndex = 0
    AppList->>AppList: contentY = 0
    AppList->>AppList: positionViewAtIndex(0, ListView.Beginning)

    User->>LauncherController: toggle visible false
    LauncherController-->>WindowedFrame: onVisibleChanged(visible false)
    WindowedFrame->>BottomBar: searchEdit.text = ""
    WindowedFrame->>BaseLayer: focus = true
    WindowedFrame->>AppList: resetViewState()
    WindowedFrame->>FolderGridViewPopup: close()
Loading

Class diagram for WindowedFrame and FreeSortListView changes

classDiagram
    class WindowedFrame {
        +LauncherController launcherController
        +FreeSortListView appList
        +BottomBar bottomBar
        +Item baseLayer
        +FolderGridViewPopup folderGridViewPopup
        +onVisibleChanged()
    }

    class FreeSortListView {
        +ListView listView
        +Timer delayedResetTimer
        +Timer enableAnimationTimer
        +bool animationEnabled
        +resetViewState()
    }

    class Timer {
        +int interval
        +bool repeat
        +start()
        +stop()
        +onTriggered()
    }

    class ListView {
        +int currentIndex
        +real contentY
        +bool highlightFollowsCurrentItem
        +Model model
        +positionViewAtIndex(index, mode)
    }

    class BottomBar {
        +SearchEdit searchEdit
    }

    class SearchEdit {
        +string text
    }

    class FolderGridViewPopup {
        +close()
    }

    WindowedFrame --> FreeSortListView : uses
    WindowedFrame --> BottomBar : uses
    WindowedFrame --> FolderGridViewPopup : controls
    FreeSortListView --> ListView : wraps
    FreeSortListView --> Timer : delayedResetTimer
    FreeSortListView --> Timer : enableAnimationTimer
    BottomBar --> SearchEdit : contains
Loading

Flow diagram for FreeSortListView resetViewState and delayed timer

flowchart TD
    A[start resetViewState] --> B{model.count === 0}
    B -->|yes| C[start delayedResetTimer]
    C --> D[return]
    B -->|no| E[store listView.highlightFollowsCurrentItem in wasFollowing]
    E --> F[set listView.highlightFollowsCurrentItem = false]
    F --> G[set listView.currentIndex = 0]
    G --> H[set listView.contentY = 0]
    H --> I[call listView.positionViewAtIndex 0, ListView.Beginning]
    I --> J[restore listView.highlightFollowsCurrentItem from wasFollowing]
    J --> K[end resetViewState]

    subgraph DelayedResetTimer_loop
      L[Timer triggered] --> M{model.count > 0}
      M -->|no| L
      M -->|yes| N[stop delayedResetTimer]
      N --> O[call resetViewState]
    end
Loading

File-Level Changes

Change Details Files
Make launcher app list reset consistently on both show and hide, while still performing cleanup only on hide.
  • Update LauncherController.visible change handler to call app list reset when the launcher becomes visible.
  • Retain and scope search text clearing, keyboard focus reset, and folder popup closing to only when the launcher hides.
qml/windowed/WindowedFrame.qml
Introduce a robust view state reset mechanism in FreeSortListView that handles empty models via a delayed timer and ensures the list positions at the top.
  • Add a repeating timer that waits for the list model to have items before performing view reset.
  • Invoke resetViewState() on component completion to normalize initial state.
  • Modify resetViewState() to defer work when the model is empty, then set current index and scroll offset to the top and explicitly position the view at index 0 while temporarily disabling highlight-follow behavior.
qml/windowed/FreeSortListView.qml

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

@deepin-ci-robot
Copy link

deepin pr auto review

这段代码主要修改了 FreeSortListView.qmlWindowedFrame.qml 两个文件,目的是在启动器显示和隐藏时更可靠地重置视图状态(滚动位置和当前选中项)。下面是对这两处修改的详细审查和改进建议:

1. FreeSortListView.qml 审查

修改点分析

  1. 引入了一个 Timer (delayedResetTimer) 用于处理模型数据为空时的重置逻辑。
  2. resetViewState 函数中增加了对 listView.model.count 的检查,若为空则启动定时器轮询。
  3. resetViewState 中增加了 positionViewAtIndex 调用。

存在的问题与改进建议

代码逻辑与性能问题

  1. 定时器轮询效率低

    • 问题repeat: true 配合 interval: 50 意味着每 50ms 检查一次数据是否加载完毕。虽然间隔很短,但在数据加载极快或极慢的情况下,这种轮询机制不够优雅且浪费资源。
    • 建议:QML 的 Model 通常会有信号通知数据变化(如 onCountChanged)。建议监听 listView.model.onCountChanged 信号来替代轮询,这样是事件驱动的,响应更及时且无空转。
    • 改进代码示例
      // 在 Connections 或直接绑定中监听
      Connections {
          target: listView.model
          function onCountChanged() {
              if (listView.model.count > 0 && waitingForData) {
                  waitingForData = false;
                  resetViewState();
              }
          }
      }
    • 如果必须用定时器:应设置 repeat: false,并在 onTriggered 中再次判断。如果数据仍未加载,再次启动定时器(递归定时器),避免持续占用主循环。
  2. 重复调用 resetViewState

    • 问题:在 Component.onCompleted 中直接调用了 resetViewState,而在 WindowedFrame.qmlonVisibleChanged 中也会调用。如果模型加载是异步的,Component.onCompleted 时的 count 可能是 0,导致启动定时器。随后 onVisibleChanged 可能再次触发定时器,导致逻辑混乱。
    • 建议:增加一个标志位(如 property bool isResetting: false)来防止重置过程被并发触发,或者确保重置逻辑是幂等的。
  3. positionViewAtIndex 的必要性

    • 问题:代码中同时设置了 contentY = 0positionViewAtIndex(0, ListView.Beginning)。通常 contentY = 0 已经足够将视图置顶。positionViewAtIndex 会触发额外的布局计算。
    • 建议:保留 contentY = 0 即可,除非 ListView 有特殊的 headerfooter 导致 contentY 无法精确定位。如果确实需要 positionViewAtIndex,建议在注释中说明原因。

代码安全问题

  1. 空模型引用
    • 问题:代码中直接访问 listView.model.count。如果 listView.model 尚未赋值或为 null,会抛出错误。
    • 建议:增加判空保护:if (listView.model && listView.model.count > 0)

2. WindowedFrame.qml 审查

修改点分析
修改了 Connections 中对 LauncherController.visible 变化的响应逻辑,区分了显示和隐藏时的处理。

存在的问题与改进建议

代码逻辑问题

  1. 逻辑冗余

    • 问题:在 LauncherController.visibletrue 时调用了 appList.resetViewState(),而在 false 时也调用了了一次。
    • 建议:明确需求。通常视图重置应该在显示时(为了回到初始状态)或隐藏时(为了清理状态)。如果两者都需要,请确保这是预期行为。如果目的是每次显示时都重置,那么隐藏时的重置可能是多余的,除非隐藏时有特定的动画或状态需要归位。
  2. 焦点管理

    • 问题baseLayer.focus = true 在隐藏时被调用。此时窗口即将隐藏,设置焦点可能没有实际效果,且可能导致焦点在隐藏瞬间跳变。
    • 建议:确认焦点重置是否必须在隐藏时进行。通常焦点重置是为了防止下次显示时焦点停留在错误的控件上,建议在显示时 (visible === true) 进行焦点重置。

3. 综合改进后的代码建议

FreeSortListView.qml

Item {
    id: root
    property bool waitingForData: false // 新增标志位

    // ... 其他代码 ...

    Timer {
        id: delayedResetTimer
        interval: 50
        repeat: false // 改为单次触发
        onTriggered: {
            if (listView.model && listView.model.count > 0) {
                resetViewState();
            } else if (waitingForData) {
                // 如果仍在等待数据,再次启动定时器(递归)
                start();
            }
        }
    }

    // 监听模型数据变化(推荐方式)
    Connections {
        target: listView.model
        function onCountChanged() {
            if (waitingForData && listView.model.count > 0) {
                waitingForData = false;
                delayedResetTimer.stop(); // 停止可能存在的定时器
                resetViewState();
            }
        }
    }

    function resetViewState() {
        // 增加判空保护
        if (!listView.model) {
            console.warn("ListView model is null, cannot reset view.");
            return;
        }

        if (listView.model.count === 0) {
            if (!waitingForData) {
                waitingForData = true;
                delayedResetTimer.start();
            }
            return;
        }

        let wasFollowing = listView.highlightFollowsCurrentItem;
        listView.highlightFollowsCurrentItem = false;
        listView.currentIndex = 0;
        listView.contentY = 0; // 通常这就足够了
        // listView.positionViewAtIndex(0, ListView.Beginning) // 视情况保留
        listView.highlightFollowsCurrentItem = wasFollowing;
    }
}

WindowedFrame.qml

Connections {
    target: LauncherController
    function onVisibleChanged() {
        if (LauncherController.visible) {
            // 显示时重置视图和焦点
            appList.resetViewState();
            baseLayer.focus = true;
        } else {
            // 隐藏时清理状态
            bottomBar.searchEdit.text = "";
            folderGridViewPopup.close();
        }
    }
}

4. 总结

  1. 性能:避免使用轮询定时器,改用信号驱动(onCountChanged)。
  2. 逻辑:确保 resetViewState 是幂等的,避免重复调用导致的状态冲突。
  3. 安全:增加对 model 的判空检查,防止运行时错误。
  4. 清晰性:明确 resetViewState 的调用时机(显示/隐藏),避免冗余操作。

以上改进能提高代码的健壮性和可维护性,同时减少不必要的性能开销。

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 delayedResetTimer currently polls listView.model.count every 50ms; consider instead reacting to a signal (e.g., onCountChanged or onModelChanged) so reset is event-driven rather than periodic polling.
  • Both the timer and resetViewState() assume listView.model is non-null and has a count property; adding defensive checks for listView.model && typeof listView.model.count === 'number' would make this more robust against model changes.
  • Since Component.onCompleted calls resetViewState() immediately, it might be worth confirming that the model is already populated at that time for all call sites, or gating the call behind the same delayed-reset logic to avoid transient empty-model behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The delayedResetTimer currently polls `listView.model.count` every 50ms; consider instead reacting to a signal (e.g., `onCountChanged` or `onModelChanged`) so reset is event-driven rather than periodic polling.
- Both the timer and `resetViewState()` assume `listView.model` is non-null and has a `count` property; adding defensive checks for `listView.model && typeof listView.model.count === 'number'` would make this more robust against model changes.
- Since `Component.onCompleted` calls `resetViewState()` immediately, it might be worth confirming that the model is already populated at that time for all call sites, or gating the call behind the same delayed-reset logic to avoid transient empty-model behavior.

## Individual Comments

### Comment 1
<location> `qml/windowed/FreeSortListView.qml:30-34` </location>
<code_context>
         }
     }
+    
+    Timer {
+        id: delayedResetTimer
+        interval: 50
+        repeat: true
+        onTriggered: {
+            if (listView.model.count > 0) {
+                delayedResetTimer.stop()
</code_context>

<issue_to_address>
**suggestion (performance):** Polling with a repeating timer for `model.count > 0` could be brittle and wasteful.

This 50ms repeating timer is effectively a tight polling loop on `model.count`. If the model is late to populate or never does, it runs indefinitely and assumes `model.count` is always readable. Prefer an event-driven approach, e.g. listening to a `model.countChanged`-style signal, or using a one-shot `Timer` you restart from `resetViewState()` while `count === 0` to avoid continuous polling.

Suggested implementation:

```
    // One-shot timer used as a deferred reset trigger. It is started from
    // resetViewState() while the model is still empty, instead of polling
    // continuously with a repeating timer.
    Timer {
        id: delayedResetTimer
        interval: 50
        repeat: false
        onTriggered: {
            // Only reset once the model is populated; otherwise the caller
            // (resetViewState) may choose to reschedule this timer.
            if (listView.model && listView.model.count > 0) {
                resetViewState()
            }
        }
    }

```

To fully implement the suggested behavior (avoiding continuous polling and only using a one-shot timer while `count === 0`), you should also update `resetViewState()` in this file:

1. At the beginning of `resetViewState()`, add logic like:
   - If `!listView.model || listView.model.count === 0`, call `delayedResetTimer.start()` and `return` early.
   - Otherwise, perform the normal reset logic immediately.

2. Ensure that no other code calls `delayedResetTimer.start()` in a loop or independently of `resetViewState()`, so the timer is only used as a deferred one-shot when the model is empty.

This will make the reset behavior event-driven from the caller’s perspective and avoid having an always-repeating 50ms polling timer.
</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.

Comment on lines +30 to +34
Timer {
id: delayedResetTimer
interval: 50
repeat: true
onTriggered: {
Copy link

Choose a reason for hiding this comment

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

suggestion (performance): Polling with a repeating timer for model.count > 0 could be brittle and wasteful.

This 50ms repeating timer is effectively a tight polling loop on model.count. If the model is late to populate or never does, it runs indefinitely and assumes model.count is always readable. Prefer an event-driven approach, e.g. listening to a model.countChanged-style signal, or using a one-shot Timer you restart from resetViewState() while count === 0 to avoid continuous polling.

Suggested implementation:

    // One-shot timer used as a deferred reset trigger. It is started from
    // resetViewState() while the model is still empty, instead of polling
    // continuously with a repeating timer.
    Timer {
        id: delayedResetTimer
        interval: 50
        repeat: false
        onTriggered: {
            // Only reset once the model is populated; otherwise the caller
            // (resetViewState) may choose to reschedule this timer.
            if (listView.model && listView.model.count > 0) {
                resetViewState()
            }
        }
    }

To fully implement the suggested behavior (avoiding continuous polling and only using a one-shot timer while count === 0), you should also update resetViewState() in this file:

  1. At the beginning of resetViewState(), add logic like:

    • If !listView.model || listView.model.count === 0, call delayedResetTimer.start() and return early.
    • Otherwise, perform the normal reset logic immediately.
  2. Ensure that no other code calls delayedResetTimer.start() in a loop or independently of resetViewState(), so the timer is only used as a deferred one-shot when the model is empty.

This will make the reset behavior event-driven from the caller’s perspective and avoid having an always-repeating 50ms polling timer.

}

function resetViewState() {
if (listView.model.count === 0) {
Copy link
Member

Choose a reason for hiding this comment

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

这个好像不对?listView.model 是 FreeSortProxyModel,不是 DelegateModel,没有 count 属性

@robertkill
Copy link
Contributor Author

其它PR修复

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.

3 participants