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 <qpointer.h>
10#include <qstyle.h>
11#include <qstyleoption.h>
12#include <qstylepainter.h>
13#if QT_CONFIG(effects)
14#include <private/qeffects_p.h>
15#endif
16#include <qtextdocument.h>
17#include <qdebug.h>
18#include <qpa/qplatformscreen.h>
19#include <qpa/qplatformcursor.h>
20#if QT_CONFIG(style_stylesheet)
21#include <private/qstylesheetstyle_p.h>
22#endif
23#include <qpa/qplatformwindow.h>
24#include <qpa/qplatformwindow_p.h>
25
26#include <qlabel.h>
27#include <QtWidgets/private/qlabel_p.h>
28#include <QtGui/private/qhighdpiscaling_p.h>
29#include <qtooltip.h>
30
31#include <QtCore/qbasictimer.h>
32
34
35using namespace Qt::StringLiterals;
36
37/*!
38 \class QToolTip
39
40 \brief The QToolTip class provides tool tips (balloon help) for any
41 widget.
42
43 \ingroup helpsystem
44 \inmodule QtWidgets
45
46 The tip is a short piece of text reminding the user of the
47 widget's function. It is drawn immediately below the given
48 position in a distinctive black-on-yellow color combination. The
49 tip can be any \l{QTextEdit}{rich text} formatted string.
50
51 Rich text displayed in a tool tip is implicitly word-wrapped unless
52 specified differently with \c{<p style='white-space:pre'>}.
53
54 UI elements that are created via \l{QAction} use the tooltip property
55 of the QAction, so for most interactive UI elements, setting that
56 property is the easiest way to provide tool tips.
57
58 \snippet tooltips/main.cpp action_tooltip
59
60 For any other widgets, the simplest and most common way to set
61 a widget's tool tip is by calling its QWidget::setToolTip() function.
62
63 \snippet tooltips/main.cpp static_tooltip
64
65 It is also possible to show different tool tips for different
66 regions of a widget, by using a QHelpEvent of type
67 QEvent::ToolTip. Intercept the help event in your widget's \l
68 {QWidget::}{event()} function and call QToolTip::showText() with
69 the text you want to display.
70
71 \snippet tooltips/main.cpp dynamic_tooltip
72
73 If you are calling QToolTip::hideText(), or QToolTip::showText()
74 with an empty string, as a result of a \l{QEvent::}{ToolTip}-event you
75 should also call \l{QEvent::}{ignore()} on the event, to signal
76 that you don't want to start any tooltip specific modes.
77
78 Note that, if you want to show tooltips in an item view, the
79 model/view architecture provides functionality to set an item's
80 tool tip; e.g., the QTableWidgetItem::setToolTip() function.
81 However, if you want to provide custom tool tips in an item view,
82 you must intercept the help event in the
83 QAbstractItemView::viewportEvent() function and handle it yourself.
84
85 The default tool tip color and font can be customized with
86 setPalette() and setFont(). When a tooltip is currently on
87 display, isVisible() returns \c true and text() the currently visible
88 text.
89
90 \note Tool tips use the inactive color group of QPalette, because tool
91 tips are not active windows.
92
93 \sa QWidget::toolTip, QAction::toolTip
94*/
95
96class QTipLabel : public QLabel
97{
99public:
103
104 void adjustTooltipScreen(const QPoint &pos);
105 void updateSize(const QPoint &pos);
106
107 bool eventFilter(QObject *, QEvent *) override;
108
110
112
113 void reuseTip(const QString &text, int msecDisplayTime, const QPoint &pos);
114 void hideTip();
116 void setTipRect(QWidget *w, const QRect &r);
117 void restartExpireTimer(int msecDisplayTime);
118 bool tipChanged(const QPoint &pos, const QString &text, QObject *o);
119 void placeTip(const QPoint &pos, QWidget *w);
120
121 static QScreen *getTipScreen(const QPoint &pos, QWidget *w);
122protected:
123 void timerEvent(QTimerEvent *e) override;
124 void paintEvent(QPaintEvent *e) override;
125 void mouseMoveEvent(QMouseEvent *e) override;
126 void resizeEvent(QResizeEvent *e) override;
127
128#if QT_CONFIG(style_stylesheet)
129public slots:
130 /** \internal
131 Cleanup the _q_stylesheet_parent property.
132 */
134 setProperty("_q_stylesheet_parent", QVariant());
135 styleSheetParent = nullptr;
136 }
137
138private:
140#endif
141
142private:
143 QWidget *widget;
144 QRect rect;
145};
146
147QTipLabel *QTipLabel::instance = nullptr;
148
149QTipLabel::QTipLabel(const QString &text, const QPoint &pos, QWidget *w, int msecDisplayTime)
150 : QLabel(w, Qt::ToolTip | Qt::BypassGraphicsProxyWidget)
151#if QT_CONFIG(style_stylesheet)
152 , styleSheetParent(nullptr)
153#endif
154 , widget(nullptr)
155{
156 delete instance;
157 instance = this;
158 setForegroundRole(QPalette::ToolTipText);
159 setBackgroundRole(QPalette::ToolTipBase);
160 setPalette(QToolTip::palette());
161 ensurePolished();
162 setMargin(1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, nullptr, this));
163 setFrameStyle(QFrame::NoFrame);
164 setAlignment(Qt::AlignLeft);
165 setIndent(1);
166 qApp->installEventFilter(this);
167 setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, nullptr, this) / 255.0);
168 setMouseTracking(true);
169 fadingOut = false;
170 reuseTip(text, msecDisplayTime, pos);
171}
172
173void QTipLabel::restartExpireTimer(int msecDisplayTime)
174{
175 Q_D(const QLabel);
176 const qsizetype textLength = d->needTextControl() ? d->control->toPlainText().size() : text().size();
177 qsizetype time = 10000 + 40 * qMax(0, textLength - 100);
178 if (msecDisplayTime > 0)
179 time = msecDisplayTime;
180 expireTimer.start(time, this);
181 hideTimer.stop();
182}
183
184void QTipLabel::reuseTip(const QString &text, int msecDisplayTime, const QPoint &pos)
185{
186#if QT_CONFIG(style_stylesheet)
187 if (styleSheetParent){
188 disconnect(styleSheetParent, &QWidget::destroyed,
189 this, &QTipLabel::styleSheetParentDestroyed);
190 styleSheetParent = nullptr;
191 }
192#endif
193
194 setText(text);
195 updateSize(pos);
196 restartExpireTimer(msecDisplayTime);
197}
198
199void QTipLabel::updateSize(const QPoint &pos)
200{
201 d_func()->setScreenForPoint(pos);
202 // Ensure that we get correct sizeHints by placing this window on the right screen.
203 QFontMetrics fm(font());
204 QSize extra(1, 0);
205 // Make it look good with the default ToolTip font on Mac, which has a small descent.
206 if (fm.descent() == 2 && fm.ascent() >= 11)
207 ++extra.rheight();
208 setWordWrap(Qt::mightBeRichText(text()));
209 QSize sh = sizeHint();
210 const QScreen *screen = getTipScreen(pos, this);
211 if (!wordWrap() && sh.width() > screen->geometry().width()) {
212 setWordWrap(true);
213 sh = sizeHint();
214 }
215 resize(sh + extra);
216}
217
218void QTipLabel::paintEvent(QPaintEvent *ev)
219{
220 QStylePainter p(this);
221 QStyleOptionFrame opt;
222 opt.initFrom(this);
223 p.drawPrimitive(QStyle::PE_PanelTipLabel, opt);
224 p.end();
225
226 QLabel::paintEvent(ev);
227}
228
229void QTipLabel::resizeEvent(QResizeEvent *e)
230{
231 QStyleHintReturnMask frameMask;
232 QStyleOption option;
233 option.initFrom(this);
234 if (style()->styleHint(QStyle::SH_ToolTip_Mask, &option, this, &frameMask))
235 setMask(frameMask.region);
236
237 QLabel::resizeEvent(e);
238}
239
240void QTipLabel::mouseMoveEvent(QMouseEvent *e)
241{
242 if (!rect.isNull()) {
243 QPoint pos = e->globalPosition().toPoint();
244 if (widget)
245 pos = widget->mapFromGlobal(pos);
246 if (!rect.contains(pos))
247 hideTip();
248 }
249 QLabel::mouseMoveEvent(e);
250}
251
253{
254 instance = nullptr;
255}
256
258{
259 if (!hideTimer.isActive())
260 hideTimer.start(300, this);
261}
262
264{
265 close(); // to trigger QEvent::Close which stops the animation
266 deleteLater();
267}
268
269void QTipLabel::setTipRect(QWidget *w, const QRect &r)
270{
271 if (Q_UNLIKELY(!r.isNull() && !w)) {
272 qWarning("QToolTip::setTipRect: Cannot pass null widget if rect is set");
273 return;
274 }
275 widget = w;
276 rect = r;
277}
278
279void QTipLabel::timerEvent(QTimerEvent *e)
280{
281 if (e->timerId() == hideTimer.timerId()
282 || e->timerId() == expireTimer.timerId()){
283 hideTimer.stop();
284 expireTimer.stop();
286 }
287}
288
289bool QTipLabel::eventFilter(QObject *o, QEvent *e)
290{
291 switch (e->type()) {
292#ifdef Q_OS_MACOS
293 case QEvent::KeyPress:
294 case QEvent::KeyRelease: {
295 const int key = static_cast<QKeyEvent *>(e)->key();
296 // Anything except key modifiers or caps-lock, etc.
297 if (key < Qt::Key_Shift || key > Qt::Key_ScrollLock)
298 hideTipImmediately();
299 break;
300 }
301#endif
302 case QEvent::Leave:
303 hideTip();
304 break;
305
306
307#if defined (Q_OS_QNX) || defined (Q_OS_WASM) // On QNX the window activate and focus events are delayed and will appear
308 // after the window is shown.
309 case QEvent::WindowActivate:
310 case QEvent::FocusIn:
311 return false;
312 case QEvent::WindowDeactivate:
313 if (o != this)
314 return false;
315 hideTipImmediately();
316 break;
317 case QEvent::FocusOut:
318 if (reinterpret_cast<QWindow*>(o) != windowHandle())
319 return false;
320 hideTipImmediately();
321 break;
322#else
323 case QEvent::WindowActivate:
324 case QEvent::WindowDeactivate:
325 case QEvent::FocusIn:
326 case QEvent::FocusOut:
327#endif
328 case QEvent::MouseButtonPress:
329 case QEvent::MouseButtonRelease:
330 case QEvent::MouseButtonDblClick:
331 case QEvent::Wheel:
333 break;
334
335 case QEvent::MouseMove:
336 if (o == widget && !rect.isNull() && !rect.contains(static_cast<QMouseEvent*>(e)->position().toPoint()))
337 hideTip();
338 break;
339 default:
340 break;
341 }
342 return false;
343}
344
345QScreen *QTipLabel::getTipScreen(const QPoint &pos, QWidget *w)
346{
347 QScreen *guess = w ? w->screen() : QGuiApplication::primaryScreen();
348 QScreen *exact = guess->virtualSiblingAt(pos);
349 return exact ? exact : guess;
350}
351
352void QTipLabel::placeTip(const QPoint &pos, QWidget *w)
353{
354#if QT_CONFIG(style_stylesheet)
355 if (testAttribute(Qt::WA_StyleSheet) || (w && qt_styleSheet(w->style()))) {
356 //the stylesheet need to know the real parent
357 QTipLabel::instance->setProperty("_q_stylesheet_parent", QVariant::fromValue(w));
358 //we force the style to be the QStyleSheetStyle, and force to clear the cache as well.
359 QTipLabel::instance->setStyleSheet("/* */"_L1);
360
361 // Set up for cleaning up this later...
362 QTipLabel::instance->styleSheetParent = w;
363 if (w) {
364 connect(w, &QWidget::destroyed,
365 QTipLabel::instance, &QTipLabel::styleSheetParentDestroyed);
366 }
367 // QTBUG-64550: A font inherited by the style sheet might change the size,
368 // particular on Windows, where the tip is not parented on a window.
369 // The updatesSize() also makes sure that the content size be updated with
370 // correct content margin.
371 QTipLabel::instance->updateSize(pos);
372 }
373#endif //QT_CONFIG(style_stylesheet)
374
375 QPoint p = pos;
376 const QScreen *screen = getTipScreen(pos, w);
377 // a QScreen's handle *should* never be null, so this is a bit paranoid
378 if (const QPlatformScreen *platformScreen = screen ? screen->handle() : nullptr) {
379 QPlatformCursor *cursor = platformScreen->cursor();
380 // default implementation of QPlatformCursor::size() returns QSize(16, 16)
381 const QSize nativeSize = cursor ? cursor->size() : QSize(16, 16);
382 const QSize cursorSize = QHighDpi::fromNativePixels(nativeSize,
383 platformScreen);
384 QPoint offset(2, cursorSize.height());
385 // assuming an arrow shape, we can just move to the side for very large cursors
386 if (cursorSize.height() > 2 * this->height())
387 offset = QPoint(cursorSize.width() / 2, 0);
388
389 p += offset;
390
391#if QT_CONFIG(wayland)
392 create();
393 if (auto waylandWindow = dynamic_cast<QNativeInterface::Private::QWaylandWindow*>(windowHandle()->handle())) {
394 // based on the existing code below, by default position at 'p' stored at the bottom right of our rect
395 // then flip to the other arbitrary 4x24 space if constrained
396 const QRect controlGeometry(QRect(p.x() - 4, p.y() - 24, 4, 24));
397 waylandWindow->setParentControlGeometry(controlGeometry);
398 waylandWindow->setExtendedWindowType(QNativeInterface::Private::QWaylandWindow::ToolTip);
399 }
400#endif
401
402 QRect screenRect = screen->geometry();
403 if (p.x() + this->width() > screenRect.x() + screenRect.width())
404 p.rx() -= 4 + this->width();
405 if (p.y() + this->height() > screenRect.y() + screenRect.height())
406 p.ry() -= 24 + this->height();
407 if (p.y() < screenRect.y())
408 p.setY(screenRect.y());
409 if (p.x() + this->width() > screenRect.x() + screenRect.width())
410 p.setX(screenRect.x() + screenRect.width() - this->width());
411 if (p.x() < screenRect.x())
412 p.setX(screenRect.x());
413 if (p.y() + this->height() > screenRect.y() + screenRect.height())
414 p.setY(screenRect.y() + screenRect.height() - this->height());
415 }
416 this->move(p);
417}
418
419bool QTipLabel::tipChanged(const QPoint &pos, const QString &text, QObject *o)
420{
421 if (QTipLabel::instance->text() != text)
422 return true;
423
424 if (o != widget)
425 return true;
426
427 if (!rect.isNull())
428 return !rect.contains(pos);
429 else
430 return false;
431}
432
433/*!
434 Shows \a text as a tool tip, with the global position \a pos as
435 the point of interest. The tool tip will be shown with a platform
436 specific offset from this point of interest.
437
438 If you specify a non-empty rect the tip will be hidden as soon
439 as you move your cursor out of this area.
440
441 The \a rect is in the coordinates of the widget you specify with
442 \a w. If the \a rect is not empty you must specify a widget.
443 Otherwise this argument can be \nullptr but it is used to
444 determine the appropriate screen on multi-head systems.
445
446 The \a msecDisplayTime parameter specifies for how long the tool tip
447 will be displayed, in milliseconds. With the default value of -1, the
448 time is based on the length of the text.
449
450 If \a text is empty the tool tip is hidden. If the text is the
451 same as the currently shown tooltip, the tip will \e not move.
452 You can force moving by first hiding the tip with an empty text,
453 and then showing the new tip at the new position.
454*/
455
456void QToolTip::showText(const QPoint &pos, const QString &text, QWidget *w, const QRect &rect, int msecDisplayTime)
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, msecDisplayTime, 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, msecDisplayTime); // 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 "qtooltip.moc"
\inmodule QtCore\reentrant
Definition qpoint.h:29
void reuseTip(const QString &text, int msecDisplayTime, const QPoint &pos)
Definition qtooltip.cpp:184
void restartExpireTimer(int msecDisplayTime)
Definition qtooltip.cpp:173
void hideTip()
Definition qtooltip.cpp:257
void resizeEvent(QResizeEvent *e) override
This event handler can be reimplemented in a subclass to receive widget resize events which are passe...
Definition qtooltip.cpp:229
void mouseMoveEvent(QMouseEvent *e) override
\reimp
Definition qtooltip.cpp:240
void hideTipImmediately()
Definition qtooltip.cpp:263
bool tipChanged(const QPoint &pos, const QString &text, QObject *o)
Definition qtooltip.cpp:419
void setTipRect(QWidget *w, const QRect &r)
Definition qtooltip.cpp:269
bool fadingOut
Definition qtooltip.cpp:111
QBasicTimer expireTimer
Definition qtooltip.cpp:109
void paintEvent(QPaintEvent *e) override
\reimp
Definition qtooltip.cpp:218
QBasicTimer hideTimer
Definition qtooltip.cpp:109
void timerEvent(QTimerEvent *e) override
This event handler can be reimplemented in a subclass to receive timer events for the object.
Definition qtooltip.cpp:279
void placeTip(const QPoint &pos, QWidget *w)
Definition qtooltip.cpp:352
static QScreen * getTipScreen(const QPoint &pos, QWidget *w)
Definition qtooltip.cpp:345
static QTipLabel * instance
Definition qtooltip.cpp:102
bool eventFilter(QObject *, QEvent *) override
Filters events if this object has been installed as an event filter for the watched object.
Definition qtooltip.cpp:289
void adjustTooltipScreen(const QPoint &pos)
void updateSize(const QPoint &pos)
Definition qtooltip.cpp:199
Combined button and popup list for selecting options.
#define qApp