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

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