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