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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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