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