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 "qgroupbox.h"
5
6#include "qapplication.h"
7#include "qbitmap.h"
8#include "qdrawutil.h"
9#include "qevent.h"
10#include "qlayout.h"
11#if QT_CONFIG(radiobutton)
12#include "qradiobutton.h"
13#endif
14#include "qstyle.h"
15#include "qstyleoption.h"
16#include "qstylepainter.h"
17#if QT_CONFIG(accessibility)
18#include "qaccessible.h"
19#endif
20#include <private/qwidget_p.h>
21#include <private/qguiapplication_p.h>
22#include <qpa/qplatformtheme.h>
23
24#include "qdebug.h"
25
26QT_BEGIN_NAMESPACE
27
28class QGroupBoxPrivate : public QWidgetPrivate
29{
30 Q_DECLARE_PUBLIC(QGroupBox)
31
32public:
33 void skip();
34 void init();
35 void calculateFrame();
36 QString title;
37 int align;
38#ifndef QT_NO_SHORTCUT
39 int shortcutId;
40#endif
41
42 void _q_fixFocus(Qt::FocusReason reason);
43 void _q_setChildrenEnabled(bool b);
44 void click();
45 bool flat;
46 bool checkable;
47 bool checked;
48 bool hover;
49 bool overCheckBox;
50 QStyle::SubControl pressedControl;
51};
52
53/*!
54 Initialize \a option with the values from this QGroupBox. This method
55 is useful for subclasses when they need a QStyleOptionGroupBox, but don't want
56 to fill in all the information themselves.
57
58 \sa QStyleOption::initFrom()
59*/
60void QGroupBox::initStyleOption(QStyleOptionGroupBox *option) const
61{
62 if (!option)
63 return;
64
65 Q_D(const QGroupBox);
66 option->initFrom(w: this);
67 option->text = d->title;
68 option->lineWidth = 1;
69 option->midLineWidth = 0;
70 option->textAlignment = Qt::Alignment(d->align);
71 option->activeSubControls |= d->pressedControl;
72 option->subControls = QStyle::SC_GroupBoxFrame;
73
74 option->state.setFlag(flag: QStyle::State_MouseOver, on: d->hover);
75 if (d->flat)
76 option->features |= QStyleOptionFrame::Flat;
77
78 if (d->checkable) {
79 option->subControls |= QStyle::SC_GroupBoxCheckBox;
80 option->state |= (d->checked ? QStyle::State_On : QStyle::State_Off);
81 if ((d->pressedControl == QStyle::SC_GroupBoxCheckBox
82 || d->pressedControl == QStyle::SC_GroupBoxLabel) && (d->hover || d->overCheckBox))
83 option->state |= QStyle::State_Sunken;
84 }
85
86 if (!option->palette.isBrushSet(cg: isEnabled() ? QPalette::Active :
87 QPalette::Disabled, cr: QPalette::WindowText))
88 option->textColor = QColor(style()->styleHint(stylehint: QStyle::SH_GroupBox_TextLabelColor,
89 opt: option, widget: this));
90
91 if (!d->title.isEmpty())
92 option->subControls |= QStyle::SC_GroupBoxLabel;
93}
94
95void QGroupBoxPrivate::click()
96{
97 Q_Q(QGroupBox);
98
99 QPointer<QGroupBox> guard(q);
100 q->setChecked(!checked);
101 if (!guard)
102 return;
103 emit q->clicked(checked);
104}
105
106/*!
107 \class QGroupBox
108 \brief The QGroupBox widget provides a group box frame with a title.
109
110 \ingroup organizers
111 \ingroup geomanagement
112 \inmodule QtWidgets
113
114 \image windows-groupbox.png
115
116 A group box provides a frame, a title on top, a keyboard shortcut, and
117 displays various other widgets inside itself. The keyboard shortcut moves
118 keyboard focus to one of the group box's child widgets.
119
120 QGroupBox also lets you set the \l title (normally set in the
121 constructor) and the title's \l alignment. Group boxes can be
122 \l checkable. Child widgets in checkable group boxes are enabled or
123 disabled depending on whether or not the group box is \l checked.
124
125 You can minimize the space consumption of a group box by enabling
126 the \l flat property. In most \l{QStyle}{styles}, enabling this
127 property results in the removal of the left, right and bottom
128 edges of the frame.
129
130 QGroupBox doesn't automatically lay out the child widgets (which
131 are often \l{QCheckBox}es or \l{QRadioButton}s but can be any
132 widgets). The following example shows how we can set up a
133 QGroupBox with a layout:
134
135 \snippet widgets/groupbox/window.cpp 2
136
137 \sa QButtonGroup, {Group Box Example}
138*/
139
140
141
142/*!
143 Constructs a group box widget with the given \a parent but with no title.
144*/
145
146QGroupBox::QGroupBox(QWidget *parent)
147 : QWidget(*new QGroupBoxPrivate, parent, { })
148{
149 Q_D(QGroupBox);
150 d->init();
151}
152
153/*!
154 Constructs a group box with the given \a title and \a parent.
155*/
156
157QGroupBox::QGroupBox(const QString &title, QWidget *parent)
158 : QGroupBox(parent)
159{
160 setTitle(title);
161}
162
163
164/*!
165 Destroys the group box.
166*/
167QGroupBox::~QGroupBox()
168{
169}
170
171void QGroupBoxPrivate::init()
172{
173 Q_Q(QGroupBox);
174 align = Qt::AlignLeft;
175#ifndef QT_NO_SHORTCUT
176 shortcutId = 0;
177#endif
178 flat = false;
179 checkable = false;
180 checked = true;
181 hover = false;
182 overCheckBox = false;
183 pressedControl = QStyle::SC_None;
184 calculateFrame();
185 q->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred,
186 QSizePolicy::GroupBox));
187}
188
189void QGroupBox::setTitle(const QString &title)
190{
191 Q_D(QGroupBox);
192 if (d->title == title) // no change
193 return;
194 d->title = title;
195#ifndef QT_NO_SHORTCUT
196 releaseShortcut(id: d->shortcutId);
197 d->shortcutId = grabShortcut(key: QKeySequence::mnemonic(text: title));
198#endif
199 d->calculateFrame();
200
201 update();
202 updateGeometry();
203#if QT_CONFIG(accessibility)
204 QAccessibleEvent event(this, QAccessible::NameChanged);
205 QAccessible::updateAccessibility(event: &event);
206#endif
207}
208
209/*!
210 \property QGroupBox::title
211 \brief the group box title text
212
213 The group box title text will have a keyboard shortcut if the title
214 contains an ampersand ('&') followed by a letter.
215
216 \snippet code/src_gui_widgets_qgroupbox.cpp 0
217
218 In the example above, \uicontrol Alt+U moves the keyboard focus to the
219 group box. See the \l {QShortcut#mnemonic}{QShortcut}
220 documentation for details (to display an actual ampersand, use
221 '&&').
222
223 There is no default title text.
224
225 \sa alignment
226*/
227
228QString QGroupBox::title() const
229{
230 Q_D(const QGroupBox);
231 return d->title;
232}
233
234/*!
235 \property QGroupBox::alignment
236 \brief the alignment of the group box title.
237
238 Most styles place the title at the top of the frame. The horizontal
239 alignment of the title can be specified using single values from
240 the following list:
241
242 \list
243 \li Qt::AlignLeft aligns the title text with the left-hand side of the group box.
244 \li Qt::AlignRight aligns the title text with the right-hand side of the group box.
245 \li Qt::AlignHCenter aligns the title text with the horizontal center of the group box.
246 \endlist
247
248 The default alignment is Qt::AlignLeft.
249
250 \sa Qt::Alignment
251*/
252Qt::Alignment QGroupBox::alignment() const
253{
254 Q_D(const QGroupBox);
255 return QFlag(d->align);
256}
257
258void QGroupBox::setAlignment(int alignment)
259{
260 Q_D(QGroupBox);
261 d->align = alignment;
262 updateGeometry();
263 update();
264}
265
266/*! \reimp
267*/
268void QGroupBox::resizeEvent(QResizeEvent *e)
269{
270 QWidget::resizeEvent(event: e);
271}
272
273/*! \reimp
274*/
275
276void QGroupBox::paintEvent(QPaintEvent *)
277{
278 QStylePainter paint(this);
279 QStyleOptionGroupBox option;
280 initStyleOption(option: &option);
281 paint.drawComplexControl(cc: QStyle::CC_GroupBox, opt: option);
282}
283
284/*! \reimp */
285bool QGroupBox::event(QEvent *e)
286{
287 Q_D(QGroupBox);
288#ifndef QT_NO_SHORTCUT
289 if (e->type() == QEvent::Shortcut) {
290 QShortcutEvent *se = static_cast<QShortcutEvent *>(e);
291 if (se->shortcutId() == d->shortcutId) {
292 if (!isCheckable()) {
293 d->_q_fixFocus(reason: Qt::ShortcutFocusReason);
294 } else {
295 d->click();
296 setFocus(Qt::ShortcutFocusReason);
297 }
298 return true;
299 }
300 }
301#endif
302 QStyleOptionGroupBox box;
303 initStyleOption(option: &box);
304 switch (e->type()) {
305 case QEvent::HoverEnter:
306 case QEvent::HoverMove: {
307 QStyle::SubControl control = style()->hitTestComplexControl(cc: QStyle::CC_GroupBox, opt: &box,
308 pt: static_cast<QHoverEvent *>(e)->position().toPoint(),
309 widget: this);
310 bool oldHover = d->hover;
311 d->hover = d->checkable && (control == QStyle::SC_GroupBoxLabel || control == QStyle::SC_GroupBoxCheckBox);
312 if (oldHover != d->hover) {
313 QRect rect = style()->subControlRect(cc: QStyle::CC_GroupBox, opt: &box, sc: QStyle::SC_GroupBoxCheckBox, widget: this)
314 | style()->subControlRect(cc: QStyle::CC_GroupBox, opt: &box, sc: QStyle::SC_GroupBoxLabel, widget: this);
315 update(rect);
316 }
317 return true;
318 }
319 case QEvent::HoverLeave:
320 d->hover = false;
321 if (d->checkable) {
322 QRect rect = style()->subControlRect(cc: QStyle::CC_GroupBox, opt: &box, sc: QStyle::SC_GroupBoxCheckBox, widget: this)
323 | style()->subControlRect(cc: QStyle::CC_GroupBox, opt: &box, sc: QStyle::SC_GroupBoxLabel, widget: this);
324 update(rect);
325 }
326 return true;
327 case QEvent::KeyPress: {
328 QKeyEvent *k = static_cast<QKeyEvent*>(e);
329 const auto buttonPressKeys = QGuiApplicationPrivate::platformTheme()
330 ->themeHint(hint: QPlatformTheme::ButtonPressKeys)
331 .value<QList<Qt::Key>>();
332 if (!k->isAutoRepeat() && buttonPressKeys.contains(t: k->key())) {
333 d->pressedControl = QStyle::SC_GroupBoxCheckBox;
334 update(style()->subControlRect(cc: QStyle::CC_GroupBox, opt: &box, sc: QStyle::SC_GroupBoxCheckBox, widget: this));
335 return true;
336 }
337 break;
338 }
339 case QEvent::KeyRelease: {
340 QKeyEvent *k = static_cast<QKeyEvent*>(e);
341 const auto buttonPressKeys = QGuiApplicationPrivate::platformTheme()
342 ->themeHint(hint: QPlatformTheme::ButtonPressKeys)
343 .value<QList<Qt::Key>>();
344 if (!k->isAutoRepeat() && buttonPressKeys.contains(t: k->key())) {
345 bool toggle = (d->pressedControl == QStyle::SC_GroupBoxLabel
346 || d->pressedControl == QStyle::SC_GroupBoxCheckBox);
347 d->pressedControl = QStyle::SC_None;
348 if (toggle)
349 d->click();
350 return true;
351 }
352 break;
353 }
354 default:
355 break;
356 }
357 return QWidget::event(event: e);
358}
359
360/*!\reimp */
361void QGroupBox::childEvent(QChildEvent *c)
362{
363 Q_D(QGroupBox);
364 /*
365 Children might have been enabled after being added to the group box, in which case
366 the childEvent handler ran too early, and we need to disabled children again.
367 */
368 if (!(c->added() || c->polished()) || !c->child()->isWidgetType())
369 return;
370 QWidget *w = static_cast<QWidget*>(c->child());
371 if (w->isWindow())
372 return;
373 if (d->checkable) {
374 if (d->checked) {
375 if (!w->testAttribute(attribute: Qt::WA_ForceDisabled))
376 w->setEnabled(true);
377 } else {
378 if (w->isEnabled()) {
379 w->setEnabled(false);
380 w->setAttribute(Qt::WA_ForceDisabled, on: false);
381 }
382 }
383 }
384}
385
386
387/*!
388 \internal
389
390 This private slot finds a widget in this group box that can accept
391 focus, and gives the focus to that widget.
392*/
393
394void QGroupBoxPrivate::_q_fixFocus(Qt::FocusReason reason)
395{
396 Q_Q(QGroupBox);
397 QWidget *fw = q->focusWidget();
398 if (!fw || fw == q) {
399 QWidget * best = nullptr;
400 QWidget * candidate = nullptr;
401 QWidget * w = q;
402 while ((w = w->nextInFocusChain()) != q) {
403 if (q->isAncestorOf(child: w) && (w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus && w->isVisibleTo(q)) {
404#if QT_CONFIG(radiobutton)
405 if (!best && qobject_cast<QRadioButton*>(object: w) && ((QRadioButton*)w)->isChecked())
406 // we prefer a checked radio button or a widget that
407 // already has focus, if there is one
408 best = w;
409 else
410#endif
411 if (!candidate)
412 // but we'll accept anything that takes focus
413 candidate = w;
414 }
415 }
416 if (best)
417 fw = best;
418 else if (candidate)
419 fw = candidate;
420 }
421 if (fw)
422 fw->setFocus(reason);
423}
424
425/*
426 Sets the right frame rect depending on the title.
427*/
428void QGroupBoxPrivate::calculateFrame()
429{
430 Q_Q(QGroupBox);
431 QStyleOptionGroupBox box;
432 q->initStyleOption(option: &box);
433 QRect contentsRect = q->style()->subControlRect(cc: QStyle::CC_GroupBox, opt: &box, sc: QStyle::SC_GroupBoxContents, widget: q);
434 q->setContentsMargins(left: contentsRect.left() - box.rect.left(), top: contentsRect.top() - box.rect.top(),
435 right: box.rect.right() - contentsRect.right(), bottom: box.rect.bottom() - contentsRect.bottom());
436 setLayoutItemMargins(element: QStyle::SE_GroupBoxLayoutItem, opt: &box);
437}
438
439/*! \reimp
440 */
441void QGroupBox::focusInEvent(QFocusEvent *fe)
442{ // note no call to super
443 Q_D(QGroupBox);
444 if (focusPolicy() == Qt::NoFocus) {
445 d->_q_fixFocus(reason: fe->reason());
446 } else {
447 QWidget::focusInEvent(event: fe);
448 }
449}
450
451
452/*!
453 \reimp
454*/
455QSize QGroupBox::minimumSizeHint() const
456{
457 Q_D(const QGroupBox);
458 QStyleOptionGroupBox option;
459 initStyleOption(option: &option);
460
461 QFontMetrics metrics(fontMetrics());
462
463 int baseWidth = metrics.horizontalAdvance(d->title) + metrics.horizontalAdvance(u' ');
464 int baseHeight = metrics.height();
465 if (d->checkable) {
466 baseWidth += style()->pixelMetric(metric: QStyle::PM_IndicatorWidth, option: &option);
467 baseWidth += style()->pixelMetric(metric: QStyle::PM_CheckBoxLabelSpacing, option: &option);
468 baseHeight = qMax(a: baseHeight, b: style()->pixelMetric(metric: QStyle::PM_IndicatorHeight, option: &option));
469 }
470
471 QSize size = style()->sizeFromContents(ct: QStyle::CT_GroupBox, opt: &option, contentsSize: QSize(baseWidth, baseHeight), w: this);
472 return size.expandedTo(otherSize: QWidget::minimumSizeHint());
473}
474
475/*!
476 \property QGroupBox::flat
477 \brief whether the group box is painted flat or has a frame
478
479 A group box usually consists of a surrounding frame with a title
480 at the top. If this property is enabled, only the top part of the frame is
481 drawn in most styles; otherwise, the whole frame is drawn.
482
483 By default, this property is disabled, i.e., group boxes are not flat unless
484 explicitly specified.
485
486 \b{Note:} In some styles, flat and non-flat group boxes have similar
487 representations and may not be as distinguishable as they are in other
488 styles.
489
490 \sa title
491*/
492bool QGroupBox::isFlat() const
493{
494 Q_D(const QGroupBox);
495 return d->flat;
496}
497
498void QGroupBox::setFlat(bool b)
499{
500 Q_D(QGroupBox);
501 if (d->flat == b)
502 return;
503 d->flat = b;
504 updateGeometry();
505 update();
506}
507
508
509/*!
510 \property QGroupBox::checkable
511 \brief whether the group box has a checkbox in its title
512
513 If this property is \c true, the group box displays its title using
514 a checkbox in place of an ordinary label. If the checkbox is checked,
515 the group box's children are enabled; otherwise, they are disabled and
516 inaccessible.
517
518 By default, group boxes are not checkable.
519
520 If this property is enabled for a group box, it will also be initially
521 checked to ensure that its contents are enabled.
522
523 \sa checked
524*/
525void QGroupBox::setCheckable(bool checkable)
526{
527 Q_D(QGroupBox);
528
529 bool wasCheckable = d->checkable;
530 d->checkable = checkable;
531
532 if (checkable) {
533 setChecked(true);
534 if (!wasCheckable) {
535 setFocusPolicy(Qt::StrongFocus);
536 d->_q_setChildrenEnabled(b: true);
537 updateGeometry();
538 }
539 } else {
540 if (wasCheckable) {
541 setFocusPolicy(Qt::NoFocus);
542 d->_q_setChildrenEnabled(b: true);
543 updateGeometry();
544 }
545 d->_q_setChildrenEnabled(b: true);
546 }
547
548 if (wasCheckable != checkable) {
549 d->calculateFrame();
550 update();
551 }
552}
553
554bool QGroupBox::isCheckable() const
555{
556 Q_D(const QGroupBox);
557 return d->checkable;
558}
559
560
561bool QGroupBox::isChecked() const
562{
563 Q_D(const QGroupBox);
564 return d->checkable && d->checked;
565}
566
567
568/*!
569 \fn void QGroupBox::toggled(bool on)
570
571 If the group box is checkable, this signal is emitted when the check box
572 is toggled. \a on is true if the check box is checked; otherwise, it is false.
573
574 \sa checkable
575*/
576
577
578/*!
579 \fn void QGroupBox::clicked(bool checked)
580 \since 4.2
581
582 This signal is emitted when the check box is activated (i.e., pressed down
583 then released while the mouse cursor is inside the button), or when the
584 shortcut key is typed. Notably, this signal is \e not emitted if you call
585 setChecked().
586
587 If the check box is checked, \a checked is true; it is false if the check
588 box is unchecked.
589
590 \sa checkable, toggled(), checked
591*/
592
593/*!
594 \property QGroupBox::checked
595 \brief whether the group box is checked
596
597 If the group box is checkable, it is displayed with a check box.
598 If the check box is checked, the group box's children are enabled;
599 otherwise, the children are disabled and are inaccessible to the user.
600
601 By default, checkable group boxes are also checked.
602
603 \sa checkable
604*/
605void QGroupBox::setChecked(bool b)
606{
607 Q_D(QGroupBox);
608 if (d->checkable && b != d->checked) {
609 update();
610 d->checked = b;
611 d->_q_setChildrenEnabled(b);
612#if QT_CONFIG(accessibility)
613 QAccessible::State st;
614 st.checked = true;
615 QAccessibleStateChangeEvent e(this, st);
616 QAccessible::updateAccessibility(event: &e);
617#endif
618 emit toggled(b);
619 }
620}
621
622/*
623 sets all children of the group box except the qt_groupbox_checkbox
624 to either disabled/enabled
625*/
626void QGroupBoxPrivate::_q_setChildrenEnabled(bool b)
627{
628 Q_Q(QGroupBox);
629 for (QObject *o : q->children()) {
630 if (o->isWidgetType()) {
631 QWidget *w = static_cast<QWidget *>(o);
632 if (b) {
633 if (!w->testAttribute(attribute: Qt::WA_ForceDisabled))
634 w->setEnabled(true);
635 } else {
636 if (w->isEnabled()) {
637 w->setEnabled(false);
638 w->setAttribute(Qt::WA_ForceDisabled, on: false);
639 }
640 }
641 }
642 }
643}
644
645/*! \reimp */
646void QGroupBox::changeEvent(QEvent *ev)
647{
648 Q_D(QGroupBox);
649 if (ev->type() == QEvent::EnabledChange) {
650 if (d->checkable && isEnabled()) {
651 // we are being enabled - disable children
652 if (!d->checked)
653 d->_q_setChildrenEnabled(b: false);
654 }
655 } else if (ev->type() == QEvent::FontChange
656#ifdef Q_OS_MAC
657 || ev->type() == QEvent::MacSizeChange
658#endif
659 || ev->type() == QEvent::StyleChange) {
660 d->calculateFrame();
661 }
662 QWidget::changeEvent(ev);
663}
664
665/*! \reimp */
666void QGroupBox::mousePressEvent(QMouseEvent *event)
667{
668 if (event->button() != Qt::LeftButton) {
669 event->ignore();
670 return;
671 }
672
673 Q_D(QGroupBox);
674 QStyleOptionGroupBox box;
675 initStyleOption(option: &box);
676 d->pressedControl = style()->hitTestComplexControl(cc: QStyle::CC_GroupBox, opt: &box,
677 pt: event->position().toPoint(), widget: this);
678 if (d->checkable && (d->pressedControl & (QStyle::SC_GroupBoxCheckBox | QStyle::SC_GroupBoxLabel))) {
679 d->overCheckBox = true;
680 update(style()->subControlRect(cc: QStyle::CC_GroupBox, opt: &box, sc: QStyle::SC_GroupBoxCheckBox, widget: this));
681 } else {
682 event->ignore();
683 }
684}
685
686/*! \reimp */
687void QGroupBox::mouseMoveEvent(QMouseEvent *event)
688{
689 Q_D(QGroupBox);
690 QStyleOptionGroupBox box;
691 initStyleOption(option: &box);
692 QStyle::SubControl pressed = style()->hitTestComplexControl(cc: QStyle::CC_GroupBox, opt: &box,
693 pt: event->position().toPoint(), widget: this);
694 bool oldOverCheckBox = d->overCheckBox;
695 d->overCheckBox = (pressed == QStyle::SC_GroupBoxCheckBox || pressed == QStyle::SC_GroupBoxLabel);
696 if (d->checkable && (d->pressedControl == QStyle::SC_GroupBoxCheckBox || d->pressedControl == QStyle::SC_GroupBoxLabel)
697 && (d->overCheckBox != oldOverCheckBox))
698 update(style()->subControlRect(cc: QStyle::CC_GroupBox, opt: &box, sc: QStyle::SC_GroupBoxCheckBox, widget: this));
699
700 event->ignore();
701}
702
703/*! \reimp */
704void QGroupBox::mouseReleaseEvent(QMouseEvent *event)
705{
706 if (event->button() != Qt::LeftButton) {
707 event->ignore();
708 return;
709 }
710
711 Q_D(QGroupBox);
712 if (!d->overCheckBox) {
713 event->ignore();
714 return;
715 }
716 QStyleOptionGroupBox box;
717 initStyleOption(option: &box);
718 QStyle::SubControl released = style()->hitTestComplexControl(cc: QStyle::CC_GroupBox, opt: &box,
719 pt: event->position().toPoint(), widget: this);
720 bool toggle = d->checkable && (released == QStyle::SC_GroupBoxLabel
721 || released == QStyle::SC_GroupBoxCheckBox);
722 d->pressedControl = QStyle::SC_None;
723 d->overCheckBox = false;
724 if (toggle)
725 d->click();
726 else if (d->checkable)
727 update(style()->subControlRect(cc: QStyle::CC_GroupBox, opt: &box, sc: QStyle::SC_GroupBoxCheckBox, widget: this));
728}
729
730QT_END_NAMESPACE
731
732#include "moc_qgroupbox.cpp"
733

source code of qtbase/src/widgets/widgets/qgroupbox.cpp