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