1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2015 David Edmundson <davidedmundson@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "kcollapsiblegroupbox.h"
9
10#include <QLabel>
11#include <QLayout>
12#include <QMouseEvent>
13#include <QPainter>
14#include <QStyle>
15#include <QStyleOption>
16#include <QTimeLine>
17
18class KCollapsibleGroupBoxPrivate
19{
20public:
21 KCollapsibleGroupBoxPrivate(KCollapsibleGroupBox *qq);
22 void updateChildrenFocus(bool expanded);
23 void recalculateHeaderSize();
24 QSize contentSize() const;
25 QSize contentMinimumSize() const;
26
27 KCollapsibleGroupBox *const q;
28 QTimeLine *animation;
29 QString title;
30 bool isExpanded = false;
31 bool headerContainsMouse = false;
32 QSize headerSize;
33 int shortcutId = 0;
34 QMap<QWidget *, Qt::FocusPolicy> focusMap; // Used to restore focus policy of widgets.
35};
36
37KCollapsibleGroupBoxPrivate::KCollapsibleGroupBoxPrivate(KCollapsibleGroupBox *qq)
38 : q(qq)
39{
40}
41
42KCollapsibleGroupBox::KCollapsibleGroupBox(QWidget *parent)
43 : QWidget(parent)
44 , d(new KCollapsibleGroupBoxPrivate(this))
45{
46 d->recalculateHeaderSize();
47
48 d->animation = new QTimeLine(500, this); // duration matches kmessagewidget
49 connect(sender: d->animation, signal: &QTimeLine::valueChanged, context: this, slot: [this](qreal value) {
50 setFixedHeight((d->contentSize().height() * value) + d->headerSize.height());
51 });
52 connect(sender: d->animation, signal: &QTimeLine::stateChanged, context: this, slot: [this](QTimeLine::State state) {
53 if (state == QTimeLine::NotRunning) {
54 d->updateChildrenFocus(expanded: d->isExpanded);
55 }
56 });
57
58 setSizePolicy(hor: QSizePolicy::MinimumExpanding, ver: QSizePolicy::Fixed);
59 setFocusPolicy(Qt::TabFocus);
60 setMouseTracking(true);
61}
62
63KCollapsibleGroupBox::~KCollapsibleGroupBox()
64{
65 if (d->animation->state() == QTimeLine::Running) {
66 d->animation->stop();
67 }
68}
69
70void KCollapsibleGroupBox::setTitle(const QString &title)
71{
72 d->title = title;
73 d->recalculateHeaderSize();
74
75 update();
76 updateGeometry();
77
78 if (d->shortcutId) {
79 releaseShortcut(id: d->shortcutId);
80 }
81
82 d->shortcutId = grabShortcut(key: QKeySequence::mnemonic(text: title));
83
84#ifndef QT_NO_ACCESSIBILITY
85 setAccessibleName(title);
86#endif
87
88 Q_EMIT titleChanged();
89}
90
91QString KCollapsibleGroupBox::title() const
92{
93 return d->title;
94}
95
96void KCollapsibleGroupBox::setExpanded(bool expanded)
97{
98 if (expanded == d->isExpanded) {
99 return;
100 }
101
102 d->isExpanded = expanded;
103 Q_EMIT expandedChanged();
104
105 d->updateChildrenFocus(expanded);
106
107 d->animation->setDirection(expanded ? QTimeLine::Forward : QTimeLine::Backward);
108 // QTimeLine::duration() must be > 0
109 const int duration = qMax(a: 1, b: style()->styleHint(stylehint: QStyle::SH_Widget_Animation_Duration));
110 d->animation->stop();
111 d->animation->setDuration(duration);
112 d->animation->start();
113
114 // when going from collapsed to expanded changing the child visibility calls an updateGeometry
115 // which calls sizeHint with expanded true before the first frame of the animation kicks in
116 // trigger an effective frame 0
117 if (expanded) {
118 setFixedHeight(d->headerSize.height());
119 }
120}
121
122bool KCollapsibleGroupBox::isExpanded() const
123{
124 return d->isExpanded;
125}
126
127void KCollapsibleGroupBox::collapse()
128{
129 setExpanded(false);
130}
131
132void KCollapsibleGroupBox::expand()
133{
134 setExpanded(true);
135}
136
137void KCollapsibleGroupBox::toggle()
138{
139 setExpanded(!d->isExpanded);
140}
141
142void KCollapsibleGroupBox::paintEvent(QPaintEvent *event)
143{
144 QPainter p(this);
145
146 QStyleOptionButton baseOption;
147 baseOption.initFrom(w: this);
148 baseOption.rect = QRect(0, 0, width(), d->headerSize.height());
149 baseOption.text = d->title;
150
151 if (d->headerContainsMouse) {
152 baseOption.state |= QStyle::State_MouseOver;
153 }
154
155 QStyle::PrimitiveElement element;
156 if (d->isExpanded) {
157 element = QStyle::PE_IndicatorArrowDown;
158 } else {
159 element = isLeftToRight() ? QStyle::PE_IndicatorArrowRight : QStyle::PE_IndicatorArrowLeft;
160 }
161
162 QStyleOptionButton indicatorOption = baseOption;
163 indicatorOption.rect = style()->subElementRect(subElement: QStyle::SE_CheckBoxIndicator, option: &indicatorOption, widget: this);
164 style()->drawPrimitive(pe: element, opt: &indicatorOption, p: &p, w: this);
165
166 QStyleOptionButton labelOption = baseOption;
167 labelOption.rect = style()->subElementRect(subElement: QStyle::SE_CheckBoxContents, option: &labelOption, widget: this);
168 style()->drawControl(element: QStyle::CE_CheckBoxLabel, opt: &labelOption, p: &p, w: this);
169
170 Q_UNUSED(event)
171}
172
173QSize KCollapsibleGroupBox::sizeHint() const
174{
175 if (d->isExpanded) {
176 return d->contentSize() + QSize(0, d->headerSize.height());
177 } else {
178 return QSize(d->contentSize().width(), d->headerSize.height());
179 }
180}
181
182QSize KCollapsibleGroupBox::minimumSizeHint() const
183{
184 int minimumWidth = qMax(a: d->contentSize().width(), b: d->headerSize.width());
185 return QSize(minimumWidth, d->headerSize.height());
186}
187
188bool KCollapsibleGroupBox::event(QEvent *event)
189{
190 switch (event->type()) {
191 case QEvent::StyleChange:
192 /*fall through*/
193 case QEvent::FontChange:
194 d->recalculateHeaderSize();
195 break;
196 case QEvent::Shortcut: {
197 QShortcutEvent *se = static_cast<QShortcutEvent *>(event);
198 if (d->shortcutId == se->shortcutId()) {
199 toggle();
200 return true;
201 }
202 break;
203 }
204 case QEvent::ChildAdded: {
205 QChildEvent *ce = static_cast<QChildEvent *>(event);
206 if (ce->child()->isWidgetType()) {
207 auto widget = static_cast<QWidget *>(ce->child());
208 // Needs to be called asynchronously because at this point the widget is likely a "real" QWidget,
209 // i.e. the QWidget base class whose constructor sets the focus policy to NoPolicy.
210 // But the constructor of the child class (not yet called) could set a different focus policy later.
211 auto focusFunc = [this, widget]() {
212 overrideFocusPolicyOf(widget);
213 };
214 QMetaObject::invokeMethod(object: this, function&: focusFunc, type: Qt::QueuedConnection);
215 }
216 break;
217 }
218 case QEvent::LayoutRequest:
219 if (d->animation->state() == QTimeLine::NotRunning) {
220 setFixedHeight(sizeHint().height());
221 }
222 break;
223 default:
224 break;
225 }
226
227 return QWidget::event(event);
228}
229
230void KCollapsibleGroupBox::mousePressEvent(QMouseEvent *event)
231{
232 const QRect headerRect(0, 0, width(), d->headerSize.height());
233 if (headerRect.contains(p: event->pos())) {
234 toggle();
235 }
236 event->setAccepted(true);
237}
238
239// if mouse has changed whether it is in the top bar or not refresh to change arrow icon
240void KCollapsibleGroupBox::mouseMoveEvent(QMouseEvent *event)
241{
242 const QRect headerRect(0, 0, width(), d->headerSize.height());
243 bool headerContainsMouse = headerRect.contains(p: event->pos());
244
245 if (headerContainsMouse != d->headerContainsMouse) {
246 d->headerContainsMouse = headerContainsMouse;
247 update();
248 }
249
250 QWidget::mouseMoveEvent(event);
251}
252
253void KCollapsibleGroupBox::leaveEvent(QEvent *event)
254{
255 d->headerContainsMouse = false;
256 update();
257 QWidget::leaveEvent(event);
258}
259
260void KCollapsibleGroupBox::keyPressEvent(QKeyEvent *event)
261{
262 // event might have just propagated up from a child, if so we don't want to react to it
263 if (!hasFocus()) {
264 return;
265 }
266 const int key = event->key();
267 if (key == Qt::Key_Space || key == Qt::Key_Enter || key == Qt::Key_Return) {
268 toggle();
269 event->setAccepted(true);
270 }
271}
272
273void KCollapsibleGroupBox::resizeEvent(QResizeEvent *event)
274{
275 const QMargins margins = contentsMargins();
276
277 if (layout()) {
278 // we don't want the layout trying to fit the current frame of the animation so always set it to the target height
279 layout()->setGeometry(QRect(margins.left(), margins.top(), width() - margins.left() - margins.right(), layout()->sizeHint().height()));
280 }
281
282 QWidget::resizeEvent(event);
283}
284
285void KCollapsibleGroupBox::overrideFocusPolicyOf(QWidget *widget)
286{
287 d->focusMap.insert(key: widget, value: widget->focusPolicy());
288
289 if (!isExpanded()) {
290 // Prevent tab focus if not expanded.
291 widget->setFocusPolicy(Qt::NoFocus);
292 }
293}
294
295void KCollapsibleGroupBoxPrivate::recalculateHeaderSize()
296{
297 QStyleOption option;
298 option.initFrom(w: q);
299
300 QSize textSize = q->style()->itemTextRect(fm: option.fontMetrics, r: QRect(), flags: Qt::TextShowMnemonic, enabled: false, text: title).size();
301
302 headerSize = q->style()->sizeFromContents(ct: QStyle::CT_CheckBox, opt: &option, contentsSize: textSize, w: q);
303 q->setContentsMargins(left: q->style()->pixelMetric(metric: QStyle::PM_IndicatorWidth), top: headerSize.height(), right: 0, bottom: 0);
304}
305
306void KCollapsibleGroupBoxPrivate::updateChildrenFocus(bool expanded)
307{
308 const auto children = q->children();
309 for (QObject *child : children) {
310 QWidget *widget = qobject_cast<QWidget *>(o: child);
311 if (!widget) {
312 continue;
313 }
314 // Restore old focus policy if expanded, remove from focus chain otherwise.
315 if (expanded) {
316 widget->setFocusPolicy(focusMap.value(key: widget));
317 } else {
318 widget->setFocusPolicy(Qt::NoFocus);
319 }
320 }
321}
322
323QSize KCollapsibleGroupBoxPrivate::contentSize() const
324{
325 if (q->layout()) {
326 const QMargins margins = q->contentsMargins();
327 const QSize marginSize(margins.left() + margins.right(), margins.top() + margins.bottom());
328 return q->layout()->sizeHint() + marginSize;
329 }
330 return QSize(0, 0);
331}
332
333QSize KCollapsibleGroupBoxPrivate::contentMinimumSize() const
334{
335 if (q->layout()) {
336 const QMargins margins = q->contentsMargins();
337 const QSize marginSize(margins.left() + margins.right(), margins.top() + margins.bottom());
338 return q->layout()->minimumSize() + marginSize;
339 }
340 return QSize(0, 0);
341}
342
343#include "moc_kcollapsiblegroupbox.cpp"
344

source code of kwidgetsaddons/src/kcollapsiblegroupbox.cpp