Skip to content

fix: Fix the issue where the theme area cannot switch focus#2901

Merged
deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
pengfeixx:fix-311337
Dec 24, 2025
Merged

fix: Fix the issue where the theme area cannot switch focus#2901
deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
pengfeixx:fix-311337

Conversation

@pengfeixx

@pengfeixx pengfeixx commented Dec 24, 2025

Copy link
Copy Markdown
Contributor

Fix the issue where the theme area cannot switch focus

Log: Fix the issue where the theme area cannot switch focus
pms: BUG-311337

Summary by Sourcery

Improve keyboard focus handling for theme selection and dock display mode selection views.

Bug Fixes:

  • Restore keyboard focus navigation and activation within the theme selection grid, including the "More Wallpapers" entry.

Enhancements:

  • Add structured focus management and keyboard navigation to theme selection items and the dock display mode selector for better accessibility.

@sourcery-ai

sourcery-ai Bot commented Dec 24, 2025

Copy link
Copy Markdown

Reviewer's Guide

Refactors the theme selection view and dock mode selector to be proper keyboard-focusable controls, introducing focus management, keyboard navigation, and activation handling while preserving existing mouse behavior and layout.

Sequence diagram for keyboard focus and activation in theme selection view

sequenceDiagram
    actor User
    participant Root as FocusScope_root
    participant ListView as ThemeListView
    participant Model as GlobalThemeModel
    participant Worker as ThemeWorker

    User->>Root: Press Left/Right/Up/Down
    Root->>Root: Update focusedThemeIndex
    Root->>ListView: Update currentIndex if page changed

    User->>Root: Press Enter/Return/Space
    Root->>Root: activateCurrentItem()
    Root->>Model: rowCount()
    alt focusedThemeIndex < rowCount
        Root->>Model: data(index(focusedThemeIndex, 0), IdRole)
        Model-->>Root: themeId
        Root->>ListView: selectTheme = themeId
        Root->>Worker: setGlobalTheme(themeId)
    else focusedThemeIndex == rowCount
        Root->>Worker: goDownloadTheme()
    end

    Model-->>ListView: modelReset()
    ListView->>Root: onModelReset handler
    Root->>Root: Compare new rowCount with lastModelCount
    alt rowCount changed
        Root->>ListView: selectTheme = empty
        Root->>Root: focusedThemeIndex = 0
        Root->>Root: lastModelCount = newCount
    else rowCount unchanged
        Root->>Root: Keep current focusedThemeIndex
    end

    User->>Root: Focus moves away
    Root->>Root: hadLostFocus = true
    User->>Root: Focus returns
    Root->>Root: If hadLostFocus, reset focusedThemeIndex to 0
Loading

Sequence diagram for dock mode selector keyboard navigation and activation

sequenceDiagram
    actor User
    participant Repeater as ModeRepeater
    participant Delegate as ModeItemDelegate
    participant Dock as DockInter

    User->>Delegate: Tab into first delegate
    Delegate->>Delegate: activeFocusOnTab (index == 0)

    User->>Delegate: Press Space or Return
    Delegate->>Delegate: activate()
    Delegate->>Dock: setDisplayMode(model.value)

    User->>Delegate: Mouse click
    Delegate->>Delegate: onClicked -> activate()
    Delegate->>Dock: setDisplayMode(model.value)

    User->>Delegate: Press Left
    alt index > 0
        Delegate->>Repeater: itemAt(index - 1)
        Repeater-->>Delegate: previousItem
        Delegate->>previousItem: children[0].forceActiveFocus()
    else index == 0
        Delegate->>Delegate: No action
    end

    User->>Delegate: Press Right
    alt index < modeRepeater.count - 1
        Delegate->>Repeater: itemAt(index + 1)
        Repeater-->>Delegate: nextItem
        Delegate->>nextItem: children[0].forceActiveFocus()
    else index is last
        Delegate->>Delegate: No action
    end

    Dock-->>Delegate: DisplayMode changes
    Delegate->>Delegate: Border visible when DisplayMode == model.value or activeFocus
Loading

File-Level Changes

Change Details Files
Wrap theme list in a FocusScope and implement explicit keyboard focus, navigation, and activation for theme tiles and the "More Wallpapers" item.
  • Replaced the root ListView with a FocusScope that proxies currentIndex/count and manages activeFocusOnTab.
  • Added focusedThemeIndex, lastModelCount, and hadLostFocus state to track keyboard focus and reset behavior on focus changes or model resets.
  • Implemented Keys handlers (left/right/up/down/enter/return/space) to move focus across the grid and pages, updating ListView.currentIndex as needed.
  • Added activateCurrentItem() to apply a theme or trigger goDownloadTheme() based on focusedThemeIndex and model row count.
src/plugin-personalization/qml/ThemeSelectView.qml
Update theme delegates and "More Wallpapers" tile to visually reflect keyboard focus and integrate with the new focus model.
  • Extended theme delegates with isFocused derived from FocusScope + focusedThemeIndex, and updated border color logic to show activeColor when either selected or focused.
  • Updated mouse click handling on theme tiles to also update focusedThemeIndex in sync with selection and theme change.
  • Refined the layout and width/height calculations in the GridLayout to use listview item properties and keep the grid size behavior consistent.
  • Enhanced the "More Wallpapers" tile to compute its global index, show a focus border when focused, and keep its mouse click behavior to call goDownloadTheme().
src/plugin-personalization/qml/ThemeSelectView.qml
Convert dock display mode selector items to keyboard-focusable D.ItemDelegate components with proper focus visuals and key handling.
  • Replaced the manual Rectangle + MouseArea based dock mode items with D.ItemDelegate inside the Repeater, enabling focusPolicy TabFocus and initial activeFocusOnTab for index 0.
  • Moved activation logic into a reusable activate() function invoked on click, Space, and Return to set dccData.dockInter.DisplayMode.
  • Rendered the selection/focus border via the delegate background Rectangle when either the current mode is selected or the delegate has activeFocus.
  • Implemented Left/Right key handlers to move focus between adjacent mode delegates using the modeRepeater reference.
src/plugin-dock/qml/main.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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

  • In ThemeSelectView.activateCurrentItem, using the hardcoded role value 0x201 makes the code brittle; consider using a named role or a shared constant from the model instead of a magic number to retrieve the theme id.
  • The keyboard Up/Down handlers in ThemeSelectView update focusedThemeIndex but do not adjust listview.currentIndex, which can cause the logical focus to move off the currently visible page; consider keeping currentIndex in sync with focusedThemeIndex as you do for Left/Right.
  • In the dock modeRepeater navigation, modeRepeater.itemAt(index ± 1).children[0].forceActiveFocus() relies on implicit child ordering and could break if the delegate structure changes; it would be more robust to give the focusable ItemDelegate an id and call forceActiveFocus() on that explicitly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In ThemeSelectView.activateCurrentItem, using the hardcoded role value `0x201` makes the code brittle; consider using a named role or a shared constant from the model instead of a magic number to retrieve the theme id.
- The keyboard Up/Down handlers in ThemeSelectView update `focusedThemeIndex` but do not adjust `listview.currentIndex`, which can cause the logical focus to move off the currently visible page; consider keeping `currentIndex` in sync with `focusedThemeIndex` as you do for Left/Right.
- In the dock `modeRepeater` navigation, `modeRepeater.itemAt(index ± 1).children[0].forceActiveFocus()` relies on implicit child ordering and could break if the delegate structure changes; it would be more robust to give the focusable `ItemDelegate` an `id` and call `forceActiveFocus()` on that explicitly.

## Individual Comments

### Comment 1
<location> `src/plugin-personalization/qml/ThemeSelectView.qml:21` </location>
<code_context>
-    readonly property int imageRectH: 86
-    readonly property int imageRectW: itemWidth
-
-    property int gridMaxColumns: Math.floor(listview.width / (itemWidth + itemSpacing))
-    property int gridMaxRows: 2
-
</code_context>

<issue_to_address>
**issue (bug_risk):** Guard against gridMaxColumns becoming 0 to avoid divide-by-zero and navigation issues.

When `listview.width` is 0 or very small, `gridMaxColumns` evaluates to 0, which is then used in `model: Math.ceil((rowCount + 1) / (2 * gridMaxColumns))` and in keyboard navigation. This can trigger divide-by-zero warnings and incorrect paging. Clamp the value to at least 1, e.g. `property int gridMaxColumns: Math.max(1, Math.floor(listview.width / (itemWidth + itemSpacing)))`.
</issue_to_address>

### Comment 2
<location> `src/plugin-personalization/qml/ThemeSelectView.qml:117-121` </location>
<code_context>
+            function onModelReset() {
+                var newCount = dccData.globalThemeModel.rowCount()
+                // 只有当模型数量真正发生变化时才重置焦点
+                if (newCount !== root.lastModelCount) {
+                    listview.selectTheme = ""
+                    root.focusedThemeIndex = 0
+                    root.lastModelCount = newCount
                 }
             }
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Reset the ListView page index when the model changes to keep focus and page in sync.

On model reset you clear `selectTheme` and set `focusedThemeIndex` to 0, but leave `listview.currentIndex` unchanged. This can leave the view scrolled to a later page while focus is on the first item, so the focused theme isn’t visible until the user navigates. To keep focus and view aligned, also set `listview.currentIndex = 0` in this branch.

```suggestion
        Connections {
            target: dccData.globalThemeModel
            function onModelReset() {
                var newCount = dccData.globalThemeModel.rowCount()
                // 只有当模型数量真正发生变化时才重置焦点
                if (newCount !== root.lastModelCount) {
                    listview.selectTheme = ""
                    root.focusedThemeIndex = 0
                    listview.currentIndex = 0
                    root.lastModelCount = newCount
                }
```
</issue_to_address>

### Comment 3
<location> `src/plugin-personalization/qml/ThemeSelectView.qml:83` </location>
<code_context>
+    function activateCurrentItem() {
+        var totalThemes = dccData.globalThemeModel.rowCount()
+        if (focusedThemeIndex < totalThemes) {
+            var themeId = dccData.globalThemeModel.data(dccData.globalThemeModel.index(focusedThemeIndex, 0), 0x201) // IdRole = Qt::UserRole + 0x101
+            listview.selectTheme = themeId
+            dccData.worker.setGlobalTheme(themeId)
</code_context>

<issue_to_address>
**suggestion:** Avoid using a hard-coded role number for the theme id; prefer a named role or constant.

Using the literal `0x201` tightly couples this code to the model’s internal role mapping and hurts maintainability. Prefer a named role from `roleNames()` (or a shared constant) so the code is self-describing and resilient to changes in role values.

Suggested implementation:

```
    readonly property int themeIdRole: Qt.UserRole + 0x101 // IdRole

    Keys.onReturnPressed: activateCurrentItem()
    Keys.onEnterPressed: activateCurrentItem()
    Keys.onSpacePressed: activateCurrentItem()

```

```
            var themeId = dccData.globalThemeModel.data(dccData.globalThemeModel.index(focusedThemeIndex, 0), themeIdRole)

```
</issue_to_address>

### Comment 4
<location> `src/plugin-dock/qml/main.qml:96-98` </location>
<code_context>
+                                Keys.onSpacePressed: activate()
+                                Keys.onReturnPressed: activate()
+
+                                Keys.onLeftPressed: {
+                                    if (index > 0) {
+                                        modeRepeater.itemAt(index - 1).children[0].forceActiveFocus()
+                                    }
+                                }
</code_context>

<issue_to_address>
**issue (bug_risk):** Avoid relying on `children[0]` to access the delegate when moving focus between items.

`modeRepeater.itemAt(index - 1)` returns the `ColumnLayout`, and this assumes its first child is always the `D.ItemDelegate`. Any change to the layout’s child structure (e.g. wrapping the delegate) will break focus navigation. Prefer making `D.ItemDelegate` the root delegate, or give it an `objectName` and resolve it via `itemAt(...).findChild(...)` instead of relying on child order.
</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.

function incrementCurrentIndex() {
listview.incrementCurrentIndex()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Guard against gridMaxColumns becoming 0 to avoid divide-by-zero and navigation issues.

When listview.width is 0 or very small, gridMaxColumns evaluates to 0, which is then used in model: Math.ceil((rowCount + 1) / (2 * gridMaxColumns)) and in keyboard navigation. This can trigger divide-by-zero warnings and incorrect paging. Clamp the value to at least 1, e.g. property int gridMaxColumns: Math.max(1, Math.floor(listview.width / (itemWidth + itemSpacing))).

Comment on lines +117 to +121
Connections {
target: dccData.globalThemeModel
function onModelReset() {
var newCount = dccData.globalThemeModel.rowCount()
// 只有当模型数量真正发生变化时才重置焦点

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Reset the ListView page index when the model changes to keep focus and page in sync.

On model reset you clear selectTheme and set focusedThemeIndex to 0, but leave listview.currentIndex unchanged. This can leave the view scrolled to a later page while focus is on the first item, so the focused theme isn’t visible until the user navigates. To keep focus and view aligned, also set listview.currentIndex = 0 in this branch.

Suggested change
Connections {
target: dccData.globalThemeModel
function onModelReset() {
var newCount = dccData.globalThemeModel.rowCount()
// 只有当模型数量真正发生变化时才重置焦点
Connections {
target: dccData.globalThemeModel
function onModelReset() {
var newCount = dccData.globalThemeModel.rowCount()
// 只有当模型数量真正发生变化时才重置焦点
if (newCount !== root.lastModelCount) {
listview.selectTheme = ""
root.focusedThemeIndex = 0
listview.currentIndex = 0
root.lastModelCount = newCount
}

function activateCurrentItem() {
var totalThemes = dccData.globalThemeModel.rowCount()
if (focusedThemeIndex < totalThemes) {
var themeId = dccData.globalThemeModel.data(dccData.globalThemeModel.index(focusedThemeIndex, 0), 0x201) // IdRole = Qt::UserRole + 0x101

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Avoid using a hard-coded role number for the theme id; prefer a named role or constant.

Using the literal 0x201 tightly couples this code to the model’s internal role mapping and hurts maintainability. Prefer a named role from roleNames() (or a shared constant) so the code is self-describing and resilient to changes in role values.

Suggested implementation:

    readonly property int themeIdRole: Qt.UserRole + 0x101 // IdRole

    Keys.onReturnPressed: activateCurrentItem()
    Keys.onEnterPressed: activateCurrentItem()
    Keys.onSpacePressed: activateCurrentItem()

            var themeId = dccData.globalThemeModel.data(dccData.globalThemeModel.index(focusedThemeIndex, 0), themeIdRole)

Fix the issue where the theme area cannot switch focus

Log: Fix the issue where the theme area cannot switch focus
pms: BUG-311337
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

我来对这段代码进行详细审查:

  1. 代码结构和逻辑改进:

main.qml:

  • 使用 ItemDelegate 替代了原来的 Item + MouseArea 组合,这是一个好的改进,提供了更好的可访问性支持
  • 添加了键盘导航支持(Left/Right箭头键),提高了可用性
  • 增加了 focusPolicy 和 activeFocusOnTab 属性,改善了键盘导航体验
  • 将激活逻辑封装到 activate() 函数中,提高了代码复用性

ThemeSelectView.qml:

  • 使用 FocusScope 作为根组件,更好地管理焦点
  • 实现了完整的键盘导航支持(上下左右箭头键)
  • 添加了焦点状态视觉反馈
  • 优化了焦点切换逻辑,避免不必要的焦点重置
  1. 性能优化建议:

main.qml:

  • 可以考虑使用 Loader 延迟加载非必要的内容
  • 建议为频繁访问的属性(如 dccData.dockInter.DisplayMode)添加缓存

ThemeSelectView.qml:

  • Image 组件已经使用了 asynchronous 属性,这是好的
  • 可以考虑使用 cache 属性进一步优化图片加载性能
  • 建议对频繁计算的属性(如 gridMaxColumns)进行缓存
  1. 可访问性改进:

main.qml:

  • 已经添加了键盘导航支持
  • 建议添加 accessible.name 和 accessible.description 属性

ThemeSelectView.qml:

  • 实现了完整的键盘导航
  • 建议为每个主题项添加适当的 accessible 描述
  • 可以考虑添加屏幕阅读器支持
  1. 安全性考虑:
  • 代码中没有发现明显的安全问题
  • 建议对用户输入进行适当的验证和清理
  • 可以考虑添加错误处理机制
  1. 其他建议:

main.qml:

  • 建议添加适当的注释说明复杂的交互逻辑
  • 可以考虑使用状态机来管理不同的显示状态

ThemeSelectView.qml:

  • 建议将一些魔法数字(如 0x201)定义为常量
  • 可以考虑将一些复杂的逻辑拆分成更小的函数
  • 建议添加更多的错误处理和边界条件检查
  1. 代码风格改进:
  • 保持了一致的缩进和格式化
  • 变量命名清晰易懂
  • 建议添加更多的类型注解
  • 可以考虑使用更现代的 QML 特性

总体来说,这些改动提高了代码的可访问性和用户体验,特别是在键盘导航方面。代码结构清晰,逻辑合理,但还可以在性能优化和错误处理方面做进一步改进。

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: caixr23, pengfeixx

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

@pengfeixx

Copy link
Copy Markdown
Contributor Author

/forcemerge

@deepin-bot

deepin-bot Bot commented Dec 24, 2025

Copy link
Copy Markdown

This pr force merged! (status: blocked)

@deepin-bot deepin-bot Bot merged commit c96fceb into linuxdeepin:master Dec 24, 2025
15 of 17 checks passed
@pengfeixx pengfeixx deleted the fix-311337 branch December 24, 2025 06:18
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