diff --git a/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.cpp b/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.cpp index 52033a9445..43c41d1e5d 100644 --- a/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.cpp +++ b/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.cpp @@ -5,11 +5,11 @@ #include +#include #include #include #include #include -#include #include #include #include @@ -25,11 +25,17 @@ #include #endif +#include "DolphinQt/QtUtils/QueueOnObject.h" #include "DolphinQt/Settings.h" namespace { std::unique_ptr 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,6 +123,49 @@ BalloonTip::BalloonTip(PrivateTag, const QString& title, QString message, QWidge 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*) { QPainter painter(this); diff --git a/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h b/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h index c5df392df9..62253da7b8 100644 --- a/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h +++ b/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h @@ -29,17 +29,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*) override; + void mouseMoveEvent(QMouseEvent*) override; + void leaveEvent(QEvent*) override; + void paintEvent(QPaintEvent*) 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; }; diff --git a/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h b/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h index 727ae11462..cf41c19118 100644 --- a/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h +++ b/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h @@ -22,17 +22,26 @@ public: void SetDescription(QString description) { m_description = std::move(description); } 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; m_timer_id = this->startTimer(TOOLTIP_DELAY); } - void leaveEvent(QEvent* event) override { KillAndHide(); } - void hideEvent(QHideEvent* event) override { KillAndHide(); } + void leaveEvent(QEvent*) 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()) + KillAndHide(); + } + void hideEvent(QHideEvent*) override { KillAndHide(); } - void timerEvent(QTimerEvent* event) override + void timerEvent(QTimerEvent*) override { this->killTimer(*m_timer_id); m_timer_id.reset();