BalloonTip: Don't hide when BalloonTip blocks the cursor

Keep the BalloonTip open when the BalloonTip's arrow prevents the cursor
from being inside the spawning ToolTipWidget, which triggers the
ToolTipWidget's leaveEvent and would previously close the BalloonTip.

When that happens track the cursor until it either leaves the
ToolTipWidget's bounding box or leaves the BalloonTip and goes back to
the ToolTipWidget, and respectively close the BalloonTip or leave it
open.
This commit is contained in:
Dentomologist 2023-11-26 13:26:52 -08:00
parent 20665ebce7
commit 821727b7a7
3 changed files with 77 additions and 10 deletions

View File

@ -5,11 +5,11 @@
#include <memory> #include <memory>
#include <QApplication>
#include <QBitmap> #include <QBitmap>
#include <QBrush> #include <QBrush>
#include <QCursor> #include <QCursor>
#include <QFont> #include <QFont>
#include <QGuiApplication>
#include <QLabel> #include <QLabel>
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
@ -25,11 +25,17 @@
#include <QToolTip> #include <QToolTip>
#endif #endif
#include "DolphinQt/QtUtils/QueueOnObject.h"
#include "DolphinQt/Settings.h" #include "DolphinQt/Settings.h"
namespace namespace
{ {
std::unique_ptr<BalloonTip> s_the_balloon_tip = nullptr; 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 } // namespace
void BalloonTip::ShowBalloon(const QString& title, const QString& message, 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() void BalloonTip::HideBalloon()
{ {
s_parent = nullptr;
#if defined(__APPLE__) #if defined(__APPLE__)
QToolTip::hideText(); QToolTip::hideText();
#else #else
@ -66,6 +73,9 @@ void BalloonTip::HideBalloon()
BalloonTip::BalloonTip(PrivateTag, const QString& title, QString message, QWidget* const parent) BalloonTip::BalloonTip(PrivateTag, const QString& title, QString message, QWidget* const parent)
: QWidget(nullptr, Qt::ToolTip) : QWidget(nullptr, Qt::ToolTip)
{ {
s_parent = parent;
setMouseTracking(true);
QColor window_color; QColor window_color;
QColor text_color; QColor text_color;
QColor dolphin_emphasis; QColor dolphin_emphasis;
@ -113,6 +123,49 @@ BalloonTip::BalloonTip(PrivateTag, const QString& title, QString message, QWidge
create_label(message); create_label(message);
} }
bool BalloonTip::IsCursorInsideWidgetBoundingBox(const QWidget& widget)
{
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*)
{
if (!IsCursorInsideWidgetBoundingBox(*s_parent))
QueueHideBalloon();
}
void BalloonTip::mouseMoveEvent(QMouseEvent*)
{
if (!IsCursorInsideWidgetBoundingBox(*s_parent))
QueueHideBalloon();
}
void BalloonTip::leaveEvent(QEvent*)
{
if (QApplication::widgetAt(QCursor::pos()) != s_parent)
QueueHideBalloon();
}
void BalloonTip::paintEvent(QPaintEvent*) void BalloonTip::paintEvent(QPaintEvent*)
{ {
QPainter painter(this); QPainter painter(this);

View File

@ -29,17 +29,22 @@ public:
const QPoint& target_arrow_tip_position, QWidget* parent, const QPoint& target_arrow_tip_position, QWidget* parent,
ShowArrow show_arrow = ShowArrow::Yes, int border_width = 1); ShowArrow show_arrow = ShowArrow::Yes, int border_width = 1);
static void HideBalloon(); 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); BalloonTip(PrivateTag, const QString& title, QString message, QWidget* parent);
protected:
void enterEvent(QEnterEvent*) override;
void mouseMoveEvent(QMouseEvent*) override;
void leaveEvent(QEvent*) override;
void paintEvent(QPaintEvent*) override;
private: private:
void UpdateBoundsAndRedraw(const QPoint& target_arrow_tip_position, ShowArrow show_arrow, void UpdateBoundsAndRedraw(const QPoint& target_arrow_tip_position, ShowArrow show_arrow,
int border_width); int border_width);
protected:
void paintEvent(QPaintEvent*) override;
private:
QColor m_border_color; QColor m_border_color;
QPixmap m_pixmap; QPixmap m_pixmap;
}; };

View File

@ -22,17 +22,26 @@ public:
void SetDescription(QString description) { m_description = std::move(description); } void SetDescription(QString description) { m_description = std::move(description); }
private: private:
void enterEvent(QEnterEvent* event) override void enterEvent(QEnterEvent*) override
{ {
if (m_timer_id) // If the timer is already running, or the cursor is reentering the ToolTipWidget after having
// hovered over the BalloonTip, don't do anything.
if (m_timer_id || BalloonTip::IsWidgetBalloonTipActive(*this))
return; return;
m_timer_id = this->startTimer(TOOLTIP_DELAY); m_timer_id = this->startTimer(TOOLTIP_DELAY);
} }
void leaveEvent(QEvent* event) override { KillAndHide(); } void leaveEvent(QEvent*) override
void hideEvent(QHideEvent* event) override { KillAndHide(); } {
// 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())
KillAndHide();
}
void hideEvent(QHideEvent*) override { KillAndHide(); }
void timerEvent(QTimerEvent* event) override void timerEvent(QTimerEvent*) override
{ {
this->killTimer(*m_timer_id); this->killTimer(*m_timer_id);
m_timer_id.reset(); m_timer_id.reset();