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

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