Skip to content

Conversation

@yixinshark
Copy link
Contributor

@yixinshark yixinshark commented Feb 5, 2026

Use manual hit detection via QTextDocument in eventFilter to resolve unreliable linkHovered signal issues during vertical movement.

Log: optimize cursor behavior for bluetooth applet link
Pms: BUG-350071

Summary by Sourcery

Improve hover-based cursor feedback for the Bluetooth applet’s airplane mode link using manual hit detection.

Bug Fixes:

  • Fix unreliable cursor changes when hovering vertically over the airplane mode link in the Bluetooth applet.

Enhancements:

  • Use an event filter and QTextDocument-based hit testing on the airplane mode label to determine when to show a pointing hand cursor.

Use manual hit detection via QTextDocument in eventFilter
to resolve unreliable linkHovered signal issues during vertical movement.

Log: optimize cursor behavior for bluetooth applet link
Pms: BUG-350071
@deepin-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: yixinshark

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 Feb 5, 2026

Reviewer's Guide

Implements a custom eventFilter on the Bluetooth applet’s airplane mode label to manually detect link hover via QTextDocument hit-testing and update the mouse cursor reliably, working around issues with the standard linkHovered signal during vertical mouse movement.

Sequence diagram for custom cursor handling via eventFilter

sequenceDiagram
    actor User
    participant QEventSystem
    participant BluetoothApplet
    participant m_airplaneModeLabel
    participant QTextDocument

    User->>m_airplaneModeLabel: Move mouse
    m_airplaneModeLabel->>QEventSystem: QMouseEvent MouseMove
    QEventSystem->>BluetoothApplet: eventFilter(watched, event)
    BluetoothApplet->>BluetoothApplet: check watched == m_airplaneModeLabel
    BluetoothApplet->>m_airplaneModeLabel: get font(), text(), contentsRect()
    BluetoothApplet->>QTextDocument: create and configure
    BluetoothApplet->>QTextDocument: setDefaultFont(font)
    BluetoothApplet->>QTextDocument: setMarkdown(text)
    BluetoothApplet->>QTextDocument: setTextWidth(contentsRect.width)
    BluetoothApplet->>QTextDocument: documentLayout.anchorAt(textPos)
    QTextDocument-->>BluetoothApplet: anchor
    alt anchor is not empty
        BluetoothApplet->>m_airplaneModeLabel: setCursor(Qt::PointingHandCursor)
    else anchor is empty
        BluetoothApplet->>m_airplaneModeLabel: setCursor(Qt::ArrowCursor)
    end
    BluetoothApplet-->>QEventSystem: return QWidget::eventFilter(watched, event)
Loading

Class diagram for updated BluetoothApplet event filtering

classDiagram
    class QWidget
    class QObject

    class BluetoothApplet {
        +updateMinHeight(minHeight int) void
        +updateSize() void
        #eventFilter(watched QObject*, event QEvent*) bool
        -m_scrollArea QScrollArea*
        -m_contentWidget QWidget*
        -m_airplaneModeWidget QWidget*
        -m_airplaneModeLabel QLabel*
        -m_minHeight int
    }

    QObject <|-- QWidget
    QWidget <|-- BluetoothApplet

    class QLabel
    class QScrollArea

    BluetoothApplet o-- QLabel : m_airplaneModeLabel
    BluetoothApplet o-- QScrollArea : m_scrollArea
Loading

File-Level Changes

Change Details Files
Add an event filter on the airplane mode label to manage cursor changes based on manual link hit-testing instead of relying on linkHovered.
  • Install the BluetoothApplet instance as an event filter on m_airplaneModeLabel during UI initialization so it can intercept mouse events.
  • Override eventFilter in BluetoothApplet to handle Leave and MouseMove events for m_airplaneModeLabel, resetting the cursor on leave and updating it based on hit-testing on move.
  • Use a temporary QTextDocument configured with the label’s font, markdown text, and content width to perform anchorAt hit-testing at the mouse position mapped into the label’s contentsRect, switching between ArrowCursor and PointingHandCursor accordingly.
  • Include QCursor and QAbstractTextDocumentLayout headers to support cursor manipulation and QTextDocument layout usage.
plugins/dde-dock/bluetooth/componments/bluetoothapplet.cpp
plugins/dde-dock/bluetooth/componments/bluetoothapplet.h

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

  • Recreating a QTextDocument and recalculating layout on every MouseMove may be expensive; consider caching a prepared QTextDocument for m_airplaneModeLabel and only rebuilding it when the label text, font, or width changes.
  • The comment mentions handling Enter events, but the eventFilter only special-cases Leave and MouseMove; if you need consistent behavior when the cursor first enters the label, consider explicitly handling QEvent::Enter (or hover events) as well.
  • The hit detection always uses doc.setMarkdown(label->text()); if m_airplaneModeLabel might contain non-Markdown rich text or HTML in other contexts, you may need to branch based on the label’s text format to keep link detection consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Recreating a QTextDocument and recalculating layout on every MouseMove may be expensive; consider caching a prepared QTextDocument for m_airplaneModeLabel and only rebuilding it when the label text, font, or width changes.
- The comment mentions handling Enter events, but the eventFilter only special-cases Leave and MouseMove; if you need consistent behavior when the cursor first enters the label, consider explicitly handling QEvent::Enter (or hover events) as well.
- The hit detection always uses doc.setMarkdown(label->text()); if m_airplaneModeLabel might contain non-Markdown rich text or HTML in other contexts, you may need to branch based on the label’s text format to keep link detection consistent.

## Individual Comments

### Comment 1
<location> `plugins/dde-dock/bluetooth/componments/bluetoothapplet.cpp:388-393` </location>
<code_context>
+    if (watched == m_airplaneModeLabel) {
+        if (event->type() == QEvent::Leave) {
+            m_airplaneModeLabel->setCursor(Qt::ArrowCursor);
+        } else if (event->type() == QEvent::MouseMove) {
+            QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
+            QLabel *label = qobject_cast<QLabel*>(watched);
+            if (label) {
+                // Manual hit test using a temporary QTextDocument
+                QTextDocument doc;
+                doc.setDefaultFont(label->font());
+                doc.setMarkdown(label->text());
</code_context>

<issue_to_address>
**suggestion (performance):** QTextDocument construction and markdown parsing on every mouse move can be expensive.

Since this runs on every mouse move, repeatedly creating a QTextDocument and reparsing the markdown may be costly. Please consider caching a prepared QTextDocument (or parsed content) that is updated only when the label text changes, and reuse it for hit testing instead of recreating it each time.

Suggested implementation:

```cpp
            QLabel *label = qobject_cast<QLabel*>(watched);
            if (label) {
                // Manual hit test using a cached QTextDocument to avoid reparsing on every mouse move.
                // The document is updated only when the label's text/font/width changes.
                const QString currentText = label->text();
                const QFont currentFont = label->font();
                const qreal currentWidth = label->contentsRect().width();

                if (m_airplaneModeDocDirty
                    || m_cachedAirplaneModeText != currentText
                    || m_cachedAirplaneModeFont != currentFont
                    || !qFuzzyCompare(m_cachedAirplaneModeWidth, currentWidth)) {

                    m_airplaneModeDoc.setDefaultFont(currentFont);
                    m_airplaneModeDoc.setMarkdown(currentText);
                    m_airplaneModeDoc.setTextWidth(currentWidth);

                    m_cachedAirplaneModeText = currentText;
                    m_cachedAirplaneModeFont = currentFont;
                    m_cachedAirplaneModeWidth = currentWidth;
                    m_airplaneModeDocDirty = false;
                }

                QTextDocument &doc = m_airplaneModeDoc;

                // Adjust for alignment and margins if necessary.
                // DTipLabel/DLabel might have specific internal layouts, but assuming standard QLabel behavior:
                // Width must be set for word wrap to match (already set when cache is updated)

                // Map mouse position to document coordinates
                // Assuming text starts at contentsRect().topLeft()
                QPoint textPos = mouseEvent->pos() - label->contentsRect().topLeft();

```

To fully support this caching, the following changes are also needed elsewhere in the codebase:

1. **Add member variables to `BluetoothApplet` (likely in `bluetoothapplet.h`):**
   ```cpp
   private:
       QTextDocument m_airplaneModeDoc;
       QString m_cachedAirplaneModeText;
       QFont m_cachedAirplaneModeFont;
       qreal m_cachedAirplaneModeWidth = 0.0;
       bool m_airplaneModeDocDirty = true;
   ```

2. **Initialize state in the `BluetoothApplet` constructor (in `bluetoothapplet.cpp`):**
   - Ensure `m_airplaneModeDocDirty` is set to `true` (if not already via in-class initializer).
   - Optionally pre-fill the cache once `m_airplaneModeLabel` is constructed and its initial text is set.

3. **Mark the cache dirty when the label content changes:**
   - If `m_airplaneModeLabel` (or its actual type, e.g. `DLabel`) emits `textChanged(const QString &)`, connect it to a lambda/slot that sets `m_airplaneModeDocDirty = true;`.
   - If the label's font or size can change dynamically (e.g. on theme/scale changes), also mark the cache dirty in those code paths or rely on the width/font comparison already present.
   Example connection in the constructor after `m_airplaneModeLabel` is created:
   ```cpp
   connect(m_airplaneModeLabel, &QLabel::textChanged, this, [this]() {
       m_airplaneModeDocDirty = true;
   });
   ```
   Adjust the signal type if `m_airplaneModeLabel` is not a plain `QLabel` and uses a different signal.
</issue_to_address>

### Comment 2
<location> `plugins/dde-dock/bluetooth/componments/bluetoothapplet.cpp:386-390` </location>
<code_context>
+{
+    // Use eventFilter because standard linkHovered signal is unreliable for vertical movement
+    if (watched == m_airplaneModeLabel) {
+        if (event->type() == QEvent::Leave) {
+            m_airplaneModeLabel->setCursor(Qt::ArrowCursor);
+        } else if (event->type() == QEvent::MouseMove) {
+            QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
+            QLabel *label = qobject_cast<QLabel*>(watched);
+            if (label) {
+                // Manual hit test using a temporary QTextDocument
</code_context>

<issue_to_address>
**suggestion:** Redundant qobject_cast when watched is already known to be the target label.

Since this block only runs when `watched == m_airplaneModeLabel`, and `m_airplaneModeLabel` is (and should remain) a `QLabel`, the `qobject_cast<QLabel*>(watched)` and null-check aren’t needed. You can instead use `m_airplaneModeLabel` directly here and rely on that invariant, which simplifies the logic and avoids the extra RTTI cost.
</issue_to_address>

### Comment 3
<location> `plugins/dde-dock/bluetooth/componments/bluetoothapplet.cpp:382` </location>
<code_context>
     updateSize();
 }
+
+bool BluetoothApplet::eventFilter(QObject *watched, QEvent *event)
+{
+    // Use eventFilter because standard linkHovered signal is unreliable for vertical movement
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the new eventFilter-based hover handling to cache the text document and/or move the logic into a dedicated label helper or subclass to avoid per-mouse-move reconstruction and generic casting.

The new `eventFilter` adds non‑trivial complexity. You can keep the behavior but reduce complexity and per‑event cost by:

1. **Avoiding document reconstruction on every mouse move**  
2. **Avoiding repeated casting/branching in a generic `eventFilter`**  
3. **Encapsulating the logic in the label itself (or at least in a small helper)**

Concrete steps:

---

### 1. Cache a `QTextDocument` instead of rebuilding on every move

Add a member to `BluetoothApplet` (or to a dedicated label subclass, see next section):

```cpp
// in BluetoothApplet
private:
    std::unique_ptr<QTextDocument> m_airplaneModeDoc;
```

Initialize/update it when the label text or size changes:

```cpp
// after setting up m_airplaneModeLabel
m_airplaneModeLabel->installEventFilter(this);
updateAirplaneModeDoc();

connect(m_airplaneModeLabel, &QLabel::linkActivated, this, [this](const QString &) {
    // if text changes dynamically, call updateAirplaneModeDoc();
});

connect(m_airplaneModeLabel, &QLabel::windowTitleChanged, this, [this]() {
    updateAirplaneModeDoc();
});

connect(m_airplaneModeLabel, &QWidget::resizeEvent, this, [this]() {
    updateAirplaneModeDoc();
});
```

And the helper:

```cpp
void BluetoothApplet::updateAirplaneModeDoc()
{
    if (!m_airplaneModeLabel)
        return;

    if (!m_airplaneModeDoc)
        m_airplaneModeDoc = std::make_unique<QTextDocument>();

    m_airplaneModeDoc->setDefaultFont(m_airplaneModeLabel->font());
    m_airplaneModeDoc->setMarkdown(m_airplaneModeLabel->text());
    m_airplaneModeDoc->setTextWidth(m_airplaneModeLabel->contentsRect().width());
}
```

Now `eventFilter` becomes cheaper and simpler:

```cpp
bool BluetoothApplet::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == m_airplaneModeLabel) {
        if (event->type() == QEvent::Leave) {
            m_airplaneModeLabel->setCursor(Qt::ArrowCursor);
            return true;
        }

        if (event->type() == QEvent::MouseMove && m_airplaneModeDoc) {
            auto *mouseEvent = static_cast<QMouseEvent *>(event);
            const QPoint textPos = mouseEvent->pos() - m_airplaneModeLabel->contentsRect().topLeft();
            const QString anchor = m_airplaneModeDoc->documentLayout()->anchorAt(textPos);
            m_airplaneModeLabel->setCursor(anchor.isEmpty() ? Qt::ArrowCursor
                                                            : Qt::PointingHandCursor);
            return true;
        }
    }

    return QWidget::eventFilter(watched, event);
}
```

This keeps your workaround but removes the per‑move document construction and most casting branching.

---

### 2. Optionally encapsulate in a small label subclass

If you want to go one step further and de‑mix concerns from `BluetoothApplet`, you can move the hover logic into a small subclass used only for this label:

```cpp
class HoverLinkLabel : public DLabel {
    Q_OBJECT
public:
    explicit HoverLinkLabel(QWidget *parent = nullptr)
        : DLabel(parent)
    {
        setMouseTracking(true);
    }

protected:
    void resizeEvent(QResizeEvent *e) override
    {
        DLabel::resizeEvent(e);
        updateDoc();
    }

    void setText(const QString &text) override
    {
        DLabel::setText(text);
        updateDoc();
    }

    void leaveEvent(QEvent *e) override
    {
        setCursor(Qt::ArrowCursor);
        DLabel::leaveEvent(e);
    }

    void mouseMoveEvent(QMouseEvent *event) override
    {
        if (m_doc) {
            const QPoint textPos = event->pos() - contentsRect().topLeft();
            const QString anchor = m_doc->documentLayout()->anchorAt(textPos);
            setCursor(anchor.isEmpty() ? Qt::ArrowCursor : Qt::PointingHandCursor);
        }
        DLabel::mouseMoveEvent(event);
    }

private:
    void updateDoc()
    {
        if (!m_doc)
            m_doc = std::make_unique<QTextDocument>();

        m_doc->setDefaultFont(font());
        m_doc->setMarkdown(text());
        m_doc->setTextWidth(contentsRect().width());
    }

    std::unique_ptr<QTextDocument> m_doc;
};
```

Usage:

```cpp
m_airplaneModeLabel = new HoverLinkLabel(m_airplaneModeWidget);
m_airplaneModeLabel->setText(tr("Disable [Airplane Mode](#) first if you want to connect to a Bluetooth"));
m_airplaneModeLabel->setTextFormat(Qt::MarkdownText);
m_airplaneModeLabel->setWordWrap(true);
```

This keeps all existing behavior, but removes link‑hover complexity from `BluetoothApplet` and avoids repeated document construction or casting in a generic `eventFilter`.
</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 +388 to +393
} else if (event->type() == QEvent::MouseMove) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
QLabel *label = qobject_cast<QLabel*>(watched);
if (label) {
// Manual hit test using a temporary QTextDocument
QTextDocument doc;
Copy link

Choose a reason for hiding this comment

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

suggestion (performance): QTextDocument construction and markdown parsing on every mouse move can be expensive.

Since this runs on every mouse move, repeatedly creating a QTextDocument and reparsing the markdown may be costly. Please consider caching a prepared QTextDocument (or parsed content) that is updated only when the label text changes, and reuse it for hit testing instead of recreating it each time.

Suggested implementation:

            QLabel *label = qobject_cast<QLabel*>(watched);
            if (label) {
                // Manual hit test using a cached QTextDocument to avoid reparsing on every mouse move.
                // The document is updated only when the label's text/font/width changes.
                const QString currentText = label->text();
                const QFont currentFont = label->font();
                const qreal currentWidth = label->contentsRect().width();

                if (m_airplaneModeDocDirty
                    || m_cachedAirplaneModeText != currentText
                    || m_cachedAirplaneModeFont != currentFont
                    || !qFuzzyCompare(m_cachedAirplaneModeWidth, currentWidth)) {

                    m_airplaneModeDoc.setDefaultFont(currentFont);
                    m_airplaneModeDoc.setMarkdown(currentText);
                    m_airplaneModeDoc.setTextWidth(currentWidth);

                    m_cachedAirplaneModeText = currentText;
                    m_cachedAirplaneModeFont = currentFont;
                    m_cachedAirplaneModeWidth = currentWidth;
                    m_airplaneModeDocDirty = false;
                }

                QTextDocument &doc = m_airplaneModeDoc;

                // Adjust for alignment and margins if necessary.
                // DTipLabel/DLabel might have specific internal layouts, but assuming standard QLabel behavior:
                // Width must be set for word wrap to match (already set when cache is updated)

                // Map mouse position to document coordinates
                // Assuming text starts at contentsRect().topLeft()
                QPoint textPos = mouseEvent->pos() - label->contentsRect().topLeft();

To fully support this caching, the following changes are also needed elsewhere in the codebase:

  1. Add member variables to BluetoothApplet (likely in bluetoothapplet.h):

    private:
        QTextDocument m_airplaneModeDoc;
        QString m_cachedAirplaneModeText;
        QFont m_cachedAirplaneModeFont;
        qreal m_cachedAirplaneModeWidth = 0.0;
        bool m_airplaneModeDocDirty = true;
  2. Initialize state in the BluetoothApplet constructor (in bluetoothapplet.cpp):

    • Ensure m_airplaneModeDocDirty is set to true (if not already via in-class initializer).
    • Optionally pre-fill the cache once m_airplaneModeLabel is constructed and its initial text is set.
  3. Mark the cache dirty when the label content changes:

    • If m_airplaneModeLabel (or its actual type, e.g. DLabel) emits textChanged(const QString &), connect it to a lambda/slot that sets m_airplaneModeDocDirty = true;.
    • If the label's font or size can change dynamically (e.g. on theme/scale changes), also mark the cache dirty in those code paths or rely on the width/font comparison already present.
      Example connection in the constructor after m_airplaneModeLabel is created:
    connect(m_airplaneModeLabel, &QLabel::textChanged, this, [this]() {
        m_airplaneModeDocDirty = true;
    });

    Adjust the signal type if m_airplaneModeLabel is not a plain QLabel and uses a different signal.

Comment on lines +386 to +390
if (event->type() == QEvent::Leave) {
m_airplaneModeLabel->setCursor(Qt::ArrowCursor);
} else if (event->type() == QEvent::MouseMove) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
QLabel *label = qobject_cast<QLabel*>(watched);
Copy link

Choose a reason for hiding this comment

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

suggestion: Redundant qobject_cast when watched is already known to be the target label.

Since this block only runs when watched == m_airplaneModeLabel, and m_airplaneModeLabel is (and should remain) a QLabel, the qobject_cast<QLabel*>(watched) and null-check aren’t needed. You can instead use m_airplaneModeLabel directly here and rely on that invariant, which simplifies the logic and avoids the extra RTTI cost.

updateSize();
}

bool BluetoothApplet::eventFilter(QObject *watched, QEvent *event)
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring the new eventFilter-based hover handling to cache the text document and/or move the logic into a dedicated label helper or subclass to avoid per-mouse-move reconstruction and generic casting.

The new eventFilter adds non‑trivial complexity. You can keep the behavior but reduce complexity and per‑event cost by:

  1. Avoiding document reconstruction on every mouse move
  2. Avoiding repeated casting/branching in a generic eventFilter
  3. Encapsulating the logic in the label itself (or at least in a small helper)

Concrete steps:


1. Cache a QTextDocument instead of rebuilding on every move

Add a member to BluetoothApplet (or to a dedicated label subclass, see next section):

// in BluetoothApplet
private:
    std::unique_ptr<QTextDocument> m_airplaneModeDoc;

Initialize/update it when the label text or size changes:

// after setting up m_airplaneModeLabel
m_airplaneModeLabel->installEventFilter(this);
updateAirplaneModeDoc();

connect(m_airplaneModeLabel, &QLabel::linkActivated, this, [this](const QString &) {
    // if text changes dynamically, call updateAirplaneModeDoc();
});

connect(m_airplaneModeLabel, &QLabel::windowTitleChanged, this, [this]() {
    updateAirplaneModeDoc();
});

connect(m_airplaneModeLabel, &QWidget::resizeEvent, this, [this]() {
    updateAirplaneModeDoc();
});

And the helper:

void BluetoothApplet::updateAirplaneModeDoc()
{
    if (!m_airplaneModeLabel)
        return;

    if (!m_airplaneModeDoc)
        m_airplaneModeDoc = std::make_unique<QTextDocument>();

    m_airplaneModeDoc->setDefaultFont(m_airplaneModeLabel->font());
    m_airplaneModeDoc->setMarkdown(m_airplaneModeLabel->text());
    m_airplaneModeDoc->setTextWidth(m_airplaneModeLabel->contentsRect().width());
}

Now eventFilter becomes cheaper and simpler:

bool BluetoothApplet::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == m_airplaneModeLabel) {
        if (event->type() == QEvent::Leave) {
            m_airplaneModeLabel->setCursor(Qt::ArrowCursor);
            return true;
        }

        if (event->type() == QEvent::MouseMove && m_airplaneModeDoc) {
            auto *mouseEvent = static_cast<QMouseEvent *>(event);
            const QPoint textPos = mouseEvent->pos() - m_airplaneModeLabel->contentsRect().topLeft();
            const QString anchor = m_airplaneModeDoc->documentLayout()->anchorAt(textPos);
            m_airplaneModeLabel->setCursor(anchor.isEmpty() ? Qt::ArrowCursor
                                                            : Qt::PointingHandCursor);
            return true;
        }
    }

    return QWidget::eventFilter(watched, event);
}

This keeps your workaround but removes the per‑move document construction and most casting branching.


2. Optionally encapsulate in a small label subclass

If you want to go one step further and de‑mix concerns from BluetoothApplet, you can move the hover logic into a small subclass used only for this label:

class HoverLinkLabel : public DLabel {
    Q_OBJECT
public:
    explicit HoverLinkLabel(QWidget *parent = nullptr)
        : DLabel(parent)
    {
        setMouseTracking(true);
    }

protected:
    void resizeEvent(QResizeEvent *e) override
    {
        DLabel::resizeEvent(e);
        updateDoc();
    }

    void setText(const QString &text) override
    {
        DLabel::setText(text);
        updateDoc();
    }

    void leaveEvent(QEvent *e) override
    {
        setCursor(Qt::ArrowCursor);
        DLabel::leaveEvent(e);
    }

    void mouseMoveEvent(QMouseEvent *event) override
    {
        if (m_doc) {
            const QPoint textPos = event->pos() - contentsRect().topLeft();
            const QString anchor = m_doc->documentLayout()->anchorAt(textPos);
            setCursor(anchor.isEmpty() ? Qt::ArrowCursor : Qt::PointingHandCursor);
        }
        DLabel::mouseMoveEvent(event);
    }

private:
    void updateDoc()
    {
        if (!m_doc)
            m_doc = std::make_unique<QTextDocument>();

        m_doc->setDefaultFont(font());
        m_doc->setMarkdown(text());
        m_doc->setTextWidth(contentsRect().width());
    }

    std::unique_ptr<QTextDocument> m_doc;
};

Usage:

m_airplaneModeLabel = new HoverLinkLabel(m_airplaneModeWidget);
m_airplaneModeLabel->setText(tr("Disable [Airplane Mode](#) first if you want to connect to a Bluetooth"));
m_airplaneModeLabel->setTextFormat(Qt::MarkdownText);
m_airplaneModeLabel->setWordWrap(true);

This keeps all existing behavior, but removes link‑hover complexity from BluetoothApplet and avoids repeated document construction or casting in a generic eventFilter.

@deepin-ci-robot
Copy link

deepin pr auto review

这段代码主要实现了在 BluetoothApplet 中处理 m_airplaneModeLabel(一个 QLabel)的鼠标悬停事件,特别是针对 Markdown 文本中的链接(Anchor)进行检测,以便在鼠标悬停在链接上时显示手型光标。

以下是对该代码的审查意见,包括语法逻辑、代码质量、代码性能和代码安全四个方面:

1. 语法逻辑

  • 基本正确:代码逻辑基本通顺,能够通过 QTextDocument 手动计算鼠标位置是否处于链接上。
  • 潜在逻辑漏洞
    • 空指针检查qobject_cast<QLabel*>(watched) 返回了 label,代码检查了 if (label),但在后续代码中直接使用了 m_airplaneModeLabel。虽然 watched == m_airplaneModeLabel 的前置条件保证了它们是同一个对象,但为了代码的清晰性和防止未来修改引入错误,建议统一使用 label 指针。
    • Markdown 解析一致性QLabel 渲染文本的方式(尤其是富文本/Markdown)与 QTextDocument 可能存在细微差异。如果 m_airplaneModeLabel 使用了特定的样式表或对齐方式,doc.setMarkdown 可能无法完全复刻其布局,导致鼠标检测区域与实际显示区域不匹配。
    • 事件传播return QWidget::eventFilter(watched, event); 是正确的,确保了未被处理的事件能继续传递。

2. 代码质量

  • 性能隐患(关键)
    • 频繁的对象创建:在 QEvent::MouseMove 事件中,每次鼠标移动都会创建一个新的 QTextDocument 对象 doc,解析 Markdown,设置字体、宽度,并计算布局。这是一个非常耗时的操作。在用户快速移动鼠标时,这会触发大量高频调用,可能导致界面卡顿(UI 线程阻塞)。
  • 代码可读性与维护性
    • 硬编码逻辑:注释中提到 "Assuming text starts at contentsRect().topLeft()",这是一种假设。如果 QLabel 的缩进、边距或对齐方式发生变化(例如居中对齐),这个假设就会失效,导致检测错误。
    • 魔法值/类型转换:使用了 static_cast<QMouseEvent *>(event),虽然这里类型是确定的,但使用 qobject_cast 或检查 event 类型更符合 Qt 的安全风格(尽管 QEvent 不是 QObject,但在类型分支内 static_cast 是可接受的)。

3. 代码性能

  • 优化建议
    • 缓存 QTextDocument:不要在每次鼠标移动时都重新创建和解析 QTextDocument。建议在 BluetoothApplet 类中添加一个成员变量 QTextDocument *m_tempDoc(或 QScopedPointer),在 initUi 或文本更新时初始化它。在 eventFilter 中仅调用 anchorAt
    • 节流:如果无法避免重计算,可以考虑对鼠标移动事件进行节流,但这在 Qt 事件循环中实现较复杂,不如缓存方案直接有效。
    • 减少不必要的调用setCursor 虽然开销不大,但如果光标状态未改变(比如已经在链接上且还在链接上移动),重复调用 setCursor 是多余的。可以记录当前的光标状态,仅在状态改变时调用。

4. 代码安全

  • 内存安全
    • 当前代码中没有明显的内存泄漏,因为 doc 是栈上创建的局部变量。
    • 如果采纳性能优化建议,将 doc 改为成员变量,则需注意在对象析构时正确释放内存。
  • 类型安全
    • qobject_cast 用于 QLabel 是安全的,如果转换失败返回 nullptr,代码已有处理。

改进后的代码建议

为了解决性能问题和提高健壮性,建议修改如下:

头文件:

private:
    // ... 其他成员 ...
    QTextDocument m_tempDoc; // 使用成员变量缓存文档布局
    bool m_isHoveringLink = false; // 缓存悬停状态

源文件:

void BluetoothApplet::initUi()
{
    // ... 原有代码 ...
    
    // 初始化文档对象,用于后续的点击检测
    m_tempDoc.setDefaultFont(m_airplaneModeLabel->font());
    // 注意:这里需要确保 m_airplaneModeLabel 已经被创建且有文本
    // 如果文本是动态变化的,需要添加一个更新 m_tempDoc 的槽函数
    updateTempDoc(); 
    
    m_airplaneModeLabel->installEventFilter(this);
    // ...
}

// 新增:更新文档内容的辅助函数
void BluetoothApplet::updateTempDoc()
{
    if (m_airplaneModeLabel) {
        m_tempDoc.setMarkdown(m_airplaneModeLabel->text());
        // 宽度可能在 resize 事件中变化,建议也在 resizeEvent 中更新
        m_tempDoc.setTextWidth(m_airplaneModeLabel->contentsRect().width());
    }
}

bool BluetoothApplet::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == m_airplaneModeLabel) {
        if (event->type() == QEvent::Leave) {
            m_airplaneModeLabel->setCursor(Qt::ArrowCursor);
            m_isHoveringLink = false;
        } else if (event->type() == QEvent::MouseMove) {
            QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
            
            // 确保文档宽度是最新的(处理窗口大小改变的情况)
            if (m_tempDoc.textWidth() != m_airplaneModeLabel->contentsRect().width()) {
                m_tempDoc.setTextWidth(m_airplaneModeLabel->contentsRect().width());
            }

            // 计算相对位置
            // 注意:这里依然假设文本左对齐。如果是居中,需要计算偏移量。
            // 对于 DTipLabel/DLabel,通常需要根据 alignment 调整 textPos
            int offsetX = 0;
            if (m_airplaneModeLabel->alignment() & Qt::AlignHCenter) {
                offsetX = (m_airplaneModeLabel->contentsRect().width() - m_tempDoc.idealWidth()) / 2;
            } else if (m_airplaneModeLabel->alignment() & Qt::AlignRight) {
                offsetX = m_airplaneModeLabel->contentsRect().width() - m_tempDoc.idealWidth();
            }

            QPoint textPos = mouseEvent->pos() - m_airplaneModeLabel->contentsRect().topLeft();
            textPos.setX(textPos.x() - offsetX); // 应用对齐偏移

            const QString anchor = m_tempDoc.documentLayout()->anchorAt(textPos);
            const bool isHovering = !anchor.isEmpty();

            // 仅在状态改变时更新光标,减少系统调用
            if (isHovering != m_isHoveringLink) {
                m_airplaneModeLabel->setCursor(isHovering ? Qt::PointingHandCursor : Qt::ArrowCursor);
                m_isHoveringLink = isHovering;
            }
        }
    } else if (watched == m_airplaneModeLabel && event->type() == QEvent::Resize) {
        // 如果 Label 大小改变,需要更新文档宽度
        updateTempDoc();
    }

    return QWidget::eventFilter(watched, event);
}

总结

主要改进点:

  1. 性能优化:将 QTextDocument 的创建和解析移出鼠标移动事件,改为成员变量缓存。这将显著降低 CPU 占用。
  2. 状态缓存:增加 m_isHoveringLink 标志,避免重复设置相同的光标。
  3. 布局修正:增加了对文本对齐方式(居中/右对齐)的简单处理,使点击检测更准确。
  4. 动态更新:增加了对 Resize 事件的处理(逻辑上),确保文档宽度与 Label 宽度一致。
  5. 结构化:将文档更新逻辑提取为 updateTempDoc,便于维护。

@deepin-bot
Copy link

deepin-bot bot commented Feb 5, 2026

TAG Bot

New tag: 2.0.25
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #426

@yixinshark yixinshark marked this pull request as draft February 9, 2026 01:29
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.

2 participants