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

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