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->geometry();
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/** \internal
385 Cleanup the _q_stylesheet_parent property.
386 */
387void QTipLabel::styleSheetParentDestroyed()
388{
389 setProperty("_q_stylesheet_parent", QVariant());
390 styleSheetParent = nullptr;
391}
392
393/*!
394 Shows \a text as a tool tip, with the global position \a pos as
395 the point of interest. The tool tip will be shown with a platform
396 specific offset from this point of interest.
397
398 If you specify a non-empty rect the tip will be hidden as soon
399 as you move your cursor out of this area.
400
401 The \a rect is in the coordinates of the widget you specify with
402 \a w. If the \a rect is not empty you must specify a widget.
403 Otherwise this argument can be \nullptr but it is used to
404 determine the appropriate screen on multi-head systems.
405
406 The \a msecDisplayTime parameter specifies for how long the tool tip
407 will be displayed, in milliseconds. With the default value of -1, the
408 time is based on the length of the text.
409
410 If \a text is empty the tool tip is hidden. If the text is the
411 same as the currently shown tooltip, the tip will \e not move.
412 You can force moving by first hiding the tip with an empty text,
413 and then showing the new tip at the new position.
414*/
415
416void QToolTip::showText(const QPoint &pos, const QString &text, QWidget *w, const QRect &rect, int msecDisplayTime)
417{
418 if (QTipLabel::instance && QTipLabel::instance->isVisible()) { // a tip does already exist
419 if (text.isEmpty()){ // empty text means hide current tip
420 QTipLabel::instance->hideTip();
421 return;
422 } else if (!QTipLabel::instance->fadingOut) {
423 // If the tip has changed, reuse the one
424 // that is showing (removes flickering)
425 QPoint localPos = pos;
426 if (w)
427 localPos = w->mapFromGlobal(pos);
428 if (QTipLabel::instance->tipChanged(localPos, text, w)){
429 QTipLabel::instance->reuseTip(text, msecDisplayTime, pos);
430 QTipLabel::instance->setTipRect(w, rect);
431 QTipLabel::instance->placeTip(pos, w);
432 }
433 return;
434 }
435 }
436
437 if (!text.isEmpty()) { // no tip can be reused, create new tip:
438 QWidget *tipLabelParent = [w]() -> QWidget* {
439#ifdef Q_OS_WIN32
440 // On windows, we can't use the widget as parent otherwise the window will be
441 // raised when the tooltip will be shown
442 Q_UNUSED(w);
443 return nullptr;
444#else
445 return w;
446#endif
447 }();
448 new QTipLabel(text, pos, tipLabelParent, msecDisplayTime); // sets QTipLabel::instance to itself
449 QWidgetPrivate::get(QTipLabel::instance)->setScreen(QTipLabel::getTipScreen(pos, w));
450 QTipLabel::instance->setTipRect(w, rect);
451 QTipLabel::instance->placeTip(pos, w);
452 QTipLabel::instance->setObjectName("qtooltip_label"_L1);
453
454#if QT_CONFIG(effects)
455 if (QApplication::isEffectEnabled(Qt::UI_FadeTooltip))
456 qFadeEffect(QTipLabel::instance);
457 else if (QApplication::isEffectEnabled(Qt::UI_AnimateTooltip))
458 qScrollEffect(QTipLabel::instance);
459 else
460 QTipLabel::instance->showNormal();
461#else
462 QTipLabel::instance->showNormal();
463#endif
464 }
465}
466
467/*!
468 \fn void QToolTip::hideText()
469 \since 4.2
470
471 Hides the tool tip. This is the same as calling showText() with an
472 empty string.
473
474 \sa showText()
475*/
476
477
478/*!
479 \since 4.4
480
481 Returns \c true if a tooltip is currently shown.
482
483 \sa showText()
484 */
485bool QToolTip::isVisible()
486{
487 return (QTipLabel::instance != nullptr && QTipLabel::instance->isVisible());
488}
489
490/*!
491 \since 4.4
492
493 Returns the tooltip text, if a tooltip is visible, or an
494 empty string if a tooltip is not visible.
495 */
496QString QToolTip::text()
497{
498 if (QTipLabel::instance)
499 return QTipLabel::instance->text();
500 return QString();
501}
502
503
504Q_GLOBAL_STATIC(QPalette, tooltip_palette)
505
506/*!
507 Returns the palette used to render tooltips.
508
509 \note Tool tips use the inactive color group of QPalette, because tool
510 tips are not active windows.
511*/
512QPalette QToolTip::palette()
513{
514 return *tooltip_palette();
515}
516
517/*!
518 \since 4.2
519
520 Returns the font used to render tooltips.
521*/
522QFont QToolTip::font()
523{
524 return QApplication::font("QTipLabel");
525}
526
527/*!
528 \since 4.2
529
530 Sets the \a palette used to render tooltips.
531
532 \note Tool tips use the inactive color group of QPalette, because tool
533 tips are not active windows.
534*/
535void QToolTip::setPalette(const QPalette &palette)
536{
537 *tooltip_palette() = palette;
538 if (QTipLabel::instance)
539 QTipLabel::instance->setPalette(palette);
540}
541
542/*!
543 \since 4.2
544
545 Sets the \a font used to render tooltips.
546*/
547void QToolTip::setFont(const QFont &font)
548{
549 QApplication::setFont(font, "QTipLabel");
550}
551
552QT_END_NAMESPACE
553
554#include "moc_qtooltip_p.cpp"
Combined button and popup list for selecting options.
#define qApp