1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include <QtWidgets/private/qtwidgetsglobal_p.h>
5
6#include <qapplication.h>
7#include <qevent.h>
8#include <qpointer.h>
9#include <qstyle.h>
10#include <qstyleoption.h>
11#include <qstylepainter.h>
12#if QT_CONFIG(effects)
13#include <private/qeffects_p.h>
14#endif
15#include <qtextdocument.h>
16#include <qdebug.h>
17#include <qpa/qplatformscreen.h>
18#include <qpa/qplatformcursor.h>
19#if QT_CONFIG(style_stylesheet)
20#include <private/qstylesheetstyle_p.h>
21#endif
22
23#include <qlabel.h>
24#include <QtWidgets/private/qlabel_p.h>
25#include <QtGui/private/qhighdpiscaling_p.h>
26#include <qtooltip.h>
27
28#include <QtCore/qbasictimer.h>
29
30QT_BEGIN_NAMESPACE
31
32using namespace Qt::StringLiterals;
33
34/*!
35 \class QToolTip
36
37 \brief The QToolTip class provides tool tips (balloon help) for any
38 widget.
39
40 \ingroup helpsystem
41 \inmodule QtWidgets
42
43 The tip is a short piece of text reminding the user of the
44 widget's function. It is drawn immediately below the given
45 position in a distinctive black-on-yellow color combination. The
46 tip can be any \l{QTextEdit}{rich text} formatted string.
47
48 Rich text displayed in a tool tip is implicitly word-wrapped unless
49 specified differently with \c{<p style='white-space:pre'>}.
50
51 UI elements that are created via \l{QAction} use the tooltip property
52 of the QAction, so for most interactive UI elements, setting that
53 property is the easiest way to provide tool tips.
54
55 \snippet tooltips/main.cpp action_tooltip
56
57 For any other widgets, the simplest and most common way to set
58 a widget's tool tip is by calling its QWidget::setToolTip() function.
59
60 \snippet tooltips/main.cpp static_tooltip
61
62 It is also possible to show different tool tips for different
63 regions of a widget, by using a QHelpEvent of type
64 QEvent::ToolTip. Intercept the help event in your widget's \l
65 {QWidget::}{event()} function and call QToolTip::showText() with
66 the text you want to display.
67
68 \snippet tooltips/main.cpp dynamic_tooltip
69
70 If you are calling QToolTip::hideText(), or QToolTip::showText()
71 with an empty string, as a result of a \l{QEvent::}{ToolTip}-event you
72 should also call \l{QEvent::}{ignore()} on the event, to signal
73 that you don't want to start any tooltip specific modes.
74
75 Note that, if you want to show tooltips in an item view, the
76 model/view architecture provides functionality to set an item's
77 tool tip; e.g., the QTableWidgetItem::setToolTip() function.
78 However, if you want to provide custom tool tips in an item view,
79 you must intercept the help event in the
80 QAbstractItemView::viewportEvent() function and handle it yourself.
81
82 The default tool tip color and font can be customized with
83 setPalette() and setFont(). When a tooltip is currently on
84 display, isVisible() returns \c true and text() the currently visible
85 text.
86
87 \note Tool tips use the inactive color group of QPalette, because tool
88 tips are not active windows.
89
90 \sa QWidget::toolTip, QAction::toolTip
91*/
92
93class QTipLabel : public QLabel
94{
95 Q_OBJECT
96public:
97 QTipLabel(const QString &text, const QPoint &pos, QWidget *w, int msecDisplayTime);
98 ~QTipLabel();
99 static QTipLabel *instance;
100
101 void adjustTooltipScreen(const QPoint &pos);
102 void updateSize(const QPoint &pos);
103
104 bool eventFilter(QObject *, QEvent *) override;
105
106 QBasicTimer hideTimer, expireTimer;
107
108 bool fadingOut;
109
110 void reuseTip(const QString &text, int msecDisplayTime, const QPoint &pos);
111 void hideTip();
112 void hideTipImmediately();
113 void setTipRect(QWidget *w, const QRect &r);
114 void restartExpireTimer(int msecDisplayTime);
115 bool tipChanged(const QPoint &pos, const QString &text, QObject *o);
116 void placeTip(const QPoint &pos, QWidget *w);
117
118 static QScreen *getTipScreen(const QPoint &pos, QWidget *w);
119protected:
120 void timerEvent(QTimerEvent *e) override;
121 void paintEvent(QPaintEvent *e) override;
122 void mouseMoveEvent(QMouseEvent *e) override;
123 void resizeEvent(QResizeEvent *e) override;
124
125#if QT_CONFIG(style_stylesheet)
126public slots:
127 /** \internal
128 Cleanup the _q_stylesheet_parent property.
129 */
130 void styleSheetParentDestroyed() {
131 setProperty(name: "_q_stylesheet_parent", value: QVariant());
132 styleSheetParent = nullptr;
133 }
134
135private:
136 QWidget *styleSheetParent;
137#endif
138
139private:
140 QWidget *widget;
141 QRect rect;
142};
143
144QTipLabel *QTipLabel::instance = nullptr;
145
146QTipLabel::QTipLabel(const QString &text, const QPoint &pos, QWidget *w, int msecDisplayTime)
147 : QLabel(w, Qt::ToolTip | Qt::BypassGraphicsProxyWidget)
148#if QT_CONFIG(style_stylesheet)
149 , styleSheetParent(nullptr)
150#endif
151 , widget(nullptr)
152{
153 delete instance;
154 instance = this;
155 setForegroundRole(QPalette::ToolTipText);
156 setBackgroundRole(QPalette::ToolTipBase);
157 setPalette(QToolTip::palette());
158 ensurePolished();
159 setMargin(1 + style()->pixelMetric(metric: QStyle::PM_ToolTipLabelFrameWidth, option: nullptr, widget: this));
160 setFrameStyle(QFrame::NoFrame);
161 setAlignment(Qt::AlignLeft);
162 setIndent(1);
163 qApp->installEventFilter(filterObj: this);
164 setWindowOpacity(style()->styleHint(stylehint: QStyle::SH_ToolTipLabel_Opacity, opt: nullptr, widget: this) / 255.0);
165 setMouseTracking(true);
166 fadingOut = false;
167 reuseTip(text, msecDisplayTime, pos);
168}
169
170void QTipLabel::restartExpireTimer(int msecDisplayTime)
171{
172 Q_D(const QLabel);
173 const qsizetype textLength = d->needTextControl() ? d->control->toPlainText().size() : text().size();
174 qsizetype time = 10000 + 40 * qMax(a: 0, b: textLength - 100);
175 if (msecDisplayTime > 0)
176 time = msecDisplayTime;
177 expireTimer.start(msec: time, obj: this);
178 hideTimer.stop();
179}
180
181void QTipLabel::reuseTip(const QString &text, int msecDisplayTime, const QPoint &pos)
182{
183#if QT_CONFIG(style_stylesheet)
184 if (styleSheetParent){
185 disconnect(sender: styleSheetParent, signal: &QWidget::destroyed,
186 receiver: this, slot: &QTipLabel::styleSheetParentDestroyed);
187 styleSheetParent = nullptr;
188 }
189#endif
190
191 setText(text);
192 updateSize(pos);
193 restartExpireTimer(msecDisplayTime);
194}
195
196void QTipLabel::updateSize(const QPoint &pos)
197{
198 d_func()->setScreenForPoint(pos);
199 // Ensure that we get correct sizeHints by placing this window on the right screen.
200 QFontMetrics fm(font());
201 QSize extra(1, 0);
202 // Make it look good with the default ToolTip font on Mac, which has a small descent.
203 if (fm.descent() == 2 && fm.ascent() >= 11)
204 ++extra.rheight();
205 setWordWrap(Qt::mightBeRichText(text()));
206 QSize sh = sizeHint();
207 const QScreen *screen = getTipScreen(pos, w: this);
208 if (!wordWrap() && sh.width() > screen->geometry().width()) {
209 setWordWrap(true);
210 sh = sizeHint();
211 }
212 resize(sh + extra);
213}
214
215void QTipLabel::paintEvent(QPaintEvent *ev)
216{
217 QStylePainter p(this);
218 QStyleOptionFrame opt;
219 opt.initFrom(w: this);
220 p.drawPrimitive(pe: QStyle::PE_PanelTipLabel, opt);
221 p.end();
222
223 QLabel::paintEvent(ev);
224}
225
226void QTipLabel::resizeEvent(QResizeEvent *e)
227{
228 QStyleHintReturnMask frameMask;
229 QStyleOption option;
230 option.initFrom(w: this);
231 if (style()->styleHint(stylehint: QStyle::SH_ToolTip_Mask, opt: &option, widget: this, returnData: &frameMask))
232 setMask(frameMask.region);
233
234 QLabel::resizeEvent(event: e);
235}
236
237void QTipLabel::mouseMoveEvent(QMouseEvent *e)
238{
239 if (!rect.isNull()) {
240 QPoint pos = e->globalPosition().toPoint();
241 if (widget)
242 pos = widget->mapFromGlobal(pos);
243 if (!rect.contains(p: pos))
244 hideTip();
245 }
246 QLabel::mouseMoveEvent(ev: e);
247}
248
249QTipLabel::~QTipLabel()
250{
251 instance = nullptr;
252}
253
254void QTipLabel::hideTip()
255{
256 if (!hideTimer.isActive())
257 hideTimer.start(msec: 300, obj: this);
258}
259
260void QTipLabel::hideTipImmediately()
261{
262 close(); // to trigger QEvent::Close which stops the animation
263 deleteLater();
264}
265
266void QTipLabel::setTipRect(QWidget *w, const QRect &r)
267{
268 if (Q_UNLIKELY(!r.isNull() && !w)) {
269 qWarning(msg: "QToolTip::setTipRect: Cannot pass null widget if rect is set");
270 return;
271 }
272 widget = w;
273 rect = r;
274}
275
276void QTipLabel::timerEvent(QTimerEvent *e)
277{
278 if (e->timerId() == hideTimer.timerId()
279 || e->timerId() == expireTimer.timerId()){
280 hideTimer.stop();
281 expireTimer.stop();
282 hideTipImmediately();
283 }
284}
285
286bool QTipLabel::eventFilter(QObject *o, QEvent *e)
287{
288 switch (e->type()) {
289#ifdef Q_OS_MACOS
290 case QEvent::KeyPress:
291 case QEvent::KeyRelease: {
292 const int key = static_cast<QKeyEvent *>(e)->key();
293 // Anything except key modifiers or caps-lock, etc.
294 if (key < Qt::Key_Shift || key > Qt::Key_ScrollLock)
295 hideTipImmediately();
296 break;
297 }
298#endif
299 case QEvent::Leave:
300 hideTip();
301 break;
302
303
304#if defined (Q_OS_QNX) || defined (Q_OS_WASM) // On QNX the window activate and focus events are delayed and will appear
305 // after the window is shown.
306 case QEvent::WindowActivate:
307 case QEvent::FocusIn:
308 return false;
309 case QEvent::WindowDeactivate:
310 if (o != this)
311 return false;
312 hideTipImmediately();
313 break;
314 case QEvent::FocusOut:
315 if (reinterpret_cast<QWindow*>(o) != windowHandle())
316 return false;
317 hideTipImmediately();
318 break;
319#else
320 case QEvent::WindowActivate:
321 case QEvent::WindowDeactivate:
322 case QEvent::FocusIn:
323 case QEvent::FocusOut:
324#endif
325 case QEvent::MouseButtonPress:
326 case QEvent::MouseButtonRelease:
327 case QEvent::MouseButtonDblClick:
328 case QEvent::Wheel:
329 hideTipImmediately();
330 break;
331
332 case QEvent::MouseMove:
333 if (o == widget && !rect.isNull() && !rect.contains(p: static_cast<QMouseEvent*>(e)->position().toPoint()))
334 hideTip();
335 break;
336 default:
337 break;
338 }
339 return false;
340}
341
342QScreen *QTipLabel::getTipScreen(const QPoint &pos, QWidget *w)
343{
344 QScreen *guess = w ? w->screen() : QGuiApplication::primaryScreen();
345 QScreen *exact = guess->virtualSiblingAt(point: pos);
346 return exact ? exact : guess;
347}
348
349void QTipLabel::placeTip(const QPoint &pos, QWidget *w)
350{
351#if QT_CONFIG(style_stylesheet)
352 if (testAttribute(attribute: Qt::WA_StyleSheet) || (w && qt_styleSheet(style: w->style()))) {
353 //the stylesheet need to know the real parent
354 QTipLabel::instance->setProperty(name: "_q_stylesheet_parent", value: QVariant::fromValue(value: w));
355 //we force the style to be the QStyleSheetStyle, and force to clear the cache as well.
356 QTipLabel::instance->setStyleSheet("/* */"_L1);
357
358 // Set up for cleaning up this later...
359 QTipLabel::instance->styleSheetParent = w;
360 if (w) {
361 connect(sender: w, signal: &QWidget::destroyed,
362 context: QTipLabel::instance, slot: &QTipLabel::styleSheetParentDestroyed);
363 }
364 // QTBUG-64550: A font inherited by the style sheet might change the size,
365 // particular on Windows, where the tip is not parented on a window.
366 // The updatesSize() also makes sure that the content size be updated with
367 // correct content margin.
368 QTipLabel::instance->updateSize(pos);
369 }
370#endif //QT_CONFIG(style_stylesheet)
371
372 QPoint p = pos;
373 const QScreen *screen = getTipScreen(pos, w);
374 // a QScreen's handle *should* never be null, so this is a bit paranoid
375 if (const QPlatformScreen *platformScreen = screen ? screen->handle() : nullptr) {
376 QPlatformCursor *cursor = platformScreen->cursor();
377 // default implementation of QPlatformCursor::size() returns QSize(16, 16)
378 const QSize nativeSize = cursor ? cursor->size() : QSize(16, 16);
379 const QSize cursorSize = QHighDpi::fromNativePixels(value: nativeSize,
380 context: platformScreen);
381 QPoint offset(2, cursorSize.height());
382 // assuming an arrow shape, we can just move to the side for very large cursors
383 if (cursorSize.height() > 2 * this->height())
384 offset = QPoint(cursorSize.width() / 2, 0);
385
386 p += offset;
387
388 QRect screenRect = screen->geometry();
389 if (p.x() + this->width() > screenRect.x() + screenRect.width())
390 p.rx() -= 4 + this->width();
391 if (p.y() + this->height() > screenRect.y() + screenRect.height())
392 p.ry() -= 24 + this->height();
393 if (p.y() < screenRect.y())
394 p.setY(screenRect.y());
395 if (p.x() + this->width() > screenRect.x() + screenRect.width())
396 p.setX(screenRect.x() + screenRect.width() - this->width());
397 if (p.x() < screenRect.x())
398 p.setX(screenRect.x());
399 if (p.y() + this->height() > screenRect.y() + screenRect.height())
400 p.setY(screenRect.y() + screenRect.height() - this->height());
401 }
402 this->move(p);
403}
404
405bool QTipLabel::tipChanged(const QPoint &pos, const QString &text, QObject *o)
406{
407 if (QTipLabel::instance->text() != text)
408 return true;
409
410 if (o != widget)
411 return true;
412
413 if (!rect.isNull())
414 return !rect.contains(p: pos);
415 else
416 return false;
417}
418
419/*!
420 Shows \a text as a tool tip, with the global position \a pos as
421 the point of interest. The tool tip will be shown with a platform
422 specific offset from this point of interest.
423
424 If you specify a non-empty rect the tip will be hidden as soon
425 as you move your cursor out of this area.
426
427 The \a rect is in the coordinates of the widget you specify with
428 \a w. If the \a rect is not empty you must specify a widget.
429 Otherwise this argument can be \nullptr but it is used to
430 determine the appropriate screen on multi-head systems.
431
432 The \a msecDisplayTime parameter specifies for how long the tool tip
433 will be displayed, in milliseconds. With the default value of -1, the
434 time is based on the length of the text.
435
436 If \a text is empty the tool tip is hidden. If the text is the
437 same as the currently shown tooltip, the tip will \e not move.
438 You can force moving by first hiding the tip with an empty text,
439 and then showing the new tip at the new position.
440*/
441
442void QToolTip::showText(const QPoint &pos, const QString &text, QWidget *w, const QRect &rect, int msecDisplayTime)
443{
444 if (QTipLabel::instance && QTipLabel::instance->isVisible()) { // a tip does already exist
445 if (text.isEmpty()){ // empty text means hide current tip
446 QTipLabel::instance->hideTip();
447 return;
448 } else if (!QTipLabel::instance->fadingOut) {
449 // If the tip has changed, reuse the one
450 // that is showing (removes flickering)
451 QPoint localPos = pos;
452 if (w)
453 localPos = w->mapFromGlobal(pos);
454 if (QTipLabel::instance->tipChanged(pos: localPos, text, o: w)){
455 QTipLabel::instance->reuseTip(text, msecDisplayTime, pos);
456 QTipLabel::instance->setTipRect(w, r: rect);
457 QTipLabel::instance->placeTip(pos, w);
458 }
459 return;
460 }
461 }
462
463 if (!text.isEmpty()) { // no tip can be reused, create new tip:
464 QWidget *tipLabelParent = [w]() -> QWidget* {
465#ifdef Q_OS_WIN32
466 // On windows, we can't use the widget as parent otherwise the window will be
467 // raised when the tooltip will be shown
468 Q_UNUSED(w);
469 return nullptr;
470#else
471 return w;
472#endif
473 }();
474 new QTipLabel(text, pos, tipLabelParent, msecDisplayTime); // sets QTipLabel::instance to itself
475 QWidgetPrivate::get(w: QTipLabel::instance)->setScreen(QTipLabel::getTipScreen(pos, w));
476 QTipLabel::instance->setTipRect(w, r: rect);
477 QTipLabel::instance->placeTip(pos, w);
478 QTipLabel::instance->setObjectName("qtooltip_label"_L1);
479
480#if QT_CONFIG(effects)
481 if (QApplication::isEffectEnabled(Qt::UI_FadeTooltip))
482 qFadeEffect(QTipLabel::instance);
483 else if (QApplication::isEffectEnabled(Qt::UI_AnimateTooltip))
484 qScrollEffect(QTipLabel::instance);
485 else
486 QTipLabel::instance->showNormal();
487#else
488 QTipLabel::instance->showNormal();
489#endif
490 }
491}
492
493/*!
494 \fn void QToolTip::hideText()
495 \since 4.2
496
497 Hides the tool tip. This is the same as calling showText() with an
498 empty string.
499
500 \sa showText()
501*/
502
503
504/*!
505 \since 4.4
506
507 Returns \c true if a tooltip is currently shown.
508
509 \sa showText()
510 */
511bool QToolTip::isVisible()
512{
513 return (QTipLabel::instance != nullptr && QTipLabel::instance->isVisible());
514}
515
516/*!
517 \since 4.4
518
519 Returns the tooltip text, if a tooltip is visible, or an
520 empty string if a tooltip is not visible.
521 */
522QString QToolTip::text()
523{
524 if (QTipLabel::instance)
525 return QTipLabel::instance->text();
526 return QString();
527}
528
529
530Q_GLOBAL_STATIC(QPalette, tooltip_palette)
531
532/*!
533 Returns the palette used to render tooltips.
534
535 \note Tool tips use the inactive color group of QPalette, because tool
536 tips are not active windows.
537*/
538QPalette QToolTip::palette()
539{
540 return *tooltip_palette();
541}
542
543/*!
544 \since 4.2
545
546 Returns the font used to render tooltips.
547*/
548QFont QToolTip::font()
549{
550 return QApplication::font(className: "QTipLabel");
551}
552
553/*!
554 \since 4.2
555
556 Sets the \a palette used to render tooltips.
557
558 \note Tool tips use the inactive color group of QPalette, because tool
559 tips are not active windows.
560*/
561void QToolTip::setPalette(const QPalette &palette)
562{
563 *tooltip_palette() = palette;
564 if (QTipLabel::instance)
565 QTipLabel::instance->setPalette(palette);
566}
567
568/*!
569 \since 4.2
570
571 Sets the \a font used to render tooltips.
572*/
573void QToolTip::setFont(const QFont &font)
574{
575 QApplication::setFont(font, className: "QTipLabel");
576}
577
578QT_END_NAMESPACE
579
580#include "qtooltip.moc"
581

source code of qtbase/src/widgets/kernel/qtooltip.cpp