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 | |
27 | QT_BEGIN_NAMESPACE |
28 | |
29 | using 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 | |
90 | class QTipLabel : public QLabel |
91 | { |
92 | Q_OBJECT |
93 | public: |
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); |
116 | protected: |
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 |
123 | public 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 | |
132 | private: |
133 | QWidget *styleSheetParent; |
134 | #endif |
135 | |
136 | private: |
137 | QWidget *widget; |
138 | QRect rect; |
139 | }; |
140 | |
141 | QTipLabel *QTipLabel::instance = nullptr; |
142 | |
143 | QTipLabel::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 | |
167 | void 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 | |
178 | void 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 | |
193 | void 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 | |
212 | void 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 | |
223 | void 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 | |
234 | void 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 | |
246 | QTipLabel::~QTipLabel() |
247 | { |
248 | instance = nullptr; |
249 | } |
250 | |
251 | void QTipLabel::hideTip() |
252 | { |
253 | if (!hideTimer.isActive()) |
254 | hideTimer.start(msec: 300, obj: this); |
255 | } |
256 | |
257 | void QTipLabel::hideTipImmediately() |
258 | { |
259 | close(); // to trigger QEvent::Close which stops the animation |
260 | deleteLater(); |
261 | } |
262 | |
263 | void 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 | |
273 | void 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 | |
283 | bool 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 | |
339 | QScreen *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 | |
346 | void 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 | |
402 | bool 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 | |
439 | void 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 | */ |
508 | bool 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 | */ |
519 | QString QToolTip::text() |
520 | { |
521 | if (QTipLabel::instance) |
522 | return QTipLabel::instance->text(); |
523 | return QString(); |
524 | } |
525 | |
526 | |
527 | Q_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 | */ |
535 | QPalette QToolTip::palette() |
536 | { |
537 | return *tooltip_palette(); |
538 | } |
539 | |
540 | /*! |
541 | \since 4.2 |
542 | |
543 | Returns the font used to render tooltips. |
544 | */ |
545 | QFont 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 | */ |
558 | void 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 | */ |
570 | void QToolTip::setFont(const QFont &font) |
571 | { |
572 | QApplication::setFont(font, className: "QTipLabel"); |
573 | } |
574 | |
575 | QT_END_NAMESPACE |
576 | |
577 | #include "qtooltip.moc" |
578 |
Definitions
Learn Advanced QML with KDAB
Find out more