diff --git a/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.cpp b/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.cpp index 435b60e462..672a4381f8 100644 --- a/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.cpp +++ b/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.cpp @@ -6,24 +6,25 @@ #include #include -#include -#include -#include +#include +#include +#include #include #include #include #include -#include -#include - +#include +#include +#include #include +#include +#include +#include #if defined(__APPLE__) #include #endif -#include "Core/Config/MainSettings.h" - #include "DolphinQt/Settings.h" namespace @@ -31,8 +32,9 @@ namespace std::unique_ptr s_the_balloon_tip = nullptr; } // namespace -void BalloonTip::ShowBalloon(const QIcon& icon, const QString& title, const QString& message, - const QPoint& pos, QWidget* parent, ShowArrow show_arrow) +void BalloonTip::ShowBalloon(const QString& title, const QString& message, + const QPoint& target_arrow_tip_position, QWidget* const parent, + const ShowArrow show_arrow, const int border_width) { HideBalloon(); if (message.isEmpty() && title.isEmpty()) @@ -42,10 +44,10 @@ void BalloonTip::ShowBalloon(const QIcon& icon, const QString& title, const QStr QString the_message = message; the_message.replace(QStringLiteral(""), QStringLiteral("")); the_message.replace(QStringLiteral(""), QStringLiteral("")); - QToolTip::showText(pos, the_message, parent); + QToolTip::showText(target_arrow_tip_position, the_message, parent); #else - s_the_balloon_tip = std::make_unique(PrivateTag{}, icon, title, message, parent); - s_the_balloon_tip->UpdateBoundsAndRedraw(pos, show_arrow); + s_the_balloon_tip = std::make_unique(PrivateTag{}, title, message, parent); + s_the_balloon_tip->UpdateBoundsAndRedraw(target_arrow_tip_position, show_arrow, border_width); #endif } @@ -54,20 +56,16 @@ void BalloonTip::HideBalloon() #if defined(__APPLE__) QToolTip::hideText(); #else - if (!s_the_balloon_tip) + if (s_the_balloon_tip == nullptr) return; s_the_balloon_tip->hide(); s_the_balloon_tip.reset(); #endif } -BalloonTip::BalloonTip(PrivateTag, const QIcon& icon, QString title, QString message, - QWidget* parent) +BalloonTip::BalloonTip(PrivateTag, const QString& title, QString message, QWidget* const parent) : QWidget(nullptr, Qt::ToolTip) { - setAttribute(Qt::WA_DeleteOnClose); - setAutoFillBackground(true); - QColor window_color; QColor text_color; QColor dolphin_emphasis; @@ -78,43 +76,41 @@ BalloonTip::BalloonTip(PrivateTag, const QIcon& icon, QString title, QString mes .arg(text_color.rgba(), 0, 16); setStyleSheet(style_sheet); - // Replace text in our our message - // if specific "tags" are used + // Replace text in our our message if specific "tags" are used message.replace(QStringLiteral(""), QStringLiteral("").arg(dolphin_emphasis.rgba(), 0, 16)); message.replace(QStringLiteral(""), QStringLiteral("")); - auto* title_label = new QLabel; - title_label->installEventFilter(this); - title_label->setText(title); - QFont f = title_label->font(); - f.setBold(true); - title_label->setFont(f); - title_label->setTextFormat(Qt::RichText); - title_label->setSizePolicy(QSizePolicy::Policy::MinimumExpanding, - QSizePolicy::Policy::MinimumExpanding); + auto* const balloontip_layout = new QVBoxLayout; + balloontip_layout->setSizeConstraint(QLayout::SetFixedSize); + setLayout(balloontip_layout); - auto* message_label = new QLabel; - message_label->installEventFilter(this); - message_label->setText(message); - message_label->setTextFormat(Qt::RichText); - message_label->setAlignment(Qt::AlignTop | Qt::AlignLeft); + const auto create_label = [=](const QString& text) { + QLabel* const label = new QLabel; + balloontip_layout->addWidget(label); - const int limit = message_label->screen()->availableGeometry().width() / 3; - message_label->setMaximumWidth(limit); - message_label->setSizePolicy(QSizePolicy::Policy::MinimumExpanding, - QSizePolicy::Policy::MinimumExpanding); - if (message_label->sizeHint().width() > limit) + label->setText(text); + label->setTextFormat(Qt::RichText); + + const int max_width = label->screen()->availableGeometry().width() / 3; + label->setMaximumWidth(max_width); + if (label->sizeHint().width() > max_width) + label->setWordWrap(true); + + return label; + }; + + if (!title.isEmpty()) { - message_label->setWordWrap(true); + QLabel* const title_label = create_label(title); + + QFont title_font = title_label->font(); + title_font.setBold(true); + title_label->setFont(title_font); } - auto* layout = new QGridLayout; - layout->addWidget(title_label, 0, 0, 1, 2); - - layout->addWidget(message_label, 1, 0, 1, 3); - layout->setSizeConstraint(QLayout::SetMinimumSize); - setLayout(layout); + if (!message.isEmpty()) + create_label(message); } void BalloonTip::paintEvent(QPaintEvent*) @@ -123,116 +119,215 @@ void BalloonTip::paintEvent(QPaintEvent*) painter.drawPixmap(rect(), m_pixmap); } -void BalloonTip::UpdateBoundsAndRedraw(const QPoint& pos, ShowArrow show_arrow) +void BalloonTip::UpdateBoundsAndRedraw(const QPoint& target_arrow_tip_position, + const ShowArrow show_arrow, const int border_full_width) { - m_show_arrow = show_arrow == ShowArrow::Yes; + const float border_half_width = border_full_width / 2.0; - QScreen* screen = QGuiApplication::screenAt(pos); - if (!screen) - screen = QGuiApplication::primaryScreen(); + // This should be odd so that the arrow tip is a single pixel wide. + const int arrow_full_width = 35; + const float arrow_half_width = arrow_full_width / 2.0; + // The y distance between the inner edge of the rectangle border and the inner tip of the arrow + // border, and also the distance between the outer edge of the rectangle border and the outer tip + // of the arrow border + const int arrow_height = (1 + arrow_full_width) / 2; + + // Distance between the label layout and the inner rectangle border edge + const int balloon_interior_padding = 12; + // Prevent the corners of the label layout from portruding into the rounded rectangle corners at + // larger border sizes. + const int rounded_corner_margin = border_half_width / 4; + const int horizontal_margin = + border_full_width + rounded_corner_margin + balloon_interior_padding; + const int vertical_margin = horizontal_margin + arrow_height; + + // Create enough space around the layout containing the title and message to draw the balloon and + // both arrow tips (at most one of which will be visible) + layout()->setContentsMargins(horizontal_margin, vertical_margin, horizontal_margin, + vertical_margin); + + QSize size_hint = sizeHint(); + + // These positions represent the middle of each edge of the BalloonTip's rounded rectangle + const float rect_width = size_hint.width() - border_full_width; + const float rect_height = size_hint.height() - border_full_width - 2 * arrow_height; + const float rect_top = border_half_width + arrow_height; + const float rect_bottom = rect_top + rect_height; + const float rect_left = border_half_width; + // rect_right isn't used for anything + + // Qt defines the radius of a rounded rectangle as "the radius of the ellipses defining the + // corner". Unlike the rectangle's edges this corresponds to the outside of the rounded curve + // instead of its middle, so we add the full width to the inner radius instead of the half width + const float corner_base_inner_radius = 7.0; + const float corner_outer_radius = corner_base_inner_radius + border_full_width; + + // This value is arbitrary but works well. + const int base_arrow_x_offset = 34; + // Adjust the offset inward to compensate for the border and rounded corner widths. This ensures + // the arrow is on the flat part of the top/bottom border. + const int adjusted_arrow_x_offset = + base_arrow_x_offset + border_full_width + static_cast(border_half_width); + // If the border is wide enough (or the BalloonTip small enough) the offset might end up past the + // midpoint; if that happens just use the midpoint instead + const int centered_arrow_x_offset = (size_hint.width() - arrow_full_width) / 2; + // If the arrow is on the left this is the distance between the left edge of the BalloonTip and + // the left edge of the arrow interior; otherwise the distance between the right edges. + const int arrow_nearest_edge_x_offset = + std::min(adjusted_arrow_x_offset, centered_arrow_x_offset); + const int arrow_tip_x_offset = arrow_nearest_edge_x_offset + arrow_half_width; + + // The BalloonTip should be contained entirely within the screen that contains the target + // position. + QScreen* screen = QGuiApplication::screenAt(target_arrow_tip_position); + if (screen == nullptr) + { + // If the target position isn't on any screen (which can happen if the window is partly off the + // screen and the user hovers over the label) then use the screen containing the cursor instead. + screen = QGuiApplication::screenAt(QCursor::pos()); + } const QRect screen_rect = screen->geometry(); - QSize sh = sizeHint(); - // The look should resemble the default tooltip style set in Settings::ApplyStyle() - const int border = 1; - const int arrow_height = 18; - const int arrow_width = 18; - const int arrow_offset = 52; - const int rect_center = 7; - const bool arrow_at_bottom = (pos.y() - sh.height() - arrow_height > 0); - const bool arrow_at_left = (pos.x() + sh.width() - arrow_width < screen_rect.width()); - const int default_padding = 10; - layout()->setContentsMargins(border + 3 + default_padding, - border + (arrow_at_bottom ? 0 : arrow_height) + 2 + default_padding, - border + 3 + default_padding, - border + (arrow_at_bottom ? arrow_height : 0) + 2 + default_padding); - updateGeometry(); - sh = sizeHint(); + QPainterPath rect_path; + rect_path.addRoundedRect(rect_left, rect_top, rect_width, rect_height, corner_outer_radius, + corner_outer_radius); - int ml, mr, mt, mb; - QSize sz = sizeHint(); - if (arrow_at_bottom) + // The default pen cap style Qt::SquareCap extends drawn lines one half width before the starting + // point and one half width after the ending point such that the starting and ending points are + // surrounded by drawn pixels in both dimensions instead of just for the width. This is a + // reasonable default but we need to draw lines precisely, and this behavior causes problems when + // drawing lines of length and width 1 (which we do for the arrow interior, and also the arrow + // border when border_full_width is 1). For those lines to correctly end up as a pixel we would + // need to offset the start and end points by 0.5 inward. However, doing that would lead them to + // be at the same point, and if the endpoints of a line are the same Qt simply doesn't draw it + // regardless of the cap style. + // + // Using Qt::FlatCap instead fixes the issue. + + m_pixmap = QPixmap(size_hint); + + QPen border_pen(m_border_color, border_full_width); + border_pen.setCapStyle(Qt::FlatCap); + + QPainter balloon_painter(&m_pixmap); + balloon_painter.setPen(border_pen); + balloon_painter.setBrush(palette().color(QPalette::Window)); + balloon_painter.drawPath(rect_path); + + QBitmap mask_bitmap(size_hint); + mask_bitmap.fill(Qt::color0); + + QPen mask_pen(Qt::color1, border_full_width); + mask_pen.setCapStyle(Qt::FlatCap); + + QPainter mask_painter(&mask_bitmap); + mask_painter.setPen(mask_pen); + mask_painter.setBrush(QBrush(Qt::color1)); + mask_painter.drawPath(rect_path); + + const bool arrow_at_bottom = + target_arrow_tip_position.y() - size_hint.height() + arrow_height >= 0; + const bool arrow_at_left = + target_arrow_tip_position.x() + size_hint.width() - arrow_tip_x_offset < screen_rect.width(); + + const float arrow_base_y = + arrow_at_bottom ? rect_bottom - border_half_width : rect_top + border_half_width; + + const float arrow_tip_vertical_offset = arrow_at_bottom ? arrow_height : -arrow_height; + const float arrow_tip_interior_y = arrow_base_y + arrow_tip_vertical_offset; + const float arrow_tip_exterior_y = + arrow_tip_interior_y + (arrow_at_bottom ? border_full_width : -border_full_width); + const float arrow_base_left_edge_x = + arrow_at_left ? arrow_nearest_edge_x_offset : + size_hint.width() - arrow_nearest_edge_x_offset - arrow_full_width; + const float arrow_base_right_edge_x = arrow_base_left_edge_x + arrow_full_width; + const float arrow_tip_x = arrow_base_left_edge_x + arrow_half_width; + + if (show_arrow == ShowArrow::Yes) { - ml = mt = 0; - mr = sz.width() - 1; - mb = sz.height() - arrow_height - 1; - } - else - { - ml = 0; - mt = arrow_height; - mr = sz.width() - 1; - mb = sz.height() - 1; + // Drawing diagonal lines in Qt is filled with edge cases and inexplicable behavior. Getting it + // to do what you want at one border size is simple enough, but doing so flexibly is an exercise + // in futility. Some examples: + // * For some values of x, diagonal lines of width x and x+1 are drawn exactly the same. + // * When drawing a triangle where p1 and p3 have exactly the same y value, they can be drawn at + // different heights. + // * Lines of width 1 sometimes get drawn one pixel past where they should even with FlatCap, + // but only on the left side (regardless of which direction the stroke was). + // + // Instead of dealing with all that, fake it with vertical lines which are much better behaved. + // Draw a bunch of vertical lines with width 1 to form the arrow border and interior. + + QPainterPath arrow_border_path; + QPainterPath arrow_interior_fill_path; + const float y_end_offset = arrow_at_bottom ? border_full_width : -border_full_width; + + // Draw the arrow border and interior lines from the outside inward. Each loop iteration draws + // one pair of border lines and one pair of interior lines. + for (int i = 1; i <= arrow_half_width; i++) + { + const float x_offset_from_arrow_base_edge = i - 0.5; + const float border_y_start = arrow_base_y + (arrow_at_bottom ? i : -i); + const float border_y_end = border_y_start + y_end_offset; + const float interior_y_start = arrow_base_y; + const float interior_y_end = border_y_start; + const float left_line_x = arrow_base_left_edge_x + x_offset_from_arrow_base_edge; + const float right_line_x = arrow_base_right_edge_x - x_offset_from_arrow_base_edge; + + arrow_border_path.moveTo(left_line_x, border_y_start); + arrow_border_path.lineTo(left_line_x, border_y_end); + + arrow_border_path.moveTo(right_line_x, border_y_start); + arrow_border_path.lineTo(right_line_x, border_y_end); + + arrow_interior_fill_path.moveTo(left_line_x, interior_y_start); + arrow_interior_fill_path.lineTo(left_line_x, interior_y_end); + + arrow_interior_fill_path.moveTo(right_line_x, interior_y_start); + arrow_interior_fill_path.lineTo(right_line_x, interior_y_end); + } + // The middle border line + arrow_border_path.moveTo(arrow_tip_x, arrow_tip_interior_y); + arrow_border_path.lineTo(arrow_tip_x, arrow_tip_interior_y + y_end_offset); + + // The middle interior line + arrow_interior_fill_path.moveTo(arrow_tip_x, arrow_base_y); + arrow_interior_fill_path.lineTo(arrow_tip_x, arrow_tip_interior_y); + + border_pen.setWidth(1); + + balloon_painter.setPen(border_pen); + balloon_painter.drawPath(arrow_border_path); + + QPen arrow_interior_fill_pen(palette().color(QPalette::Window), 1); + arrow_interior_fill_pen.setCapStyle(Qt::FlatCap); + balloon_painter.setPen(arrow_interior_fill_pen); + balloon_painter.drawPath(arrow_interior_fill_path); + + mask_pen.setWidth(1); + mask_painter.setPen(mask_pen); + + mask_painter.drawPath(arrow_border_path); + mask_painter.drawPath(arrow_interior_fill_path); } - QPainterPath path; - path.moveTo(ml + rect_center, mt); - if (!arrow_at_bottom && arrow_at_left) - { - if (m_show_arrow) - { - path.lineTo(ml + arrow_offset - arrow_width, mt); - path.lineTo(ml + arrow_offset, mt - arrow_height); - path.lineTo(ml + arrow_offset + arrow_width, mt); - } - move(qMax(pos.x() - arrow_offset, screen_rect.left() + 2), pos.y()); - } - else if (!arrow_at_bottom && !arrow_at_left) - { - if (m_show_arrow) - { - path.lineTo(mr - arrow_offset - arrow_width, mt); - path.lineTo(mr - arrow_offset, mt - arrow_height); - path.lineTo(mr - arrow_offset + arrow_width, mt); - } - move(qMin(pos.x() - sh.width() + arrow_offset, screen_rect.right() - sh.width() - 2), pos.y()); - } - path.lineTo(mr - rect_center, mt); - path.arcTo(QRect(mr - rect_center * 2, mt, rect_center * 2, rect_center * 2), 90, -90); - path.lineTo(mr, mb - rect_center); - path.arcTo(QRect(mr - rect_center * 2, mb - rect_center * 2, rect_center * 2, rect_center * 2), 0, - -90); - if (arrow_at_bottom && !arrow_at_left) - { - if (m_show_arrow) - { - path.lineTo(mr - arrow_offset + arrow_width, mb); - path.lineTo(mr - arrow_offset, mb + arrow_height); - path.lineTo(mr - arrow_offset - arrow_width, mb); - } - move(qMin(pos.x() - sh.width() + arrow_offset, screen_rect.right() - sh.width() - 2), - pos.y() - sh.height()); - } - else if (arrow_at_bottom && arrow_at_left) - { - if (m_show_arrow) - { - path.lineTo(arrow_offset + arrow_width, mb); - path.lineTo(arrow_offset, mb + arrow_height); - path.lineTo(arrow_offset - arrow_width, mb); - } - move(qMax(pos.x() - arrow_offset, screen_rect.x() + 2), pos.y() - sh.height()); - } - path.lineTo(ml + rect_center, mb); - path.arcTo(QRect(ml, mb - rect_center * 2, rect_center * 2, rect_center * 2), -90, -90); - path.lineTo(ml, mt + rect_center); - path.arcTo(QRect(ml, mt, rect_center * 2, rect_center * 2), 180, -90); + setMask(mask_bitmap); - // Set the mask - QBitmap bitmap(sizeHint()); - bitmap.fill(Qt::color0); - QPainter painter1(&bitmap); - painter1.setPen(QPen(Qt::color1, border)); - painter1.setBrush(QBrush(Qt::color1)); - painter1.drawPath(path); - setMask(bitmap); + // Place the arrow tip at the target position whether the arrow tip is drawn or not + const int target_balloontip_global_x = + target_arrow_tip_position.x() - static_cast(arrow_tip_x); + const int rightmost_valid_balloontip_global_x = screen_rect.width() - size_hint.width(); + // If the balloon would extend off the screen, push it left or right until it's not + const int actual_balloontip_global_x = + std::max(0, std::min(rightmost_valid_balloontip_global_x, target_balloontip_global_x)); + // The tip pixel should be in the middle of the control, and arrow_tip_exterior_y is at the bottom + // of that pixel. When arrow_at_bottom is true the arrow is above arrow_tip_exterior_y and so the + // tip pixel is in the right place, but when it's false the arrow is below arrow_tip_exterior_y + // so the tip pixel would be the one below that. Make this adjustment to fix that. + const int tip_pixel_adjustment = arrow_at_bottom ? 0 : 1; + const int actual_balloontip_global_y = + target_arrow_tip_position.y() - arrow_tip_exterior_y - tip_pixel_adjustment; - // Draw the border - m_pixmap = QPixmap(sz); - QPainter painter2(&m_pixmap); - painter2.setPen(QPen(m_border_color)); - painter2.setBrush(palette().color(QPalette::Window)); - painter2.drawPath(path); + move(actual_balloontip_global_x, actual_balloontip_global_y); show(); } diff --git a/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h b/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h index a0a918325b..c5df392df9 100644 --- a/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h +++ b/Source/Core/DolphinQt/Config/ToolTipControls/BalloonTip.h @@ -3,10 +3,14 @@ #pragma once -#include +#include #include #include +class QPaintEvent; +class QPoint; +class QString; + class BalloonTip : public QWidget { Q_OBJECT @@ -21,15 +25,16 @@ public: Yes, No }; - static void ShowBalloon(const QIcon& icon, const QString& title, const QString& msg, - const QPoint& pos, QWidget* parent, - ShowArrow show_arrow = ShowArrow::Yes); + static void ShowBalloon(const QString& title, const QString& message, + const QPoint& target_arrow_tip_position, QWidget* parent, + ShowArrow show_arrow = ShowArrow::Yes, int border_width = 1); static void HideBalloon(); - BalloonTip(PrivateTag, const QIcon& icon, QString title, QString msg, QWidget* parent); + BalloonTip(PrivateTag, const QString& title, QString message, QWidget* parent); private: - void UpdateBoundsAndRedraw(const QPoint&, ShowArrow); + void UpdateBoundsAndRedraw(const QPoint& target_arrow_tip_position, ShowArrow show_arrow, + int border_width); protected: void paintEvent(QPaintEvent*) override; @@ -37,5 +42,4 @@ protected: private: QColor m_border_color; QPixmap m_pixmap; - bool m_show_arrow = true; }; diff --git a/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h b/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h index 1f7c574345..727ae11462 100644 --- a/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h +++ b/Source/Core/DolphinQt/Config/ToolTipControls/ToolTipWidget.h @@ -37,7 +37,7 @@ private: this->killTimer(*m_timer_id); m_timer_id.reset(); - BalloonTip::ShowBalloon(QIcon(), m_title, m_description, + BalloonTip::ShowBalloon(m_title, m_description, this->parentWidget()->mapToGlobal(GetToolTipPosition()), this); }