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