Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qtooltip.cpp
Go to the documentation of this file.
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// Qt-Security score:significant reason:default
4
5#include <QtWidgets/private/qtwidgetsglobal_p.h>
6
7#include <qapplication.h>
8#include <qevent.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 <qpa/qplatformscreen.h>
16#include <qpa/qplatformcursor.h>
17#if QT_CONFIG(style_stylesheet)
18#include <private/qstylesheetstyle_p.h>
19#endif
20#include <qpa/qplatformwindow.h>
21#include <qpa/qplatformwindow_p.h>
22
23#include <QtWidgets/private/qlabel_p.h>
24#include <QtGui/private/qhighdpiscaling_p.h>
25#include <qtooltip.h>
26#include <QtWidgets/private/qtooltip_p.h>
27
28using namespace std::chrono_literals;
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
93QTipLabel *QTipLabel::instance = nullptr;
94
95QTipLabel::QTipLabel(const QString &text, QPoint pos, QWidget *w,
96 std::chrono::milliseconds displayTime)
97 : QLabel(w, Qt::ToolTip | Qt::BypassGraphicsProxyWidget)
98#if QT_CONFIG(style_stylesheet)
99 , styleSheetParent(nullptr)
100#endif
101 , widget(nullptr)
102 , fadingOut(false)
103{
104 delete instance;
105 instance = this;
106 setForegroundRole(QPalette::ToolTipText);
107 setBackgroundRole(QPalette::ToolTipBase);
108 setPalette(QToolTip::palette());
109 ensurePolished();
110 setMargin(1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, nullptr, this));
111 setFrameStyle(QFrame::NoFrame);
112 setAlignment(Qt::AlignLeft);
113 setIndent(1);
114 qApp->installEventFilter(this);
115 setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, nullptr, this) / 255.0);
116 setMouseTracking(true);
117 reuseTip(text, displayTime, pos);
118}
119
120void QTipLabel::restartExpireTimer(std::chrono::milliseconds displayTime)
121{
122 Q_D(const QLabel);
123 std::chrono::milliseconds time;
124 if (displayTime > 0ms) {
125 time = displayTime;
126 } else {
127 const qsizetype textLength =
128 d->needTextControl() ? d->control->toPlainText().size() : text().size();
129 time = std::chrono::milliseconds(10000 + 40 * qMax(0, textLength - 100));
130 }
131 expireTimer.start(time, this);
132 hideTimer.stop();
133}
134
135void QTipLabel::reuseTip(const QString &text, std::chrono::milliseconds displayTime, QPoint pos)
136{
137#if QT_CONFIG(style_stylesheet)
138 if (styleSheetParent){
139 disconnect(styleSheetParent, &QWidget::destroyed,
140 this, &QTipLabel::styleSheetParentDestroyed);
141 styleSheetParent = nullptr;
142 }
143#endif
144
145 setWordWrap(Qt::mightBeRichText(text));
146 setText(text);
147 updateSize(pos);
148 restartExpireTimer(displayTime);
149}
150
151void QTipLabel::updateSize(const QPoint &pos)
152{
153 d_func()->setScreenForPoint(pos);
154 // Ensure that we get correct sizeHints by placing this window on the right screen.
155 QFontMetrics fm(font());
156 QSize extra(1, 0);
157 // Make it look good with the default ToolTip font on Mac, which has a small descent.
158 if (fm.descent() == 2 && fm.ascent() >= 11)
159 ++extra.rheight();
160 QSize sh = sizeHint();
161 const QScreen *screen = getTipScreen(pos, this);
162 if (!wordWrap() && sh.width() > screen->geometry().width()) {
163 setWordWrap(true);
164 sh = sizeHint();
165 }
166 resize(sh + extra);
167}
168
169void QTipLabel::paintEvent(QPaintEvent *ev)
170{
171 QStylePainter p(this);
172 QStyleOptionFrame opt;
173 opt.initFrom(this);
174 p.drawPrimitive(QStyle::PE_PanelTipLabel, opt);
175 p.end();
176
177 QLabel::paintEvent(ev);
178}
179
180void QTipLabel::resizeEvent(QResizeEvent *e)
181{
182 QStyleHintReturnMask frameMask;
183 QStyleOption option;
184 option.initFrom(this);
185 if (style()->styleHint(QStyle::SH_ToolTip_Mask, &option, this, &frameMask))
186 setMask(frameMask.region);
187
188 QLabel::resizeEvent(e);
189}
190
191void QTipLabel::mouseMoveEvent(QMouseEvent *e)
192{
193 if (!rect.isNull()) {
194 QPointF pos = e->globalPosition();
195 if (widget)
196 pos = widget->mapFromGlobal(pos);
197 if (!rect.contains(pos.toPoint()))
198 hideTip();
199 }
200 QLabel::mouseMoveEvent(e);
201}
202
203QTipLabel::~QTipLabel()
204{
205 instance = nullptr;
206}
207
208void QTipLabel::hideTip()
209{
210 if (!hideTimer.isActive())
211 hideTimer.start(300, this);
212}
213
214void QTipLabel::hideTipImmediately()
215{
216 close(); // to trigger QEvent::Close which stops the animation
217 deleteLater();
218}
219
220void QTipLabel::setTipRect(QWidget *w, const QRect &r)
221{
222 if (Q_UNLIKELY(!r.isNull() && !w)) {
223 qWarning("QToolTip::setTipRect: Cannot pass null widget if rect is set");
224 return;
225 }
226 widget = w;
227 rect = r;
228}
229
230void QTipLabel::timerEvent(QTimerEvent *e)
231{
232 if (e->timerId() == hideTimer.timerId()
233 || e->timerId() == expireTimer.timerId()){
234 hideTimer.stop();
235 expireTimer.stop();
236 hideTipImmediately();
237 }
238}
239
240bool QTipLabel::eventFilter(QObject *o, QEvent *e)
241{
242 switch (e->type()) {
243#ifdef Q_OS_MACOS
244 case QEvent::KeyPress:
245 case QEvent::KeyRelease: {
246 const int key = static_cast<QKeyEvent *>(e)->key();
247 // Anything except key modifiers or caps-lock, etc.
248 if (key < Qt::Key_Shift || key > Qt::Key_ScrollLock)
249 hideTipImmediately();
250 break;
251 }
252#endif
253 case QEvent::Leave:
254 hideTip();
255 break;
256
257
258#if defined (Q_OS_QNX) || defined (Q_OS_WASM) // On QNX the window activate and focus events are delayed and will appear
259 // after the window is shown.
260 case QEvent::WindowActivate:
261 case QEvent::FocusIn:
262 return false;
263 case QEvent::WindowDeactivate:
264 if (o != this)
265 return false;
266 hideTipImmediately();
267 break;
268 case QEvent::FocusOut:
269 if (reinterpret_cast<QWindow*>(o) != windowHandle())
270 return false;
271 hideTipImmediately();
272 break;
273#else
274 case QEvent::WindowActivate:
275 case QEvent::WindowDeactivate:
276 case QEvent::FocusIn:
277 case QEvent::FocusOut:
278#endif
279 case QEvent::MouseButtonPress:
280 case QEvent::MouseButtonRelease:
281 case QEvent::MouseButtonDblClick:
282 case QEvent::Wheel:
283 hideTipImmediately();
284 break;
285
286 case QEvent::MouseMove:
287 if (o == widget && !rect.isNull() && !rect.contains(static_cast<QMouseEvent*>(e)->position().toPoint()))
288 hideTip();
289 break;
290 default:
291 break;
292 }
293 return false;
294}
295
296QScreen *QTipLabel::getTipScreen(const QPoint &pos, QWidget *w)
297{
298 QScreen *guess = w ? w->screen() : QGuiApplication::primaryScreen();
299 QScreen *exact = guess->virtualSiblingAt(pos);
300 return exact ? exact : guess;
301}
302
303void QTipLabel::placeTip(const QPoint &pos, QWidget *w)
304{
305#if QT_CONFIG(style_stylesheet)
306 if (testAttribute(Qt::WA_StyleSheet) || (w && qt_styleSheet(w->style()))) {
307 //the stylesheet need to know the real parent
308 QTipLabel::instance->setProperty("_q_stylesheet_parent", QVariant::fromValue(w));
309 //we force the style to be the QStyleSheetStyle, and force to clear the cache as well.
310 QTipLabel::instance->setStyleSheet("/* */"_L1);
311
312 // Set up for cleaning up this later...
313 QTipLabel::instance->styleSheetParent = w;
314 if (w) {
315 connect(w, &QWidget::destroyed,
316 QTipLabel::instance, &QTipLabel::styleSheetParentDestroyed);
317 }
318 // QTBUG-64550: A font inherited by the style sheet might change the size,
319 // particular on Windows, where the tip is not parented on a window.
320 // The updatesSize() also makes sure that the content size be updated with
321 // correct content margin.
322 QTipLabel::instance->updateSize(pos);
323 }
324#endif //QT_CONFIG(style_stylesheet)
325
326 QPoint p = pos;
327 const QScreen *screen = getTipScreen(pos, w);
328 // a QScreen's handle *should* never be null, so this is a bit paranoid
329 if (const QPlatformScreen *platformScreen = screen ? screen->handle() : nullptr) {
330 QPlatformCursor *cursor = platformScreen->cursor();
331 // default implementation of QPlatformCursor::size() returns QSize(16, 16)
332 const QSize nativeSize = cursor ? cursor->size() : QSize(16, 16);
333 const QSize cursorSize = QHighDpi::fromNativePixels(nativeSize,
334 platformScreen);
335 QPoint offset(2, cursorSize.height());
336 // assuming an arrow shape, we can just move to the side for very large cursors
337 if (cursorSize.height() > 2 * this->height())
338 offset = QPoint(cursorSize.width() / 2, 0);
339
340 p += offset;
341
342#if QT_CONFIG(wayland)
343 if (w) {
344 create();
345 if (auto waylandWindow = dynamic_cast<QNativeInterface::Private::QWaylandWindow*>(windowHandle()->handle())) {
346 QWidget *parentWindow = w->window();
347 parentWindow->createWinId();
348 windowHandle()->setTransientParent(parentWindow->windowHandle());
349
350 // based on the existing code below, by default position at 'p' stored at the bottom right of our rect
351 // then flip to the other arbitrary 4x24 space if constrained
352 const QRect controlGeometry = QRect(p.x() - 4, p.y() - 24, 4, 24)
353 .translated(-parentWindow->mapToGlobal(QPoint(0, 0)));
354 waylandWindow->setParentControlGeometry(controlGeometry);
355 waylandWindow->setExtendedWindowType(QNativeInterface::Private::QWaylandWindow::ToolTip);
356 }
357 }
358#endif
359
360 QRect screenRect = screen->availableGeometry();
361 if (p.x() + this->width() > screenRect.x() + screenRect.width())
362 p.rx() -= 4 + this->width();
363 if (p.y() + this->height() > screenRect.y() + screenRect.height())
364 p.ry() -= 24 + this->height();
365 if (p.y() < screenRect.y())
366 p.setY(screenRect.y());
367 if (p.x() + this->width() > screenRect.x() + screenRect.width())
368 p.setX(screenRect.x() + screenRect.width() - this->width());
369 if (p.x() < screenRect.x())
370 p.setX(screenRect.x());
371 if (p.y() + this->height() > screenRect.y() + screenRect.height())
372 p.setY(screenRect.y() + screenRect.height() - this->height());
373 }
374 this->move(p);
375}
376
377bool QTipLabel::tipChanged(const QPoint &pos, const QString &text, QObject *o)
378{
379 if (QTipLabel::instance->text() != text)
380 return true;
381
382 if (o != widget)
383 return true;
384
385 if (!rect.isNull())
386 return !rect.contains(pos);
387 else
388 return false;
389}
390
391#if QT_CONFIG(style_stylesheet)
392/** \internal
393 Cleanup the _q_stylesheet_parent property.
394 */
395void QTipLabel::styleSheetParentDestroyed()
396{
397 setProperty("_q_stylesheet_parent", QVariant());
398 styleSheetParent = nullptr;
399}
400#endif
401
402/*!
403 Shows \a text as a tool tip, with the global position \a pos as
404 the point of interest. The tool tip will be shown with a platform
405 specific offset from this point of interest.
406
407 If you specify a non-empty rect the tip will be hidden as soon
408 as you move your cursor out of this area.
409
410 The \a rect is in the coordinates of the widget you specify with
411 \a w. If the \a rect is not empty you must specify a widget.
412 Otherwise this argument can be \nullptr but it is used to
413 determine the appropriate screen on multi-head systems.
414
415 The \a msecDisplayTime parameter specifies for how long the tool tip
416 will be displayed, in milliseconds. With the default value of -1, the
417 time is based on the length of the text.
418
419 If \a text is empty the tool tip is hidden. If the text is the
420 same as the currently shown tooltip, the tip will \e not move.
421 You can force moving by first hiding the tip with an empty text,
422 and then showing the new tip at the new position.
423*/
424
425void QToolTip::showText(const QPoint &pos, const QString &text, QWidget *w, const QRect &rect, int msecDisplayTime)
426{
427 showText(pos, text, w, rect, std::chrono::milliseconds(msecDisplayTime));
428}
429
430/*!
431 \overload
432 Shows \a text as a tool tip, with the global position \a pos as
433 the point of interest. The tool tip will be shown with a platform
434 specific offset from this point of interest.
435
436 If you specify a non-empty rect the tip will be hidden as soon
437 as you move your cursor out of this area.
438
439 The \a rect is in the coordinates of the widget you specify with
440 \a w. If the \a rect is not empty you must specify a widget.
441 Otherwise this argument can be \nullptr but it is used to
442 determine the appropriate screen on multi-head systems.
443
444 The \a displayTime parameter specifies for how long the tool tip
445 will be displayed in milliseconds. With the default value of -1, the
446 time is based on the length of the text.
447
448 If \a text is empty the tool tip is hidden. If the text is the
449 same as the currently shown tooltip, the tip will \e not move.
450 You can force moving by first hiding the tip with an empty text,
451 and then showing the new tip at the new position.
452
453 \since 6.13
454 */
455void QToolTip::showText(QPoint pos, const QString &text, QWidget *w, const QRect &rect,
456 std::chrono::milliseconds displayTime)
457{
458 if (QTipLabel::instance && QTipLabel::instance->isVisible()) { // a tip does already exist
459 if (text.isEmpty()){ // empty text means hide current tip
460 QTipLabel::instance->hideTip();
461 return;
462 } else if (!QTipLabel::instance->fadingOut) {
463 // If the tip has changed, reuse the one
464 // that is showing (removes flickering)
465 QPoint localPos = pos;
466 if (w)
467 localPos = w->mapFromGlobal(pos);
468 if (QTipLabel::instance->tipChanged(localPos, text, w)){
469 QTipLabel::instance->reuseTip(text, displayTime, pos);
470 QTipLabel::instance->setTipRect(w, rect);
471 QTipLabel::instance->placeTip(pos, w);
472 }
473 return;
474 }
475 }
476
477 if (!text.isEmpty()) { // no tip can be reused, create new tip:
478 QWidget *tipLabelParent = [w]() -> QWidget* {
479#ifdef Q_OS_WIN32
480 // On windows, we can't use the widget as parent otherwise the window will be
481 // raised when the tooltip will be shown
482 Q_UNUSED(w);
483 return nullptr;
484#else
485 return w;
486#endif
487 }();
488 new QTipLabel(text, pos, tipLabelParent, displayTime); // sets QTipLabel::instance to itself
489 QWidgetPrivate::get(QTipLabel::instance)->setScreen(QTipLabel::getTipScreen(pos, w));
490 QTipLabel::instance->setTipRect(w, rect);
491 QTipLabel::instance->placeTip(pos, w);
492 QTipLabel::instance->setObjectName("qtooltip_label"_L1);
493
494#if QT_CONFIG(effects)
495 if (QApplication::isEffectEnabled(Qt::UI_FadeTooltip))
496 qFadeEffect(QTipLabel::instance);
497 else if (QApplication::isEffectEnabled(Qt::UI_AnimateTooltip))
498 qScrollEffect(QTipLabel::instance);
499 else
500 QTipLabel::instance->showNormal();
501#else
502 QTipLabel::instance->showNormal();
503#endif
504 }
505}
506
507/*!
508 \fn void QToolTip::hideText()
509 \since 4.2
510
511 Hides the tool tip. This is the same as calling showText() with an
512 empty string.
513
514 \sa showText()
515*/
516
517
518/*!
519 \since 4.4
520
521 Returns \c true if a tooltip is currently shown.
522
523 \sa showText()
524 */
525bool QToolTip::isVisible()
526{
527 return (QTipLabel::instance != nullptr && QTipLabel::instance->isVisible());
528}
529
530/*!
531 \since 4.4
532
533 Returns the tooltip text, if a tooltip is visible, or an
534 empty string if a tooltip is not visible.
535 */
536QString QToolTip::text()
537{
538 if (QTipLabel::instance)
539 return QTipLabel::instance->text();
540 return QString();
541}
542
543
544Q_GLOBAL_STATIC(QPalette, tooltip_palette)
545
546/*!
547 Returns the palette used to render tooltips.
548
549 \note Tool tips use the inactive color group of QPalette, because tool
550 tips are not active windows.
551*/
552QPalette QToolTip::palette()
553{
554 return *tooltip_palette();
555}
556
557/*!
558 \since 4.2
559
560 Returns the font used to render tooltips.
561*/
562QFont QToolTip::font()
563{
564 return QApplication::font("QTipLabel");
565}
566
567/*!
568 \since 4.2
569
570 Sets the \a palette used to render tooltips.
571
572 \note Tool tips use the inactive color group of QPalette, because tool
573 tips are not active windows.
574*/
575void QToolTip::setPalette(const QPalette &palette)
576{
577 *tooltip_palette() = palette;
578 if (QTipLabel::instance)
579 QTipLabel::instance->setPalette(palette);
580}
581
582/*!
583 \since 4.2
584
585 Sets the \a font used to render tooltips.
586*/
587void QToolTip::setFont(const QFont &font)
588{
589 QApplication::setFont(font, "QTipLabel");
590}
591
592QT_END_NAMESPACE
593
594#include "moc_qtooltip_p.cpp"
#define qApp