Merge pull request #12329 from Dentomologist/balloontip_fix_premature_close_on_balloontip_hover

BalloonTip: Don't hide when the BalloonTip blocks the cursor
This commit is contained in:
JMC47
2025-09-28 14:03:16 -04:00
committed by GitHub
3 changed files with 108 additions and 14 deletions

View File

@@ -5,11 +5,11 @@
#include <memory>
#include <QApplication>
#include <QBitmap>
#include <QBrush>
#include <QCursor>
#include <QFont>
#include <QGuiApplication>
#include <QLabel>
#include <QPainter>
#include <QPainterPath>
@@ -25,11 +25,17 @@
#include <QToolTip>
#endif
#include "DolphinQt/QtUtils/QueueOnObject.h"
#include "DolphinQt/Settings.h"
namespace
{
std::unique_ptr<BalloonTip> s_the_balloon_tip = nullptr;
// Remember the parent ToolTipWidget so cursor-related events can see whether the cursor is inside
// the parent's bounding box or not. Use this variable instead of BalloonTip's parent() member
// because the ToolTipWidget isn't responsible for deleting the BalloonTip and so doesn't set its
// parent member.
QWidget* s_parent = nullptr;
} // namespace
void BalloonTip::ShowBalloon(const QString& title, const QString& message,
@@ -53,6 +59,7 @@ void BalloonTip::ShowBalloon(const QString& title, const QString& message,
void BalloonTip::HideBalloon()
{
s_parent = nullptr;
#if defined(__APPLE__)
QToolTip::hideText();
#else
@@ -66,6 +73,9 @@ void BalloonTip::HideBalloon()
BalloonTip::BalloonTip(PrivateTag, const QString& title, QString message, QWidget* const parent)
: QWidget(nullptr, Qt::ToolTip)
{
s_parent = parent;
setMouseTracking(true);
QColor window_color;
QColor text_color;
QColor dolphin_emphasis;
@@ -113,10 +123,61 @@ BalloonTip::BalloonTip(PrivateTag, const QString& title, QString message, QWidge
create_label(message);
}
void BalloonTip::paintEvent(QPaintEvent*)
bool BalloonTip::IsCursorInsideWidgetBoundingBox(const QWidget& widget)
{
const QPoint local_cursor_position = widget.mapFromGlobal(QCursor::pos());
return widget.rect().contains(local_cursor_position);
}
bool BalloonTip::IsCursorOnBalloonTip()
{
return s_the_balloon_tip != nullptr &&
QApplication::widgetAt(QCursor::pos()) == s_the_balloon_tip.get();
}
bool BalloonTip::IsWidgetBalloonTipActive(const QWidget& widget)
{
return &widget == s_parent;
}
// Hiding the balloon causes the BalloonTip widget to be deleted. Triggering that deletion while
// inside a BalloonTip event handler leads to a use-after-free crash or worse, so queue the deletion
// for later.
static void QueueHideBalloon()
{
QueueOnObject(s_parent, BalloonTip::HideBalloon);
}
void BalloonTip::enterEvent(QEnterEvent* const event)
{
if (!IsCursorInsideWidgetBoundingBox(*s_parent))
QueueHideBalloon();
QWidget::enterEvent(event);
}
void BalloonTip::mouseMoveEvent(QMouseEvent* const event)
{
if (!IsCursorInsideWidgetBoundingBox(*s_parent))
QueueHideBalloon();
QWidget::mouseMoveEvent(event);
}
void BalloonTip::leaveEvent(QEvent* const event)
{
if (QApplication::widgetAt(QCursor::pos()) != s_parent)
QueueHideBalloon();
QWidget::leaveEvent(event);
}
void BalloonTip::paintEvent(QPaintEvent* const event)
{
QPainter painter(this);
painter.drawPixmap(rect(), m_pixmap);
QWidget::paintEvent(event);
}
void BalloonTip::UpdateBoundsAndRedraw(const QPoint& target_arrow_tip_position,

View File

@@ -7,6 +7,9 @@
#include <QPixmap>
#include <QWidget>
class QEnterEvent;
class QEvent;
class QMouseEvent;
class QPaintEvent;
class QPoint;
class QString;
@@ -29,17 +32,22 @@ public:
const QPoint& target_arrow_tip_position, QWidget* parent,
ShowArrow show_arrow = ShowArrow::Yes, int border_width = 1);
static void HideBalloon();
static bool IsCursorInsideWidgetBoundingBox(const QWidget& widget);
static bool IsCursorOnBalloonTip();
static bool IsWidgetBalloonTipActive(const QWidget& widget);
BalloonTip(PrivateTag, const QString& title, QString message, QWidget* parent);
protected:
void enterEvent(QEnterEvent* event) override;
void leaveEvent(QEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void paintEvent(QPaintEvent* event) override;
private:
void UpdateBoundsAndRedraw(const QPoint& target_arrow_tip_position, ShowArrow show_arrow,
int border_width);
protected:
void paintEvent(QPaintEvent*) override;
private:
QColor m_border_color;
QPixmap m_pixmap;
};

View File

@@ -9,6 +9,11 @@
#include "DolphinQt/Config/ToolTipControls/BalloonTip.h"
class QEnterEvent;
class QEvent;
class QHideEvent;
class QTimerEvent;
constexpr int TOOLTIP_DELAY = 300;
template <class Derived>
@@ -22,28 +27,48 @@ public:
void SetDescription(QString description) { m_description = std::move(description); }
private:
void enterEvent(QEnterEvent* event) override
void enterEvent(QEnterEvent* const event) override
{
if (m_timer_id)
return;
m_timer_id = this->startTimer(TOOLTIP_DELAY);
// If the timer is already running, or the cursor is reentering the ToolTipWidget after having
// hovered over the BalloonTip, don't start a new timer.
if (!m_timer_id && !BalloonTip::IsWidgetBalloonTipActive(*this))
m_timer_id = this->startTimer(TOOLTIP_DELAY);
Derived::enterEvent(event);
}
void leaveEvent(QEvent* event) override { KillAndHide(); }
void hideEvent(QHideEvent* event) override { KillAndHide(); }
void leaveEvent(QEvent* const event) override
{
// If the cursor would still be inside the ToolTipWidget but the BalloonTip is covering that
// part of it, keep the BalloonTip open. In that case the BalloonTip will then track the cursor
// and close itself if it leaves the bounding box of this ToolTipWidget.
if (!BalloonTip::IsCursorInsideWidgetBoundingBox(*this) || !BalloonTip::IsCursorOnBalloonTip())
KillTimerAndHideBalloon();
void timerEvent(QTimerEvent* event) override
Derived::leaveEvent(event);
}
void hideEvent(QHideEvent* const event) override
{
KillTimerAndHideBalloon();
Derived::hideEvent(event);
}
void timerEvent(QTimerEvent* const event) override
{
this->killTimer(*m_timer_id);
m_timer_id.reset();
BalloonTip::ShowBalloon(m_title, m_description,
this->parentWidget()->mapToGlobal(GetToolTipPosition()), this);
Derived::timerEvent(event);
}
virtual QPoint GetToolTipPosition() const = 0;
void KillAndHide()
void KillTimerAndHideBalloon()
{
if (m_timer_id)
{