Skip to content

Fix: Optimize tooltip positioning logic.#580

Merged
deepin-bot[bot] merged 1 commit intolinuxdeepin:masterfrom
JWWTSL:master
Mar 4, 2026
Merged

Fix: Optimize tooltip positioning logic.#580
deepin-bot[bot] merged 1 commit intolinuxdeepin:masterfrom
JWWTSL:master

Conversation

@JWWTSL
Copy link
Contributor

@JWWTSL JWWTSL commented Mar 3, 2026

Log: The Popup is rendered on the window's Overlay layer and is not part of the content visual tree. As a result, the tooltip does not follow its anchor item during scrolling or window resizing, causing positional drift. To resolve this, AlertToolTip has been changed from a ToolTip (which uses Popup) to a regular Item, making it part of the content visual tree. This ensures it naturally scrolls with its parent, respects container clipping, and maintains correct positioning at all times.

PMS: bug-341973

Summary by Sourcery

Convert the alert tooltip to a regular control-based item to keep its position in sync with its target within the content visual tree.

Bug Fixes:

  • Ensure alert tooltips remain correctly positioned relative to their target during scrolling and window resizing instead of drifting.

Enhancements:

  • Add configurable timeout-based auto-hide behavior to alert tooltips.
  • Adjust tooltip entrance animation and connector positioning for the new control-based implementation.
  • Update the AlertToolTip component metadata to reflect the current copyright years.

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 3, 2026

Reviewer's Guide

Refactors AlertToolTip from a ToolTip/Popup-based component to a regular Control-based item that lives in the content visual tree, adds explicit text/timeout handling and a custom enter animation, and adjusts layout/connector positioning so the tooltip scrolls and clips correctly with its target.

Sequence diagram for updated tooltip positioning and scrolling behavior

sequenceDiagram
    actor User
    participant Viewport
    participant TargetItem
    participant NewAlertToolTip as AlertToolTip_ControlItem

    User->>TargetItem: hover
    TargetItem->>NewAlertToolTip: set target, text, timeout
    NewAlertToolTip->>NewAlertToolTip: compute _displayY = target.height
    NewAlertToolTip->>NewAlertToolTip: set y = _displayY + spacing
    NewAlertToolTip->>NewAlertToolTip: enterAnim.start()

    User->>Viewport: scroll or resize window
    Viewport->>TargetItem: layout updated (position, clipping)
    TargetItem->>NewAlertToolTip: position follows parent in content tree
    NewAlertToolTip->>NewAlertToolTip: y recomputed via binding to _displayY

    Note over NewAlertToolTip: Tooltip remains aligned and clipped with TargetItem

    User->>NewAlertToolTip: wait for timeout
    NewAlertToolTip->>NewAlertToolTip: Timer triggers onTriggered
    NewAlertToolTip->>NewAlertToolTip: visible = false
Loading

Updated class diagram for AlertToolTip Control-based implementation

classDiagram
    class AlertToolTip {
        +Item target
        +string text
        +int timeout
        +real _displayY
        +int x
        +int y
        +int topPadding
        +int bottomPadding
        +int leftPadding
        +int rightPadding
        +int implicitWidth
        +int implicitHeight
        +int z
    }

    class FloatingPanel {
        +int radius
        +int borderWidth
        +int margins
        +int normalColor
        +int borderColor
    }

    class ContentText {
        +Palette textColor
        +int horizontalAlignment
        +int verticalAlignment
        +int fontPixelSize
        +string fontFamily
        +bool wrapMode
    }

    class TooltipTimer {
        +int interval
        +bool running
        +onTriggered()
    }

    class EnterAnimation {
        +AlertToolTip target
        +string property
        +real from
        +real to
        +int duration
        +start()
    }

    class ConnectorBoxShadow {
        +Palette dropShadowColor
        +Palette backgroundColor
        +Palette borderColor
        +real x
        +real y
        +int width
        +int height
        +int shadowBlur
        +int radius
        +int cornerRadius
    }

    AlertToolTip o-- FloatingPanel : background
    AlertToolTip o-- ContentText : contentItem
    AlertToolTip o-- TooltipTimer : timer
    AlertToolTip o-- EnterAnimation : enterAnim
    AlertToolTip o-- ConnectorBoxShadow : connectorShadow
    AlertToolTip --> Item : target
    ContentText --> AlertToolTip : owner
    TooltipTimer --> AlertToolTip : controls_visibility
    EnterAnimation --> AlertToolTip : animates_y
    ConnectorBoxShadow --> AlertToolTip : anchored_to_tooltip
Loading

File-Level Changes

Change Details Files
Replace Popup-based ToolTip with a Control-based tooltip that is part of the content visual tree.
  • Change root QML type from ToolTip (Popup-based) to Control to ensure it follows its target during scrolling and resizing.
  • Introduce explicit properties for text, timeout, and an internal _displayY to control vertical positioning independent of ToolTip semantics.
  • Set a fixed z-order using D.DTK.TopOrder to keep the tooltip above normal content while remaining in the same visual tree.
qt6/src/qml/AlertToolTip.qml
Reimplement visibility timing and enter animation without ToolTip transitions.
  • Add an internal Timer bound to the timeout property to auto-hide the tooltip while visible, replacing ToolTip/Popup closePolicy behavior.
  • Trigger the enter animation via Component.onCompleted instead of ToolTip enter/exit transitions, since Control does not have those states.
  • Use a standalone NumberAnimation targeting the control.y property, animating from _displayY to _displayY + DS.Style.control.spacing.
qt6/src/qml/AlertToolTip.qml
Adjust layout and connector (BoxShadow) positioning to match the new layout model.
  • Compute y using _displayY + DS.Style.control.spacing instead of directly from target.height, making it resilient to future positioning changes.
  • Update the BoxShadow connector y-offset to -height * 0.75, removing dependence on margins and padding that are no longer available on Control.
  • Keep implicit sizing and padding based on DS.Style while ensuring width is clamped to the target width for proper alignment with the anchor.
qt6/src/qml/AlertToolTip.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 2 issues, and left some high level feedback:

  • The new text property on Control is never bound to contentText.text, so the tooltip content will always be empty; consider wiring contentText.text: control.text (or equivalent) to preserve behavior.
  • The enter animation is only triggered once via Component.onCompleted, so reopening the tooltip by toggling visible will not re-run the animation; consider starting the animation in onVisibleChanged when becoming visible instead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `text` property on `Control` is never bound to `contentText.text`, so the tooltip content will always be empty; consider wiring `contentText.text: control.text` (or equivalent) to preserve behavior.
- The enter animation is only triggered once via `Component.onCompleted`, so reopening the tooltip by toggling `visible` will not re-run the animation; consider starting the animation in `onVisibleChanged` when becoming visible instead.

## Individual Comments

### Comment 1
<location path="qt6/src/qml/AlertToolTip.qml" line_range="52" />
<code_context>
-        // NumberAnimation { properties: "opacity"; from: 0.0; to: 1.0; duration: 200 }
-        NumberAnimation { properties: "y"; from: control.target.height; to: control.target.height + DS.Style.control.spacing; duration: 200 }
-    }
+    Component.onCompleted: enterAnim.start()

-    exit: Transition {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The enter animation only runs once on component creation, not on subsequent show/hide cycles.

With `Component.onCompleted: enterAnim.start()`, the enter animation only runs once when the tooltip instance is created. Re-showing the same control (e.g. toggling `visible` or reusing the instance) won’t trigger the enter animation again, changing the previous behavior. To match the old behavior, start `enterAnim` in response to visibility or state changes instead of only on creation.

Suggested implementation:

```
    onVisibleChanged: if (visible) enterAnim.restart()

```

If the tooltip uses a different property or state to represent "shown" (e.g. `control.visible`, a `state` change, or `control.toolTipVisible`), replace `visible` in the handler with the appropriate property or attach the handler to the root/control item that actually changes visibility. This ensures the enter animation runs on every show cycle, matching the old `enter` transition behavior.
</issue_to_address>

### Comment 2
<location path="qt6/src/qml/AlertToolTip.qml" line_range="54-60" />
<code_context>
-    exit: Transition {
-        // NumberAnimation { properties: "opacity"; from: 1.0; to: 0.0 }
-        NumberAnimation { properties: "y"; from: control.target.height + DS.Style.control.spacing ; to: control.target.height }
+    NumberAnimation {
+        id: enterAnim
+        target: control
+        property: "y"
+        from: control._displayY
+        to: control._displayY + DS.Style.control.spacing
+        duration: 200
     }

</code_context>
<issue_to_address>
**question:** Only the enter animation remains; the exit animation was removed, changing the close behavior.

With only the enter `NumberAnimation`, the tooltip now vanishes instantly (e.g., when the timeout triggers) instead of animating out. If that behavior isn’t desired and you want consistent UX, consider reintroducing a matching exit animation or using states/transitions to preserve the previous behavior.
</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.

// NumberAnimation { properties: "opacity"; from: 0.0; to: 1.0; duration: 200 }
NumberAnimation { properties: "y"; from: control.target.height; to: control.target.height + DS.Style.control.spacing; duration: 200 }
}
Component.onCompleted: enterAnim.start()
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): The enter animation only runs once on component creation, not on subsequent show/hide cycles.

With Component.onCompleted: enterAnim.start(), the enter animation only runs once when the tooltip instance is created. Re-showing the same control (e.g. toggling visible or reusing the instance) won’t trigger the enter animation again, changing the previous behavior. To match the old behavior, start enterAnim in response to visibility or state changes instead of only on creation.

Suggested implementation:

    onVisibleChanged: if (visible) enterAnim.restart()

If the tooltip uses a different property or state to represent "shown" (e.g. control.visible, a state change, or control.toolTipVisible), replace visible in the handler with the appropriate property or attach the handler to the root/control item that actually changes visibility. This ensures the enter animation runs on every show cycle, matching the old enter transition behavior.

Comment on lines +54 to +60
NumberAnimation {
id: enterAnim
target: control
property: "y"
from: control._displayY
to: control._displayY + DS.Style.control.spacing
duration: 200
Copy link

Choose a reason for hiding this comment

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

question: Only the enter animation remains; the exit animation was removed, changing the close behavior.

With only the enter NumberAnimation, the tooltip now vanishes instantly (e.g., when the timeout triggers) instead of animating out. If that behavior isn’t desired and you want consistent UX, consider reintroducing a matching exit animation or using states/transitions to preserve the previous behavior.

}

contentItem: Text {
id: contentText
Copy link
Contributor

Choose a reason for hiding this comment

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

这个id不需要了吧,

exit: Transition {
// NumberAnimation { properties: "opacity"; from: 1.0; to: 0.0 }
NumberAnimation { properties: "y"; from: control.target.height + DS.Style.control.spacing ; to: control.target.height }
NumberAnimation {
Copy link
Contributor

Choose a reason for hiding this comment

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

是不是不需要定义这条个_displayY,这个直接用动画Behavior on y 就行了吧,

@JWWTSL JWWTSL force-pushed the master branch 2 times, most recently from 5acf10f to 7d7cac4 Compare March 4, 2026 02:07
Timer {
interval: control.timeout
running: control.timeout > 0 && control.visible
onTriggered: control.visible = false
Copy link
Contributor

Choose a reason for hiding this comment

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

这样弄的话,之前通过visible绑定的就丢掉了,
另外,查看下需不需要加上show这种之前popup提供的接口,兼容一下,

@JWWTSL JWWTSL force-pushed the master branch 2 times, most recently from e52a343 to 8111b24 Compare March 4, 2026 03:12
// from binding context during recreation.
Loader {
active: showAlert && alertText.length !== 0
active: alertText.length !== 0
Copy link
Contributor

Choose a reason for hiding this comment

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

重新创建会导致文本错误?

}

onVisibleChanged: {
if (visible) {
Copy link
Contributor

Choose a reason for hiding this comment

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

这里是重置_expired吧,不管什么时候都需要的话,优化下代码,

NumberAnimation { duration: 200 }
}
opacity: _shown ? 1 : 0
enabled: _shown
Copy link
Contributor

Choose a reason for hiding this comment

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

enabled不需要吧,

Log: The Popup is rendered on the window's Overlay layer and is not part of the content visual tree. As a result, the tooltip does not follow its anchor item during scrolling or window resizing, causing positional drift. To resolve this, AlertToolTip has been changed from a ToolTip (which uses Popup) to a regular Item, making it part of the content visual tree. This ensures it naturally scrolls with its parent, respects container clipping, and maintains correct positioning at all times.

PMS: bug-341973
@deepin-ci-robot
Copy link
Contributor

deepin pr auto review

这段代码主要对 AlertToolTip.qml 进行了重构,将其从继承自 ToolTip 改为继承自 Control,并修改了 EditPanel.qmlLoader 的激活逻辑。以下是对这段 diff 的详细审查和改进意见:

1. 语法逻辑审查

  • AlertToolTip.qml:
    • 基类变更: 从 ToolTip 变更为 Control。这是一个重大的逻辑变更。ToolTip 原生处理了悬停、延迟显示、自动关闭等行为。改为 Control 后,必须手动实现这些逻辑。
    • 显示逻辑: 引入了 _expired 属性和 Timer 来控制超时隐藏。逻辑上看起来是自洽的:当 visible 变为 true 或 text 变化时重置状态并启动计时器。
    • Loader 逻辑 (EditPanel.qml): active 属性从 showAlert && alertText.length !== 0 变为 alertText.length !== 0。配合 AlertToolTipvisible 属性(推测由外部绑定到 showAlert),这确实可以防止组件销毁和重建导致的上下文丢失问题,逻辑上是合理的。

2. 代码质量

  • 命名规范:
    • _expired_shown 使用了下划线前缀,通常表示私有属性。在 QML 中这是常见的做法,但要注意不要与 Qt 内部属性冲突。
  • 代码可读性:
    • y 的计算表达式 (target ? target.height : 0) + (_shown ? DS.Style.control.spacing : 0) 比原版更清晰,明确区分了高度基准和间距。
    • implicitWidth 增加了对 target 为空的防御性判断,提高了鲁棒性。
  • 注释:
    • EditPanel.qml 中添加的注释非常有价值,解释了修改 Loader 逻辑的具体原因(避免重建导致绑定上下文错误),这对后续维护很有帮助。

3. 代码性能

  • Loader 优化 (EditPanel.qml):
    • 优点: 仅在 alertText 为空时销毁组件。这意味着在频繁切换 showAlert 状态(例如鼠标快速进出)但文本不变的情况下,避免了组件的重复实例化和销毁,减少了 GC 压力和 CPU 开销。
    • 潜在问题: 只要 alertText 不为空,Loader 就会保持活跃。如果 alertText 长期不为空但 showAlertfalseAlertToolTip 组件虽然不可见,但仍然存在于内存中。考虑到 AlertToolTip 通常比较轻量,这是一个合理的权衡。
  • 动画与绑定:
    • Behavior on yopacity 的绑定会在 _shown 变化时触发。这是标准的 QML 动画实现方式,性能开销可控。
  • Timer:
    • autoHideTimer 的逻辑是高效的,只在必要时运行。

4. 代码安全

  • 空指针检查:
    • implicitWidth 中增加了 target ? ... : ... 的判断,防止了 target 为 null 时的崩溃风险。
  • 内存泄漏:
    • 目前没有明显的内存泄漏风险。Loader 负责管理 AlertToolTip 的生命周期。
  • 版权年份:
    • 将版权年份从 2021-2022 更新至 2026 是标准的维护操作。

改进意见

  1. AlertToolTip.qml - 防御性编程与 z-index:

    • y 的计算中,如果 target 为 null,y 将为 0。这可能导致 ToolTip 显示在窗口左上角。建议在 target 为 null 时直接隐藏组件或设置 visible: false,或者给出默认位置。
    • z: D.DTK.TopOrder 确保了层级,这是好的做法。
  2. AlertToolTip.qml - 动画与可见性:

    • 代码中移除了 enterexit Transition,转而使用 Behavior on yopacity 绑定。
    • 注意: 当 visible 变为 false 时,opacity 会变为 0,y 会归位。如果 visible 变化非常快,动画可能会被截断或看起来不连贯。建议检查是否需要保留 Transition 来处理进入/退出的完整动画序列,或者确保 visible 的切换逻辑不会过于频繁。
    • 建议: 考虑添加 enabled: _shown,这已经实现了,很好。这可以防止在隐藏状态下接收事件。
  3. AlertToolTip.qml - 布局计算:

    • y: -height * 0.75 (BoxShadow) 的计算依赖于 height。这是一个相对布局,是合理的。
    • 原代码中的 control.topMargin 被移除了。由于新代码中 Control 不再有 margins 属性(原 ToolTip 有),这可能是故意的。但需确认视觉上 BoxShadow 的位置是否正确。
  4. EditPanel.qml - Loader 的 active 属性:

    • 当前逻辑:active: alertText.length !== 0
    • 潜在风险: 如果 alertText 是一个频繁变化的绑定(例如每秒更新),且偶尔为空,会导致 Loader 频繁销毁和重建,反而影响性能。
    • 建议: 如果 alertText 变化不频繁,当前逻辑是最佳实践。如果变化频繁,可能需要更复杂的逻辑(例如延迟销毁)。

总结

这段代码重构总体上是积极且高质量的。它解决了 ToolTip 组件在某些复杂场景下的生命周期管理问题(通过 Loader 优化),并提供了更灵活的显示控制(通过 Control 自定义)。

主要优点:

  • 解决了 EditPanel 中组件重建导致的文本上下文错误。
  • 提供了更平滑的动画控制(通过 Behavior)。
  • 增强了代码的鲁棒性(空指针检查)。

建议关注点:

  • 确保 target 为 null 时的行为符合预期。
  • 验证快速切换 visible 时的动画效果。
  • 确认 BoxShadow 的位置在移除 topMargin 后依然准确。

@deepin-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: 18202781743, JWWTSL

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

@JWWTSL
Copy link
Contributor Author

JWWTSL commented Mar 4, 2026

/forcemerge

@deepin-bot
Copy link
Contributor

deepin-bot bot commented Mar 4, 2026

This pr force merged! (status: unstable)

@deepin-bot deepin-bot bot merged commit 1d51c87 into linuxdeepin:master Mar 4, 2026
15 of 19 checks passed
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