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 "qwhatsthis.h"
41#include "qpointer.h"
42#include "qapplication.h"
43#include <private/qguiapplication_p.h>
44#include "qwidget.h"
45#include "qevent.h"
46#include "qpixmap.h"
47#include "qscreen.h"
48#include "qpainter.h"
49#include "qtimer.h"
50#if QT_CONFIG(action)
51#include "qaction.h"
52#endif // QT_CONFIG(action)
53#include "qcursor.h"
54#include "qbitmap.h"
55#include "qtextdocument.h"
56#include <qpa/qplatformtheme.h>
57#include "private/qtextdocumentlayout_p.h"
58#include "qdebug.h"
59#ifndef QT_NO_ACCESSIBILITY
60#include "qaccessible.h"
61#endif
62
63QT_BEGIN_NAMESPACE
64
65/*!
66 \class QWhatsThis
67 \brief The QWhatsThis class provides a simple description of any
68 widget, i.e. answering the question "What's This?".
69
70 \ingroup helpsystem
71 \inmodule QtWidgets
72
73 "What's This?" help is part of an application's online help
74 system, and provides users with information about the
75 functionality and usage of a particular widget. "What's This?"
76 help texts are typically longer and more detailed than
77 \l{QToolTip}{tooltips}, but generally provide less information
78 than that supplied by separate help windows.
79
80 QWhatsThis provides a single window with an explanatory text that
81 pops up when the user asks "What's This?". The default way for
82 users to ask the question is to move the focus to the relevant
83 widget and press Shift+F1. The help text appears immediately; it
84 goes away as soon as the user does something else.
85 (Note that if there is a shortcut for Shift+F1, this mechanism
86 will not work.) Some dialogs provide a "?" button that users can
87 click to enter "What's This?" mode; they then click the relevant
88 widget to pop up the "What's This?" window. It is also possible to
89 provide a a menu option or toolbar button to switch into "What's
90 This?" mode.
91
92 To add "What's This?" text to a widget or an action, you simply
93 call QWidget::setWhatsThis() or QAction::setWhatsThis().
94
95 The text can be either rich text or plain text. If you specify a
96 rich text formatted string, it will be rendered using the default
97 stylesheet, making it possible to embed images in the displayed
98 text. To be as fast as possible, the default stylesheet uses a
99 simple method to determine whether the text can be rendered as
100 plain text. See Qt::mightBeRichText() for details.
101
102 \snippet whatsthis/whatsthis.cpp 0
103
104 An alternative way to enter "What's This?" mode is to call
105 createAction(), and add the returned QAction to either a menu or
106 a tool bar. By invoking this context help action (in the picture
107 below, the button with the arrow and question mark icon) the user
108 switches into "What's This?" mode. If they now click on a widget
109 the appropriate help text is shown. The mode is left when help is
110 given or when the user presses Esc.
111
112 \image whatsthis.png
113
114 You can enter "What's This?" mode programmatically with
115 enterWhatsThisMode(), check the mode with inWhatsThisMode(), and
116 return to normal mode with leaveWhatsThisMode().
117
118 If you want to control the "What's This?" behavior of a widget
119 manually see Qt::WA_CustomWhatsThis.
120
121 It is also possible to show different help texts for different
122 regions of a widget, by using a QHelpEvent of type
123 QEvent::WhatsThis. Intercept the help event in your widget's
124 QWidget::event() function and call QWhatsThis::showText() with the
125 text you want to display for the position specified in
126 QHelpEvent::pos(). If the text is rich text and the user clicks
127 on a link, the widget also receives a QWhatsThisClickedEvent with
128 the link's reference as QWhatsThisClickedEvent::href(). If a
129 QWhatsThisClickedEvent is handled (i.e. QWidget::event() returns
130 true), the help window remains visible. Call
131 QWhatsThis::hideText() to hide it explicitly.
132
133 \sa QToolTip
134*/
135
136Q_CORE_EXPORT void qDeleteInEventHandler(QObject *o);
137
138class QWhatsThat : public QWidget
139{
140 Q_OBJECT
141
142public:
143 QWhatsThat(const QString& txt, QWidget* parent, QWidget *showTextFor);
144 ~QWhatsThat() ;
145
146 static QWhatsThat *instance;
147
148protected:
149 void showEvent(QShowEvent *e) override;
150 void mousePressEvent(QMouseEvent*) override;
151 void mouseReleaseEvent(QMouseEvent*) override;
152 void mouseMoveEvent(QMouseEvent*) override;
153 void keyPressEvent(QKeyEvent*) override;
154 void paintEvent(QPaintEvent*) override;
155
156private:
157 QPointer<QWidget>widget;
158 bool pressed;
159 QString text;
160 QTextDocument* doc;
161 QString anchor;
162 QPixmap background;
163};
164
165QWhatsThat *QWhatsThat::instance = nullptr;
166
167// shadowWidth not const, for XP drop-shadow-fu turns it to 0
168static int shadowWidth = 6; // also used as '5' and '6' and even '8' below
169static const int vMargin = 8;
170static const int hMargin = 12;
171
172static inline bool dropShadow()
173{
174 if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme())
175 return theme->themeHint(QPlatformTheme::DropShadow).toBool();
176 return false;
177}
178
179QWhatsThat::QWhatsThat(const QString& txt, QWidget* parent, QWidget *showTextFor)
180 : QWidget(parent, Qt::Popup),
181 widget(showTextFor), pressed(false), text(txt)
182{
183 delete instance;
184 instance = this;
185 setAttribute(Qt::WA_DeleteOnClose, true);
186 setAttribute(Qt::WA_NoSystemBackground, true);
187 if (parent)
188 setPalette(parent->palette());
189 setMouseTracking(true);
190 setFocusPolicy(Qt::StrongFocus);
191#ifndef QT_NO_CURSOR
192 setCursor(Qt::ArrowCursor);
193#endif
194 QRect r;
195 doc = nullptr;
196 ensurePolished(); // Ensures style sheet font before size calc
197 if (Qt::mightBeRichText(text)) {
198 doc = new QTextDocument();
199 doc->setUndoRedoEnabled(false);
200 doc->setDefaultFont(QApplication::font(this));
201#ifdef QT_NO_TEXTHTMLPARSER
202 doc->setPlainText(text);
203#else
204 doc->setHtml(text);
205#endif
206 doc->setUndoRedoEnabled(false);
207 doc->adjustSize();
208 r.setTop(0);
209 r.setLeft(0);
210 r.setSize(doc->size().toSize());
211 }
212 else
213 {
214 int sw = QGuiApplication::primaryScreen()->virtualGeometry().width() / 3;
215 if (sw < 200)
216 sw = 200;
217 else if (sw > 300)
218 sw = 300;
219
220 r = fontMetrics().boundingRect(0, 0, sw, 1000,
221 Qt::AlignLeft | Qt::AlignTop
222 | Qt::TextWordWrap | Qt::TextExpandTabs,
223 text);
224 }
225 shadowWidth = dropShadow() ? 0 : 6;
226 resize(r.width() + 2*hMargin + shadowWidth, r.height() + 2*vMargin + shadowWidth);
227}
228
229QWhatsThat::~QWhatsThat()
230{
231 instance = nullptr;
232 if (doc)
233 delete doc;
234}
235
236void QWhatsThat::showEvent(QShowEvent *)
237{
238 background = QGuiApplication::primaryScreen()->grabWindow(0, x(), y(), width(), height());
239}
240
241void QWhatsThat::mousePressEvent(QMouseEvent* e)
242{
243 pressed = true;
244 if (e->button() == Qt::LeftButton && rect().contains(e->position().toPoint())) {
245 if (doc)
246 anchor = doc->documentLayout()->anchorAt(e->position().toPoint() - QPoint(hMargin, vMargin));
247 return;
248 }
249 close();
250}
251
252void QWhatsThat::mouseReleaseEvent(QMouseEvent* e)
253{
254 if (!pressed)
255 return;
256 if (widget && e->button() == Qt::LeftButton && doc && rect().contains(e->position().toPoint())) {
257 QString a = doc->documentLayout()->anchorAt(e->position().toPoint() - QPoint(hMargin, vMargin));
258 QString href;
259 if (anchor == a)
260 href = a;
261 anchor.clear();
262 if (!href.isEmpty()) {
263 QWhatsThisClickedEvent e(href);
264 if (QCoreApplication::sendEvent(widget, &e))
265 return;
266 }
267 }
268 close();
269}
270
271void QWhatsThat::mouseMoveEvent(QMouseEvent* e)
272{
273#ifdef QT_NO_CURSOR
274 Q_UNUSED(e);
275#else
276 if (!doc)
277 return;
278 QString a = doc->documentLayout()->anchorAt(e->position().toPoint() - QPoint(hMargin, vMargin));
279 if (!a.isEmpty())
280 setCursor(Qt::PointingHandCursor);
281 else
282 setCursor(Qt::ArrowCursor);
283#endif
284}
285
286void QWhatsThat::keyPressEvent(QKeyEvent*)
287{
288 close();
289}
290
291void QWhatsThat::paintEvent(QPaintEvent*)
292{
293 const bool drawShadow = dropShadow();
294
295 QRect r = rect();
296 r.adjust(0, 0, -1, -1);
297 if (drawShadow)
298 r.adjust(0, 0, -shadowWidth, -shadowWidth);
299 QPainter p(this);
300 p.drawPixmap(0, 0, background);
301 p.setPen(QPen(palette().toolTipText(), 0));
302 p.setBrush(palette().toolTipBase());
303 p.drawRect(r);
304 int w = r.width();
305 int h = r.height();
306 p.setPen(palette().brush(QPalette::Dark).color());
307 p.drawRect(1, 1, w-2, h-2);
308 if (drawShadow) {
309 p.setPen(palette().shadow().color());
310 p.drawPoint(w + 5, 6);
311 p.drawLine(w + 3, 6, w + 5, 8);
312 p.drawLine(w + 1, 6, w + 5, 10);
313 int i;
314 for(i=7; i < h; i += 2)
315 p.drawLine(w, i, w + 5, i + 5);
316 for(i = w - i + h; i > 6; i -= 2)
317 p.drawLine(i, h, i + 5, h + 5);
318 for(; i > 0 ; i -= 2)
319 p.drawLine(6, h + 6 - i, i + 5, h + 5);
320 }
321 r.adjust(0, 0, 1, 1);
322 p.setPen(palette().toolTipText().color());
323 r.adjust(hMargin, vMargin, -hMargin, -vMargin);
324
325 if (doc) {
326 p.translate(r.x(), r.y());
327 QRect rect = r;
328 rect.translate(-r.x(), -r.y());
329 p.setClipRect(rect);
330 QAbstractTextDocumentLayout::PaintContext context;
331 context.palette.setBrush(QPalette::Text, context.palette.toolTipText());
332 doc->documentLayout()->draw(&p, context);
333 }
334 else
335 {
336 p.drawText(r, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap | Qt::TextExpandTabs, text);
337 }
338}
339
340static const char * const button_image[] = {
341"16 16 3 1",
342" c None",
343"o c #000000",
344"a c #000080",
345"o aaaaa ",
346"oo aaa aaa ",
347"ooo aaa aaa",
348"oooo aa aa",
349"ooooo aa aa",
350"oooooo a aaa",
351"ooooooo aaa ",
352"oooooooo aaa ",
353"ooooooooo aaa ",
354"ooooo aaa ",
355"oo ooo ",
356"o ooo aaa ",
357" ooo aaa ",
358" ooo ",
359" ooo ",
360" ooo "};
361
362class QWhatsThisPrivate : public QObject
363{
364 public:
365 QWhatsThisPrivate();
366 ~QWhatsThisPrivate();
367 static QWhatsThisPrivate *instance;
368 bool eventFilter(QObject *, QEvent *) override;
369#if QT_CONFIG(action)
370 QPointer<QAction> action;
371#endif // QT_CONFIG(action)
372 static void say(QWidget *, const QString &, int x = 0, int y = 0);
373 static void notifyToplevels(QEvent *e);
374 bool leaveOnMouseRelease;
375};
376
377void QWhatsThisPrivate::notifyToplevels(QEvent *e)
378{
379 const QWidgetList toplevels = QApplication::topLevelWidgets();
380 for (auto *w : toplevels)
381 QCoreApplication::sendEvent(w, e);
382}
383
384QWhatsThisPrivate *QWhatsThisPrivate::instance = nullptr;
385
386QWhatsThisPrivate::QWhatsThisPrivate()
387 : leaveOnMouseRelease(false)
388{
389 instance = this;
390 qApp->installEventFilter(this);
391
392 QPoint pos = QCursor::pos();
393 if (QWidget *w = QApplication::widgetAt(pos)) {
394 QHelpEvent e(QEvent::QueryWhatsThis, w->mapFromGlobal(pos), pos);
395 const bool sentEvent = QCoreApplication::sendEvent(w, &e);
396#ifdef QT_NO_CURSOR
397 Q_UNUSED(sentEvent);
398#else
399 QGuiApplication::setOverrideCursor((!sentEvent || !e.isAccepted())?
400 Qt::ForbiddenCursor:Qt::WhatsThisCursor);
401 } else {
402 QGuiApplication::setOverrideCursor(Qt::WhatsThisCursor);
403#endif
404 }
405#ifndef QT_NO_ACCESSIBILITY
406 QAccessibleEvent event(this, QAccessible::ContextHelpStart);
407 QAccessible::updateAccessibility(&event);
408#endif
409}
410
411QWhatsThisPrivate::~QWhatsThisPrivate()
412{
413#if QT_CONFIG(action)
414 if (action)
415 action->setChecked(false);
416#endif // QT_CONFIG(action)
417#ifndef QT_NO_CURSOR
418 QGuiApplication::restoreOverrideCursor();
419#endif
420#ifndef QT_NO_ACCESSIBILITY
421 QAccessibleEvent event(this, QAccessible::ContextHelpEnd);
422 QAccessible::updateAccessibility(&event);
423#endif
424 instance = nullptr;
425}
426
427bool QWhatsThisPrivate::eventFilter(QObject *o, QEvent *e)
428{
429 if (!o->isWidgetType())
430 return false;
431 QWidget * w = static_cast<QWidget *>(o);
432 bool customWhatsThis = w->testAttribute(Qt::WA_CustomWhatsThis);
433 switch (e->type()) {
434 case QEvent::MouseButtonPress:
435 {
436 QMouseEvent *me = static_cast<QMouseEvent*>(e);
437 if (me->button() == Qt::RightButton || customWhatsThis)
438 return false;
439 QHelpEvent e(QEvent::WhatsThis, me->position().toPoint(), me->globalPosition().toPoint());
440 if (!QCoreApplication::sendEvent(w, &e) || !e.isAccepted())
441 leaveOnMouseRelease = true;
442
443 } break;
444
445 case QEvent::MouseMove:
446 {
447 QMouseEvent *me = static_cast<QMouseEvent*>(e);
448 QHelpEvent e(QEvent::QueryWhatsThis, me->position().toPoint(), me->globalPosition().toPoint());
449 const bool sentEvent = QCoreApplication::sendEvent(w, &e);
450#ifdef QT_NO_CURSOR
451 Q_UNUSED(sentEvent);
452#else
453 QGuiApplication::changeOverrideCursor((!sentEvent || !e.isAccepted())?
454 Qt::ForbiddenCursor:Qt::WhatsThisCursor);
455#endif
456 Q_FALLTHROUGH();
457 }
458 case QEvent::MouseButtonRelease:
459 case QEvent::MouseButtonDblClick:
460 if (leaveOnMouseRelease && e->type() == QEvent::MouseButtonRelease)
461 QWhatsThis::leaveWhatsThisMode();
462 if (static_cast<QMouseEvent*>(e)->button() == Qt::RightButton || customWhatsThis)
463 return false; // ignore RMB release
464 break;
465 case QEvent::KeyPress:
466 {
467 QKeyEvent* kev = (QKeyEvent*)e;
468#if QT_CONFIG(shortcut)
469 if (kev->matches(QKeySequence::Cancel)) {
470 QWhatsThis::leaveWhatsThisMode();
471 return true;
472 } else
473#endif
474 if (customWhatsThis) {
475 return false;
476 } else if (kev->key() == Qt::Key_Menu ||
477 (kev->key() == Qt::Key_F10 &&
478 kev->modifiers() == Qt::ShiftModifier)) {
479 // we don't react to these keys, they are used for context menus
480 return false;
481 } else if (kev->key() != Qt::Key_Shift && kev->key() != Qt::Key_Alt // not a modifier key
482 && kev->key() != Qt::Key_Control && kev->key() != Qt::Key_Meta) {
483 QWhatsThis::leaveWhatsThisMode();
484 }
485 } break;
486 default:
487 return false;
488 }
489 return true;
490}
491
492#if QT_CONFIG(action)
493class QWhatsThisAction: public QAction
494{
495 Q_OBJECT
496
497public:
498 explicit QWhatsThisAction(QObject* parent = nullptr);
499
500private slots:
501 void actionTriggered();
502};
503
504QWhatsThisAction::QWhatsThisAction(QObject *parent) : QAction(tr("What's This?"), parent)
505{
506#ifndef QT_NO_IMAGEFORMAT_XPM
507 QPixmap p(button_image);
508 setIcon(p);
509#endif
510 setCheckable(true);
511 connect(this, SIGNAL(triggered()), this, SLOT(actionTriggered()));
512#ifndef QT_NO_SHORTCUT
513 setShortcut(Qt::ShiftModifier | Qt::Key_F1);
514#endif
515}
516
517void QWhatsThisAction::actionTriggered()
518{
519 if (isChecked()) {
520 QWhatsThis::enterWhatsThisMode();
521 QWhatsThisPrivate::instance->action = this;
522 }
523}
524#endif // QT_CONFIG(action)
525
526/*!
527 This function switches the user interface into "What's This?"
528 mode. The user interface can be switched back into normal mode by
529 the user (e.g. by them clicking or pressing Esc), or
530 programmatically by calling leaveWhatsThisMode().
531
532 When entering "What's This?" mode, a QEvent of type
533 Qt::EnterWhatsThisMode is sent to all toplevel widgets.
534
535 \sa inWhatsThisMode(), leaveWhatsThisMode()
536*/
537void QWhatsThis::enterWhatsThisMode()
538{
539 if (QWhatsThisPrivate::instance)
540 return;
541 (void) new QWhatsThisPrivate;
542 QEvent e(QEvent::EnterWhatsThisMode);
543 QWhatsThisPrivate::notifyToplevels(&e);
544 }
545
546/*!
547 Returns \c true if the user interface is in "What's This?" mode;
548 otherwise returns \c false.
549
550 \sa enterWhatsThisMode()
551*/
552bool QWhatsThis::inWhatsThisMode()
553{
554 return (QWhatsThisPrivate::instance != nullptr);
555}
556
557/*!
558 If the user interface is in "What's This?" mode, this function
559 switches back to normal mode; otherwise it does nothing.
560
561 When leaving "What's This?" mode, a QEvent of type
562 Qt::LeaveWhatsThisMode is sent to all toplevel widgets.
563
564 \sa enterWhatsThisMode(), inWhatsThisMode()
565*/
566void QWhatsThis::leaveWhatsThisMode()
567{
568 delete QWhatsThisPrivate::instance;
569 QEvent e(QEvent::LeaveWhatsThisMode);
570 QWhatsThisPrivate::notifyToplevels(&e);
571}
572
573void QWhatsThisPrivate::say(QWidget * widget, const QString &text, int x, int y)
574{
575 if (text.size() == 0)
576 return;
577 // make a fresh widget, and set it up
578 QWhatsThat *whatsThat = new QWhatsThat(text, nullptr, widget);
579
580 // okay, now to find a suitable location
581 QScreen *screen = widget ? widget->screen()
582 : QGuiApplication::screenAt(QPoint(x, y));
583 if (!screen)
584 screen = QGuiApplication::primaryScreen();
585 QRect screenRect = screen->geometry();
586
587 int w = whatsThat->width();
588 int h = whatsThat->height();
589 int sx = screenRect.x();
590 int sy = screenRect.y();
591
592 // first try locating the widget immediately above/below,
593 // with nice alignment if possible.
594 QPoint pos;
595 if (widget)
596 pos = widget->mapToGlobal(QPoint(0,0));
597
598 if (widget && w > widget->width() + 16)
599 x = pos.x() + widget->width()/2 - w/2;
600 else
601 x = x - w/2;
602
603 // squeeze it in if that would result in part of what's this
604 // being only partially visible
605 if (x + w + shadowWidth > sx+screenRect.width()) {
606 x = (widget ? qMin(screenRect.width(), pos.x() + widget->width())
607 : screenRect.width())
608 - w;
609 }
610
611 if (x < sx)
612 x = sx;
613
614 if (widget && h > widget->height() + 16) {
615 y = pos.y() + widget->height() + 2; // below, two pixels spacing
616 // what's this is above or below, wherever there's most space
617 if (y + h + 10 > sy + screenRect.height())
618 y = pos.y() + 2 - shadowWidth - h; // above, overlap
619 }
620 y = y + 2;
621
622 // squeeze it in if that would result in part of what's this
623 // being only partially visible
624 if (y + h + shadowWidth > sy + screenRect.height()) {
625 y = (widget ? qMin(screenRect.height(), pos.y() + widget->height())
626 : screenRect.height())
627 - h;
628 }
629 if (y < sy)
630 y = sy;
631
632 whatsThat->move(x, y);
633 whatsThat->show();
634 whatsThat->grabKeyboard();
635}
636
637/*!
638 Shows \a text as a "What's This?" window, at global position \a
639 pos. The optional widget argument, \a w, is used to determine the
640 appropriate screen on multi-head systems.
641
642 \sa hideText()
643*/
644void QWhatsThis::showText(const QPoint &pos, const QString &text, QWidget *w)
645{
646 leaveWhatsThisMode();
647 QWhatsThisPrivate::say(w, text, pos.x(), pos.y());
648}
649
650/*!
651 If a "What's This?" window is showing, this destroys it.
652
653 \sa showText()
654*/
655void QWhatsThis::hideText()
656{
657 qDeleteInEventHandler(QWhatsThat::instance);
658}
659
660/*!
661 Returns a ready-made QAction, used to invoke "What's This?" context
662 help, with the given \a parent.
663
664 The returned QAction provides a convenient way to let users enter
665 "What's This?" mode.
666*/
667#if QT_CONFIG(action)
668QAction *QWhatsThis::createAction(QObject *parent)
669{
670 return new QWhatsThisAction(parent);
671}
672#endif // QT_CONFIG(action)
673
674QT_END_NAMESPACE
675
676#include "qwhatsthis.moc"
677