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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

source code of qtbase/src/widgets/kernel/qtooltip.cpp