diff --git a/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.cpp b/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.cpp index 52033a9445..ffed038c19 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,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, diff --git a/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h b/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h index c5df392df9..ca62c1cfea 100644 --- a/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h +++ b/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h @@ -7,6 +7,9 @@ #include #include +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; }; diff --git a/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h b/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h index 727ae11462..94cd88b87e 100644 --- a/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h +++ b/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h @@ -9,6 +9,11 @@ #include "DolphinQt/Config/ToolTipControls/BalloonTip.h" +class QEnterEvent; +class QEvent; +class QHideEvent; +class QTimerEvent; + constexpr int TOOLTIP_DELAY = 300; template @@ -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) {