Skip to content

fix(ComboBox): fix highlight lingering and add keyboard navigation in popup#585

Open
electricface wants to merge 1 commit intolinuxdeepin:masterfrom
electricface:swt/fix-bug306683
Open

fix(ComboBox): fix highlight lingering and add keyboard navigation in popup#585
electricface wants to merge 1 commit intolinuxdeepin:masterfrom
electricface:swt/fix-bug306683

Conversation

@electricface
Copy link
Member

@electricface electricface commented Mar 10, 2026

Fix highlight lingering after mouse leaves.
Add keyboard navigation:
- Up/Down: move from current hovered or last-hovered item
- Home/End: jump to first/last item
- Enter: confirm highlighted item in either mouse or keyboard mode
Mouse and keyboard modes switch seamlessly in both directions.

fix(ComboBox): 修复 ComboBox 的高亮残留问题,并为弹窗增加键盘导航功能

修复鼠标移出后高亮项仍然残留的问题
增加键盘导航支持:
- 上/下键:在当前悬停或最后悬停的选项间移动
- Home/End 键:跳转至第一个或最后一个选项
- 回车键:确认选中当前高亮的选项(支持鼠标或键盘操作模式)
鼠标模式与键盘模式可双向无缝切换

Log: 修复ComboBox的菜单项鼠标离开后高亮残留问题
Influence: ComboBox菜单项
PMS: BUG-304991

@deepin-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: electricface

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

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

ComboBox menu item highlighting is now gated on a new popup-level hover state so that the highlighted style clears when the mouse leaves the popup list area, implemented separately for Qt5 (MouseArea) and Qt6 (HoverHandler).

Class diagram for updated ComboBox popup and delegate highlighting (Qt5 and Qt6)

classDiagram
    class ComboBox {
        int currentIndex
        int highlightedIndex
        bool hoverEnabled
        Popup popup
    }

    class Popup {
        bool contentHovered
        int implicitWidth
        ArrowListView contentItem
    }

    class ArrowListView {
        int maxVisibleItems
        ListView view
    }

    class ListView {
        int currentIndex
        int highlightRangeMode
        int highlightMoveDuration
    }

    class DelegateItem {
        bool highlighted
        bool hoverEnabled
        bool autoExclusive
        bool checked
        +updateHighlighted(popupContentHovered, controlHighlightedIndex, index)
    }

    class MouseArea_Qt5 {
        bool hoverEnabled
        int acceptedButtons
        +onContainsMouseChanged(containsMouse)
    }

    class HoverHandler_Qt6 {
        +onHoveredChanged(hovered)
    }

    ComboBox --> Popup : owns
    Popup --> ArrowListView : contentItem
    ArrowListView --> ListView : view
    ArrowListView --> MouseArea_Qt5 : Qt5 only
    ArrowListView --> HoverHandler_Qt6 : Qt6 only
    ListView --> DelegateItem : provides delegates

    DelegateItem : highlighted = popup.contentHovered && control.highlightedIndex === index
    MouseArea_Qt5 : onContainsMouseChanged sets popup.contentHovered
    HoverHandler_Qt6 : onHoveredChanged sets popup.contentHovered
Loading

Flow diagram for ComboBox popup hover state driving delegate.highlighted

flowchart TD
    A[Mouse enters menu item area] --> B[Popup.contentHovered set to true]
    B --> C[Control.highlightedIndex updated for hovered item]
    C --> D[Delegate.highlighted evaluates popup.contentHovered && control.highlightedIndex === index]
    D --> E[Matching delegate shows highlighted style]

    F[Mouse leaves popup list area] --> G[Popup.contentHovered set to false]
    G --> H[Delegate.highlighted re-evaluates to false for all items]
    H --> I[All menu items clear highlighted style]
Loading

File-Level Changes

Change Details Files
Gate ComboBox delegate highlighting on popup list hover state to avoid lingering highlight after mouse exit.
  • Introduce popup.contentHovered boolean property to represent whether the mouse is inside the popup list area.
  • Update menu item delegate.highlighted binding to require both popup.contentHovered and control.highlightedIndex === index.
  • In the Qt5 ComboBox, add a full-area MouseArea with hoverEnabled and acceptedButtons: Qt.NoButton to keep popup.contentHovered in sync with pointer presence over the list content.
  • In the Qt6 ComboBox, add a HoverHandler in the list content to keep popup.contentHovered in sync with pointer hover state.
  • Update copyright year range in the Qt5 ComboBox QML file header.
src/qml/ComboBox.qml
qt6/src/qml/ComboBox.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

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:

  • In both Qt5 and Qt6 versions, tying delegate.highlighted to popup.contentHovered && control.highlightedIndex === index means keyboard-based navigation will no longer visually highlight items when the mouse is outside the popup; consider whether contentHovered should be ignored (or handled differently) for focus/keyboard navigation to preserve accessibility and expected behavior.
  • For the Qt6 HoverHandler, instead of imperatively setting popup.contentHovered in onHoveredChanged, consider binding popup.contentHovered directly to the handler’s hovered property to avoid potential desynchronization if additional hover logic is added later.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In both Qt5 and Qt6 versions, tying `delegate.highlighted` to `popup.contentHovered && control.highlightedIndex === index` means keyboard-based navigation will no longer visually highlight items when the mouse is outside the popup; consider whether `contentHovered` should be ignored (or handled differently) for focus/keyboard navigation to preserve accessibility and expected behavior.
- For the Qt6 `HoverHandler`, instead of imperatively setting `popup.contentHovered` in `onHoveredChanged`, consider binding `popup.contentHovered` directly to the handler’s `hovered` property to avoid potential desynchronization if additional hover logic is added later.

## Individual Comments

### Comment 1
<location path="qt6/src/qml/ComboBox.qml" line_range="39-42" />
<code_context>
         text: control.textRole ? (Array.isArray(control.model) ? modelData[control.textRole] : (model[control.textRole] === undefined ? modelData[control.textRole] : model[control.textRole])) : modelData
         icon.name: (control.iconNameRole && model[control.iconNameRole] !== undefined) ? model[control.iconNameRole] : null
-        highlighted: control.highlightedIndex === index
+        highlighted: popup.contentHovered && control.highlightedIndex === index
         hoverEnabled: control.hoverEnabled
         autoExclusive: true
</code_context>
<issue_to_address>
**issue (bug_risk):** In Qt 6, `popup` is both missing an `id` and not visible from the delegate context.

`popup.contentHovered` will always fail here: the `Popup` has no `id`, so `popup` is undefined in this file, and even with `id: popup` the delegate can’t see it because the popup isn’t in the delegate’s context. Instead, expose the hover state via something the delegate can access (for example, a `control.popupContentHovered` property updated by the popup) and bind `highlighted` to that property.
</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 39 to 42
highlighted: popup.contentHovered && control.highlightedIndex === index
hoverEnabled: control.hoverEnabled
autoExclusive: true
checked: control.currentIndex === index
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): In Qt 6, popup is both missing an id and not visible from the delegate context.

popup.contentHovered will always fail here: the Popup has no id, so popup is undefined in this file, and even with id: popup the delegate can’t see it because the popup isn’t in the delegate’s context. Instead, expose the hover state via something the delegate can access (for example, a control.popupContentHovered property updated by the popup) and bind highlighted to that property.

text: control.textRole ? (Array.isArray(control.model) ? modelData[control.textRole] : (model[control.textRole] === undefined ? modelData[control.textRole] : model[control.textRole])) : modelData
icon.name: (control.iconNameRole && model[control.iconNameRole] !== undefined) ? model[control.iconNameRole] : null
highlighted: control.highlightedIndex === index
highlighted: popup.contentHovered && control.highlightedIndex === index
Copy link
Contributor

Choose a reason for hiding this comment

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

popup是可以从外部赋值的,不应该在ComboBox里访问popup,可以参照 https://github.com/linuxdeepin/dtkdeclarative/pull/569/changes

@electricface electricface force-pushed the swt/fix-bug306683 branch 2 times, most recently from 957b205 to 0fcafee Compare March 11, 2026 02:36
@deepin-ci-robot
Copy link
Contributor

deepin pr auto review

这段代码主要实现了 ComboBox 组件中键盘导航与鼠标悬停的交互逻辑,并添加了一些调试日志。以下是对这段代码的审查意见,分为语法逻辑、代码质量、代码性能和代码安全四个方面:

1. 语法逻辑

  • 调试代码未清理
    • 代码中存在大量 console.log 调试信息(例如 [ComboBox] popup opened...)。这些日志在开发阶段很有用,但在提交代码前应该被移除或使用条件编译(如 Qt.debug)包裹,以免在生产环境中污染控制台或影响性能。
  • 调试矩形未移除
    • T.ComboBox 的顶部添加了一个带有绿色边框的 Rectanglez: 999)。这显然是用于调试布局的,必须在最终版本中删除,否则会严重影响UI外观。
  • 键盘导航初始逻辑
    • Keys.onPressedQt.Key_Down 处理中:if (control.keyboardNavIndex === -1) control.keyboardNavIndex = Math.min(popup.lastHoveredIndex + 1, control.count - 1)
    • 问题:当用户首次按下向下键时,如果 lastHoveredIndex 为 -1(默认值),计算结果为 0。逻辑上是通的,但依赖于 lastHoveredIndex 的状态。如果 lastHoveredIndex 没有被正确重置(例如在 onOpened 中),可能会导致行为不一致。目前的 onOpened 已重置,逻辑尚可。
  • HoverHandler 的作用域
    • HoverHandler 被放置在 ArrowListView(即 contentItem)中。这意味着只有当鼠标在 ListView 的内容区域移动时才会触发。如果 ListView 有内边距或背景区域,鼠标在这些区域移动可能不会触发 contentHovered 变为 true。这可能导致键盘导航状态无法正确切换回鼠标模式。建议确认 ArrowListView 的布局,或者考虑将 HoverHandler 放在 Popupbackground 或更高层级。

2. 代码质量

  • 魔法数字
    • z: 999 是一个典型的魔法数字。虽然这里用于调试,但正式代码中若需调整层级,建议定义一个清晰的常量或属性。
  • 代码重复
    • popup.contentHovered = falseKeys.onPressedUpDown 分支中重复出现。虽然逻辑简单,但可以考虑提取或在进入键盘导航模式时统一处理。
  • 状态管理复杂度
    • 引入了 keyboardNavIndexcontentHoveredlastHoveredIndex 三个状态来协调鼠标和键盘。这增加了状态机的复杂度。建议添加注释清晰地说明这三者之间的转换关系(例如:鼠标移动 -> contentHovered=true, keyboardNavIndex=-1;键盘按键 -> contentHovered=false, keyboardNavIndex 变化)。
  • 命名规范
    • keyboardNavIndex 命名清晰,但 contentHovered 稍显模糊。它实际上表示"鼠标是否正在悬停在列表项上导致的高亮"。可以考虑重命名为 isHoverActivemouseNavigationActive 以更准确地反映其含义。

3. 代码性能

  • 频繁的属性绑定与信号处理
    • highlighted 属性绑定了三元表达式,依赖 popup.contentHoveredcontrol.keyboardNavIndex。这两个属性在键盘和鼠标交互时会频繁变化。虽然 QML 的绑定系统很高效,但在极端情况下(如快速滚动或快速按键),可能会增加一定的计算开销。
    • Connections 监听 onHighlightedIndexChangedhighlightedIndex 在鼠标移动时会频繁变化,导致频繁执行函数内的逻辑(包括 console.log)。
  • Console.log 开销
    • 如前所述,大量的 console.log 在高频事件(如按键、鼠标移动)中触发,会显著降低运行时性能,尤其是在嵌入式设备或低端机器上。

4. 代码安全

  • 边界检查
    • Math.min(..., control.count - 1)Math.max(..., 0) 的使用很好,防止了数组越界。
    • Qt.Key_Return 处理中检查了 control.keyboardNavIndex >= 0,这是安全的。
  • 事件处理
    • event.accepted = true 被正确设置,防止事件冒泡导致意外的行为(如父组件滚动)。

改进建议总结

  1. 清理代码:立即删除顶部的绿色边框 Rectangle 和所有 console.log 语句。
  2. 优化状态管理:添加详细注释说明 keyboardNavIndexcontentHoveredlastHoveredIndex 的交互逻辑,确保维护者能理解状态转换的意图。
  3. HoverHandler 位置:确认 HoverHandler 的覆盖范围是否足够,或者是否需要在 Popup 层级处理鼠标离开事件以重置状态。
  4. 性能优化:移除高频事件中的日志输出。如果 highlighted 的绑定逻辑非常复杂,可以考虑在状态改变时显式设置而非纯绑定,但在当前场景下绑定可能是更优解。
  5. 代码复用:考虑将 Keys.onPressed 中的逻辑提取为一个函数,或者使用状态机模式来管理键盘/鼠标模式的切换,使代码结构更清晰。

修改后的代码片段示例(仅展示关键部分修改)

T.ComboBox {
    id: control
    // 删除了调试用的 Rectangle

    property string iconNameRole
    property string alertText
    // ...
    // 状态说明:
    // keyboardNavIndex: 键盘导航时的当前索引,-1 表示未激活键盘导航
    // contentHovered: 鼠标是否正在悬停控制高亮
    // lastHoveredIndex: 上次鼠标悬停的索引,用于从键盘切回鼠标时的参考
    property int keyboardNavIndex: -1
    // ...

    delegate: ItemDelegate {
        // ...
        highlighted: popup.contentHovered
            ? control.highlightedIndex === index
            : control.keyboardNavIndex === index
        onHighlightedChanged: {
            if (highlighted && popup.contentHovered)
                popup.lastHoveredIndex = index
        }
        // ...
    }

    popup: Popup {
        id: popup
        // ...
        onOpened: {
            // 初始化状态
            control.keyboardNavIndex = -1
            popup.lastHoveredIndex = -1
            // 如果需要,可以在这里设置初始 keyboardNavIndex,例如基于 currentIndex
            // control.keyboardNavIndex = control.currentIndex 
        }
        
        contentItem: ArrowListView {
            // ...
            Keys.onPressed: (event) => {
                // 移除了 console.log
                switch (event.key) {
                case Qt.Key_Down:
                    // 切换到键盘模式
                    popup.contentHovered = false
                    if (control.keyboardNavIndex === -1) {
                        // 从上次鼠标位置或当前选中位置开始
                        var startIdx = popup.lastHoveredIndex >= 0 ? popup.lastHoveredIndex : control.currentIndex
                        control.keyboardNavIndex = Math.min(startIdx + 1, control.count - 1)
                    } else {
                        control.keyboardNavIndex = Math.min(control.keyboardNavIndex + 1, control.count - 1)
                    }
                    event.accepted = true
                    break
                case Qt.Key_Up:
                    popup.contentHovered = false
                    if (control.keyboardNavIndex === -1) {
                        var startIdx = popup.lastHoveredIndex >= 0 ? popup.lastHoveredIndex : control.currentIndex
                        control.keyboardNavIndex = Math.max(startIdx - 1, 0)
                    } else {
                        control.keyboardNavIndex = Math.max(control.keyboardNavIndex - 1, 0)
                    }
                    event.accepted = true
                    break
                case Qt.Key_Return:
                case Qt.Key_Enter:
                    if (control.keyboardNavIndex >= 0) {
                        control.currentIndex = control.keyboardNavIndex
                        control.activated(control.keyboardNavIndex)
                        popup.close()
                    } else {
                        // 如果没有键盘导航索引,可能是在鼠标模式下按回车,通常关闭弹窗即可
                        // 或者根据需求选择是否执行 currentIndex
                        popup.close()
                    }
                    event.accepted = true
                    break
                }
            }
            
            HoverHandler {
                id: hoverHandler
                onHoveredChanged: {
                    if (hovered) {
                        popup.contentHovered = true
                        control.keyboardNavIndex = -1
                    } else {
                        // 注意:这里设置为 false 可能会导致鼠标移出列表一瞬间,
                        // 用户按键盘键时状态判断有微小延迟,但在当前逻辑下是必要的
                        popup.contentHovered = false
                    }
                }
            }
            
            Connections {
                target: control
                function onHighlightedIndexChanged() {
                    // 只有当鼠标真正在操作(highlightedIndex 变化)且当前不是键盘模式时,才强制切回鼠标模式
                    // 这里的逻辑是为了防止键盘操作修改了 highlightedIndex 导致误判
                    if (control.highlightedIndex >= 0 && !popup.contentHovered) {
                         // 此时说明鼠标移动导致了 highlightedIndex 变化,或者外部逻辑改变了它
                         // 如果是键盘操作,通常会先设置 keyboardNavIndex,不会触发这里的逻辑(因为 keyboardNavIndex != -1 时逻辑分支不同)
                         // 但为了安全,确保状态一致性
                         popup.contentHovered = true
                         control.keyboardNavIndex = -1
                    }
                }
            }
        }
        // ...
    }
}

… popup

Fix highlight lingering after mouse leaves.
Add keyboard navigation:
- Up/Down: move from current hovered or last-hovered item
- Home/End: jump to first/last item
- Enter: confirm highlighted item in either mouse or keyboard mode
Mouse and keyboard modes switch seamlessly in both directions.

fix(ComboBox): 修复 ComboBox 的高亮残留问题,并为弹窗增加键盘导航功能

修复鼠标移出后高亮项仍然残留的问题
增加键盘导航支持:
- 上/下键:在当前悬停或最后悬停的选项间移动
- Home/End 键:跳转至第一个或最后一个选项
- 回车键:确认选中当前高亮的选项(支持鼠标或键盘操作模式)
鼠标模式与键盘模式可双向无缝切换

Log: 修复ComboBox的菜单项鼠标离开后高亮残留问题
Influence: ComboBox菜单项
PMS: BUG-304991
@electricface electricface changed the title fix(ComboBox): fix the issue of highlighted state lingering after mouse leaves menu items fix(ComboBox): fix highlight lingering and add keyboard navigation in popup Mar 11, 2026
view.highlightRangeMode: ListView.ApplyRange
view.highlightMoveDuration: 0
Keys.priority: Keys.BeforeItem
Keys.onPressed: (event) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

这是手搓了个ComboBox的按键处理呀,无法借助qt里面的处理么,

@deepin-bot
Copy link
Contributor

deepin-bot bot commented Mar 12, 2026

TAG Bot

New tag: 6.7.36
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #587

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