1// Copyright (C) 2020 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 "qquickbuttongroup_p.h"
5
6#include <QtCore/private/qobject_p.h>
7#include <QtCore/qmetaobject.h>
8#include <QtCore/qvariant.h>
9#include <QtQml/qqmlinfo.h>
10
11#include "qquickabstractbutton_p_p.h"
12
13QT_BEGIN_NAMESPACE
14
15/*!
16 \qmltype ButtonGroup
17 \inherits QtObject
18//! \instantiates QQuickButtonGroup
19 \inqmlmodule QtQuick.Controls
20 \since 5.7
21 \ingroup utilities
22 \brief Mutually-exclusive group of checkable buttons.
23
24 ButtonGroup is a non-visual, mutually exclusive group of buttons.
25 It is used with controls such as RadioButton, where only one of the options
26 can be selected at a time.
27
28 The most straight-forward way to use ButtonGroup is to assign
29 a list of buttons. For example, the list of children of a
30 \l{Item Positioners}{positioner} or a \l{Qt Quick Layouts}{layout}
31 that manages a group of mutually exclusive buttons.
32
33 \code
34 ButtonGroup {
35 buttons: column.children
36 }
37
38 Column {
39 id: column
40
41 RadioButton {
42 checked: true
43 text: qsTr("DAB")
44 }
45
46 RadioButton {
47 text: qsTr("FM")
48 }
49
50 RadioButton {
51 text: qsTr("AM")
52 }
53 }
54 \endcode
55
56 Mutually exclusive buttons do not always share the same parent item,
57 or the parent layout may sometimes contain items that should not be
58 included in the button group. Such cases are best handled using
59 the \l group attached property.
60
61 \code
62 ButtonGroup { id: radioGroup }
63
64 Column {
65 Label {
66 text: qsTr("Radio:")
67 }
68
69 RadioButton {
70 checked: true
71 text: qsTr("DAB")
72 ButtonGroup.group: radioGroup
73 }
74
75 RadioButton {
76 text: qsTr("FM")
77 ButtonGroup.group: radioGroup
78 }
79
80 RadioButton {
81 text: qsTr("AM")
82 ButtonGroup.group: radioGroup
83 }
84 }
85 \endcode
86
87 More advanced use cases can be handled using the \c addButton() and
88 \c removeButton() methods.
89
90 \sa RadioButton, {Button Controls}
91*/
92
93/*!
94 \qmlsignal QtQuick.Controls::ButtonGroup::clicked(AbstractButton button)
95 \since QtQuick.Controls 2.1 (Qt 5.8)
96
97 This signal is emitted when a \a button in the group has been clicked.
98
99 This signal is convenient for implementing a common signal handler for
100 all buttons in the same group.
101
102 \code
103 ButtonGroup {
104 buttons: column.children
105 onClicked: console.log("clicked:", button.text)
106 }
107
108 Column {
109 id: column
110 Button { text: "First" }
111 Button { text: "Second" }
112 Button { text: "Third" }
113 }
114 \endcode
115
116 \sa AbstractButton::clicked()
117*/
118
119class QQuickButtonGroupPrivate : public QObjectPrivate
120{
121public:
122 Q_DECLARE_PUBLIC(QQuickButtonGroup)
123
124 void clear();
125 void buttonClicked();
126 void _q_updateCurrent();
127 void updateCheckState();
128 void setCheckState(Qt::CheckState state);
129
130 static void buttons_append(QQmlListProperty<QQuickAbstractButton> *prop, QQuickAbstractButton *obj);
131 static qsizetype buttons_count(QQmlListProperty<QQuickAbstractButton> *prop);
132 static QQuickAbstractButton *buttons_at(QQmlListProperty<QQuickAbstractButton> *prop, qsizetype index);
133 static void buttons_clear(QQmlListProperty<QQuickAbstractButton> *prop);
134
135 bool complete = true;
136 bool exclusive = true;
137 bool settingCheckState = false;
138 Qt::CheckState checkState = Qt::Unchecked;
139 QPointer<QQuickAbstractButton> checkedButton;
140 QList<QQuickAbstractButton*> buttons;
141};
142
143void QQuickButtonGroupPrivate::clear()
144{
145 for (QQuickAbstractButton *button : std::as_const(t&: buttons)) {
146 auto *attached = qobject_cast<QQuickButtonGroupAttached *>(
147 object: qmlAttachedPropertiesObject<QQuickButtonGroup>(obj: button, create: false));
148 if (attached) {
149 attached->setGroup(nullptr);
150 } else {
151 QQuickAbstractButtonPrivate::get(button)->group = nullptr;
152 QObjectPrivate::disconnect(sender: button, signal: &QQuickAbstractButton::clicked, receiverPrivate: this, slot: &QQuickButtonGroupPrivate::buttonClicked);
153 QObjectPrivate::disconnect(sender: button, signal: &QQuickAbstractButton::checkedChanged, receiverPrivate: this, slot: &QQuickButtonGroupPrivate::_q_updateCurrent);
154 }
155 }
156 buttons.clear();
157}
158
159void QQuickButtonGroupPrivate::buttonClicked()
160{
161 Q_Q(QQuickButtonGroup);
162 QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton*>(object: q->sender());
163 if (button)
164 emit q->clicked(button);
165}
166
167void QQuickButtonGroupPrivate::_q_updateCurrent()
168{
169 Q_Q(QQuickButtonGroup);
170 if (exclusive) {
171 QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton*>(object: q->sender());
172 if (button && button->isChecked())
173 q->setCheckedButton(button);
174 else if (!buttons.contains(t: checkedButton))
175 q->setCheckedButton(nullptr);
176 }
177 updateCheckState();
178}
179
180void QQuickButtonGroupPrivate::updateCheckState()
181{
182 if (!complete || settingCheckState)
183 return;
184
185 bool anyChecked = false;
186 bool allChecked = !buttons.isEmpty();
187 for (QQuickAbstractButton *button : std::as_const(t&: buttons)) {
188 const bool isChecked = button->isChecked();
189 anyChecked |= isChecked;
190 allChecked &= isChecked;
191 }
192 setCheckState(Qt::CheckState(anyChecked + allChecked));
193}
194
195void QQuickButtonGroupPrivate::setCheckState(Qt::CheckState state)
196{
197 Q_Q(QQuickButtonGroup);
198 if (checkState == state)
199 return;
200
201 checkState = state;
202 emit q->checkStateChanged();
203}
204
205void QQuickButtonGroupPrivate::buttons_append(QQmlListProperty<QQuickAbstractButton> *prop, QQuickAbstractButton *obj)
206{
207 QQuickButtonGroup *q = static_cast<QQuickButtonGroup *>(prop->object);
208 q->addButton(button: obj);
209}
210
211qsizetype QQuickButtonGroupPrivate::buttons_count(QQmlListProperty<QQuickAbstractButton> *prop)
212{
213 QQuickButtonGroupPrivate *p = static_cast<QQuickButtonGroupPrivate *>(prop->data);
214 return p->buttons.size();
215}
216
217QQuickAbstractButton *QQuickButtonGroupPrivate::buttons_at(QQmlListProperty<QQuickAbstractButton> *prop, qsizetype index)
218{
219 QQuickButtonGroupPrivate *p = static_cast<QQuickButtonGroupPrivate *>(prop->data);
220 return p->buttons.value(i: index);
221}
222
223void QQuickButtonGroupPrivate::buttons_clear(QQmlListProperty<QQuickAbstractButton> *prop)
224{
225 QQuickButtonGroupPrivate *p = static_cast<QQuickButtonGroupPrivate *>(prop->data);
226 if (!p->buttons.isEmpty()) {
227 p->clear();
228 QQuickButtonGroup *q = static_cast<QQuickButtonGroup *>(prop->object);
229 // QTBUG-52358: don't clear the checked button immediately
230 QMetaObject::invokeMethod(obj: q, member: "_q_updateCurrent", c: Qt::QueuedConnection);
231 emit q->buttonsChanged();
232 }
233}
234
235QQuickButtonGroup::QQuickButtonGroup(QObject *parent)
236 : QObject(*(new QQuickButtonGroupPrivate), parent)
237{
238}
239
240QQuickButtonGroup::~QQuickButtonGroup()
241{
242 Q_D(QQuickButtonGroup);
243 d->clear();
244}
245
246QQuickButtonGroupAttached *QQuickButtonGroup::qmlAttachedProperties(QObject *object)
247{
248 return new QQuickButtonGroupAttached(object);
249}
250
251/*!
252 \qmlproperty AbstractButton QtQuick.Controls::ButtonGroup::checkedButton
253
254 This property holds the currently selected button in an exclusive group,
255 or \c null if there is none or the group is non-exclusive.
256
257 By default, it is the first checked button added to an exclusive button group.
258
259 \sa exclusive
260*/
261QQuickAbstractButton *QQuickButtonGroup::checkedButton() const
262{
263 Q_D(const QQuickButtonGroup);
264 return d->checkedButton;
265}
266
267void QQuickButtonGroup::setCheckedButton(QQuickAbstractButton *checkedButton)
268{
269 Q_D(QQuickButtonGroup);
270 if (d->checkedButton == checkedButton)
271 return;
272
273 if (d->checkedButton)
274 d->checkedButton->setChecked(false);
275 d->checkedButton = checkedButton;
276 if (checkedButton)
277 checkedButton->setChecked(true);
278 emit checkedButtonChanged();
279}
280
281/*!
282 \qmlproperty list<AbstractButton> QtQuick.Controls::ButtonGroup::buttons
283
284 This property holds the list of buttons.
285
286 \code
287 ButtonGroup {
288 buttons: column.children
289 }
290
291 Column {
292 id: column
293
294 RadioButton {
295 checked: true
296 text: qsTr("Option A")
297 }
298
299 RadioButton {
300 text: qsTr("Option B")
301 }
302 }
303 \endcode
304
305 \sa group
306*/
307QQmlListProperty<QQuickAbstractButton> QQuickButtonGroup::buttons()
308{
309 Q_D(QQuickButtonGroup);
310 return QQmlListProperty<QQuickAbstractButton>(this, d,
311 QQuickButtonGroupPrivate::buttons_append,
312 QQuickButtonGroupPrivate::buttons_count,
313 QQuickButtonGroupPrivate::buttons_at,
314 QQuickButtonGroupPrivate::buttons_clear);
315}
316
317/*!
318 \since QtQuick.Controls 2.3 (Qt 5.10)
319 \qmlproperty bool QtQuick.Controls::ButtonGroup::exclusive
320
321 This property holds whether the button group is exclusive. The default value is \c true.
322
323 If this property is \c true, then only one button in the group can be checked at any given time.
324 The user can click on any button to check it, and that button will replace the existing one as
325 the checked button in the group.
326
327 In an exclusive group, the user cannot uncheck the currently checked button by clicking on it;
328 instead, another button in the group must be clicked to set the new checked button for that group.
329
330 In a non-exclusive group, checking and unchecking buttons does not affect the other buttons in
331 the group. Furthermore, the value of the \l checkedButton property is \c null.
332*/
333bool QQuickButtonGroup::isExclusive() const
334{
335 Q_D(const QQuickButtonGroup);
336 return d->exclusive;
337}
338
339void QQuickButtonGroup::setExclusive(bool exclusive)
340{
341 Q_D(QQuickButtonGroup);
342 if (d->exclusive == exclusive)
343 return;
344
345 d->exclusive = exclusive;
346 emit exclusiveChanged();
347}
348
349/*!
350 \since QtQuick.Controls 2.4 (Qt 5.11)
351 \qmlproperty enumeration QtQuick.Controls::ButtonGroup::checkState
352
353 This property holds the combined check state of the button group.
354
355 Available states:
356 \value Qt.Unchecked None of the buttons are checked.
357 \value Qt.PartiallyChecked Some of the buttons are checked.
358 \value Qt.Checked All of the buttons are checked.
359
360 Setting the check state of a non-exclusive button group to \c Qt.Unchecked
361 or \c Qt.Checked unchecks or checks all buttons in the group, respectively.
362 \c Qt.PartiallyChecked is ignored.
363
364 Setting the check state of an exclusive button group to \c Qt.Unchecked
365 unchecks the \l checkedButton. \c Qt.Checked and \c Qt.PartiallyChecked
366 are ignored.
367*/
368Qt::CheckState QQuickButtonGroup::checkState() const
369{
370 Q_D(const QQuickButtonGroup);
371 return d->checkState;
372}
373
374void QQuickButtonGroup::setCheckState(Qt::CheckState state)
375{
376 Q_D(QQuickButtonGroup);
377 if (d->checkState == state || state == Qt::PartiallyChecked)
378 return;
379
380 d->settingCheckState = true;
381 if (d->exclusive) {
382 if (d->checkedButton && state == Qt::Unchecked)
383 setCheckedButton(nullptr);
384 } else {
385 for (QQuickAbstractButton *button : std::as_const(t&: d->buttons))
386 button->setChecked(state == Qt::Checked);
387 }
388 d->settingCheckState = false;
389 d->setCheckState(state);
390}
391
392/*!
393 \qmlmethod void QtQuick.Controls::ButtonGroup::addButton(AbstractButton button)
394
395 Adds a \a button to the button group.
396
397 \note Manually adding objects to a button group is typically unnecessary.
398 The \l buttons property and the \l group attached property provide a
399 convenient and declarative syntax.
400
401 \sa buttons, group
402*/
403void QQuickButtonGroup::addButton(QQuickAbstractButton *button)
404{
405 Q_D(QQuickButtonGroup);
406 if (!button || d->buttons.contains(t: button))
407 return;
408
409 QQuickAbstractButtonPrivate::get(button)->group = this;
410 QObjectPrivate::connect(sender: button, signal: &QQuickAbstractButton::clicked, receiverPrivate: d, slot: &QQuickButtonGroupPrivate::buttonClicked);
411 QObjectPrivate::connect(sender: button, signal: &QQuickAbstractButton::checkedChanged, receiverPrivate: d, slot: &QQuickButtonGroupPrivate::_q_updateCurrent);
412
413 if (d->exclusive && button->isChecked())
414 setCheckedButton(button);
415
416 d->buttons.append(t: button);
417 d->updateCheckState();
418 emit buttonsChanged();
419}
420
421/*!
422 \qmlmethod void QtQuick.Controls::ButtonGroup::removeButton(AbstractButton button)
423
424 Removes a \a button from the button group.
425
426 \note Manually removing objects from a button group is typically unnecessary.
427 The \l buttons property and the \l group attached property provide a
428 convenient and declarative syntax.
429
430 \sa buttons, group
431*/
432void QQuickButtonGroup::removeButton(QQuickAbstractButton *button)
433{
434 Q_D(QQuickButtonGroup);
435 if (!button || !d->buttons.contains(t: button))
436 return;
437
438 QQuickAbstractButtonPrivate::get(button)->group = nullptr;
439 QObjectPrivate::disconnect(sender: button, signal: &QQuickAbstractButton::clicked, receiverPrivate: d, slot: &QQuickButtonGroupPrivate::buttonClicked);
440 QObjectPrivate::disconnect(sender: button, signal: &QQuickAbstractButton::checkedChanged, receiverPrivate: d, slot: &QQuickButtonGroupPrivate::_q_updateCurrent);
441
442 if (d->checkedButton == button)
443 setCheckedButton(nullptr);
444
445 d->buttons.removeOne(t: button);
446 d->updateCheckState();
447 emit buttonsChanged();
448}
449
450void QQuickButtonGroup::classBegin()
451{
452 Q_D(QQuickButtonGroup);
453 d->complete = false;
454}
455
456void QQuickButtonGroup::componentComplete()
457{
458 Q_D(QQuickButtonGroup);
459 d->complete = true;
460 if (!d->buttons.isEmpty())
461 d->updateCheckState();
462}
463
464class QQuickButtonGroupAttachedPrivate : public QObjectPrivate
465{
466public:
467 QQuickButtonGroup *group = nullptr;
468};
469
470QQuickButtonGroupAttached::QQuickButtonGroupAttached(QObject *parent)
471 : QObject(*(new QQuickButtonGroupAttachedPrivate), parent)
472{
473}
474
475/*!
476 \qmlattachedproperty ButtonGroup QtQuick.Controls::ButtonGroup::group
477
478 This property attaches a button to a button group.
479
480 \code
481 ButtonGroup { id: group }
482
483 RadioButton {
484 checked: true
485 text: qsTr("Option A")
486 ButtonGroup.group: group
487 }
488
489 RadioButton {
490 text: qsTr("Option B")
491 ButtonGroup.group: group
492 }
493 \endcode
494
495 \sa buttons
496*/
497QQuickButtonGroup *QQuickButtonGroupAttached::group() const
498{
499 Q_D(const QQuickButtonGroupAttached);
500 return d->group;
501}
502
503void QQuickButtonGroupAttached::setGroup(QQuickButtonGroup *group)
504{
505 Q_D(QQuickButtonGroupAttached);
506 if (d->group == group)
507 return;
508
509 auto *button = qobject_cast<QQuickAbstractButton *>(object: parent());
510 if (d->group)
511 d->group->removeButton(button);
512 d->group = group;
513 if (group)
514 group->addButton(button);
515 emit groupChanged();
516}
517
518QT_END_NAMESPACE
519
520#include "moc_qquickbuttongroup_p.cpp"
521

source code of qtdeclarative/src/quicktemplates/qquickbuttongroup.cpp