KWidgetsAddons

kcollapsiblegroupbox.cpp
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(d->animation, &QTimeLine::valueChanged, this, [this](qreal value) {
50 setFixedHeight((d->contentSize().height() * value) + d->headerSize.height());
51 });
52 connect(d->animation, &QTimeLine::stateChanged, this, [this](QTimeLine::State state) {
53 if (state == QTimeLine::NotRunning) {
54 d->updateChildrenFocus(d->isExpanded);
55 }
56 });
57
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
71{
72 d->title = title;
73 d->recalculateHeaderSize();
74
75 update();
77
78 if (d->shortcutId) {
79 releaseShortcut(d->shortcutId);
80 }
81
82 d->shortcutId = grabShortcut(QKeySequence::mnemonic(title));
83
84#ifndef QT_NO_ACCESSIBILITY
85 setAccessibleName(title);
86#endif
87
89}
90
91QString KCollapsibleGroupBox::title() const
92{
93 return d->title;
94}
95
97{
98 if (expanded == d->isExpanded) {
99 return;
100 }
101
102 d->isExpanded = expanded;
104
105 d->updateChildrenFocus(expanded);
106
107 // Only animate when expanding/collapsing while visible.
108 if (isVisible()) {
109 d->animation->setDirection(expanded ? QTimeLine::Forward : QTimeLine::Backward);
110 // QTimeLine::duration() must be > 0
111 const int duration = qMax(1, style()->styleHint(QStyle::SH_Widget_Animation_Duration));
112 d->animation->stop();
113 d->animation->setDuration(duration);
114 d->animation->start();
115
116 // when going from collapsed to expanded changing the child visibility calls an updateGeometry
117 // which calls sizeHint with expanded true before the first frame of the animation kicks in
118 // trigger an effective frame 0
119 if (expanded) {
120 setFixedHeight(d->headerSize.height());
121 }
122 } else {
123 setFixedHeight(sizeHint().height());
124 }
125}
126
128{
129 return d->isExpanded;
130}
131
133{
134 setExpanded(false);
135}
136
138{
139 setExpanded(true);
140}
141
143{
144 setExpanded(!d->isExpanded);
145}
146
147void KCollapsibleGroupBox::paintEvent(QPaintEvent *event)
148{
149 QPainter p(this);
150
151 QStyleOptionButton baseOption;
152 baseOption.initFrom(this);
153 baseOption.rect = QRect(0, 0, width(), d->headerSize.height());
154 baseOption.text = d->title;
155
156 if (d->headerContainsMouse) {
157 baseOption.state |= QStyle::State_MouseOver;
158 }
159
161 if (d->isExpanded) {
163 } else {
165 }
166
167 QStyleOptionButton indicatorOption = baseOption;
168 indicatorOption.rect = style()->subElementRect(QStyle::SE_CheckBoxIndicator, &indicatorOption, this);
169 style()->drawPrimitive(element, &indicatorOption, &p, this);
170
171 QStyleOptionButton labelOption = baseOption;
172 labelOption.rect = style()->subElementRect(QStyle::SE_CheckBoxContents, &labelOption, this);
173 style()->drawControl(QStyle::CE_CheckBoxLabel, &labelOption, &p, this);
174
175 Q_UNUSED(event)
176}
177
178QSize KCollapsibleGroupBox::sizeHint() const
179{
180 if (d->isExpanded) {
181 return d->contentSize() + QSize(0, d->headerSize.height());
182 } else {
183 return QSize(d->contentSize().width(), d->headerSize.height());
184 }
185}
186
187QSize KCollapsibleGroupBox::minimumSizeHint() const
188{
189 int minimumWidth = qMax(d->contentSize().width(), d->headerSize.width());
190 return QSize(minimumWidth, d->headerSize.height());
191}
192
193bool KCollapsibleGroupBox::event(QEvent *event)
194{
195 switch (event->type()) {
197 /*fall through*/
199 d->recalculateHeaderSize();
200 break;
201 case QEvent::Shortcut: {
202 QShortcutEvent *se = static_cast<QShortcutEvent *>(event);
203 if (d->shortcutId == se->shortcutId()) {
204 toggle();
205 return true;
206 }
207 break;
208 }
209 case QEvent::ChildAdded: {
210 QChildEvent *ce = static_cast<QChildEvent *>(event);
211 if (ce->child()->isWidgetType()) {
212 auto widget = static_cast<QWidget *>(ce->child());
213 // Needs to be called asynchronously because at this point the widget is likely a "real" QWidget,
214 // i.e. the QWidget base class whose constructor sets the focus policy to NoPolicy.
215 // But the constructor of the child class (not yet called) could set a different focus policy later.
216 auto focusFunc = [this, widget]() {
217 overrideFocusPolicyOf(widget);
218 };
220 }
221 break;
222 }
224 if (d->animation->state() == QTimeLine::NotRunning) {
225 setFixedHeight(sizeHint().height());
226 }
227 break;
228 default:
229 break;
230 }
231
232 return QWidget::event(event);
233}
234
235void KCollapsibleGroupBox::mousePressEvent(QMouseEvent *event)
236{
237 const QRect headerRect(0, 0, width(), d->headerSize.height());
238 if (headerRect.contains(event->pos())) {
239 toggle();
240 }
241 event->setAccepted(true);
242}
243
244// if mouse has changed whether it is in the top bar or not refresh to change arrow icon
245void KCollapsibleGroupBox::mouseMoveEvent(QMouseEvent *event)
246{
247 const QRect headerRect(0, 0, width(), d->headerSize.height());
248 bool headerContainsMouse = headerRect.contains(event->pos());
249
250 if (headerContainsMouse != d->headerContainsMouse) {
251 d->headerContainsMouse = headerContainsMouse;
252 update();
253 }
254
256}
257
258void KCollapsibleGroupBox::leaveEvent(QEvent *event)
259{
260 d->headerContainsMouse = false;
261 update();
262 QWidget::leaveEvent(event);
263}
264
265void KCollapsibleGroupBox::keyPressEvent(QKeyEvent *event)
266{
267 // event might have just propagated up from a child, if so we don't want to react to it
268 if (!hasFocus()) {
269 return;
270 }
271 const int key = event->key();
272 if (key == Qt::Key_Space || key == Qt::Key_Enter || key == Qt::Key_Return) {
273 toggle();
274 event->setAccepted(true);
275 }
276}
277
278void KCollapsibleGroupBox::resizeEvent(QResizeEvent *event)
279{
280 const QMargins margins = contentsMargins();
281
282 if (layout()) {
283 // we don't want the layout trying to fit the current frame of the animation so always set it to the target height
284 layout()->setGeometry(QRect(margins.left(), margins.top(), width() - margins.left() - margins.right(), layout()->sizeHint().height()));
285 }
286
288}
289
290void KCollapsibleGroupBox::overrideFocusPolicyOf(QWidget *widget)
291{
292 d->focusMap.insert(widget, widget->focusPolicy());
293
294 if (!isExpanded()) {
295 // Prevent tab focus if not expanded.
297 }
298}
299
300void KCollapsibleGroupBoxPrivate::recalculateHeaderSize()
301{
302 QStyleOption option;
303 option.initFrom(q);
304
305 QSize textSize = q->style()->itemTextRect(option.fontMetrics, QRect(), Qt::TextShowMnemonic, false, title).size();
306
307 headerSize = q->style()->sizeFromContents(QStyle::CT_CheckBox, &option, textSize, q);
308 q->setContentsMargins(q->style()->pixelMetric(QStyle::PM_IndicatorWidth), headerSize.height(), 0, 0);
309}
310
311void KCollapsibleGroupBoxPrivate::updateChildrenFocus(bool expanded)
312{
313 const auto children = q->children();
314 for (QObject *child : children) {
315 QWidget *widget = qobject_cast<QWidget *>(child);
316 if (!widget) {
317 continue;
318 }
319 // Restore old focus policy if expanded, remove from focus chain otherwise.
320 if (expanded) {
321 widget->setFocusPolicy(focusMap.value(widget));
322 } else {
324 }
325 }
326}
327
328QSize KCollapsibleGroupBoxPrivate::contentSize() const
329{
330 if (q->layout()) {
331 const QMargins margins = q->contentsMargins();
332 const QSize marginSize(margins.left() + margins.right(), margins.top() + margins.bottom());
333 return q->layout()->sizeHint() + marginSize;
334 }
335 return QSize(0, 0);
336}
337
338QSize KCollapsibleGroupBoxPrivate::contentMinimumSize() const
339{
340 if (q->layout()) {
341 const QMargins margins = q->contentsMargins();
342 const QSize marginSize(margins.left() + margins.right(), margins.top() + margins.bottom());
343 return q->layout()->minimumSize() + marginSize;
344 }
345 return QSize(0, 0);
346}
347
348#include "moc_kcollapsiblegroupbox.cpp"
A groupbox featuring a clickable header and arrow indicator that can be expanded and collapsed to rev...
bool isExpanded() const
Whether contents are shown During animations, this will reflect the target state at the end of the an...
void expandedChanged()
Emitted when the widget expands or collapsed.
void expand()
Equivalent to setExpanded(true)
void titleChanged()
Emitted when the title is changed.
void collapse()
Equivalent to setExpanded(false)
void toggle()
Expands if collapsed and vice versa.
void setTitle(const QString &title)
Set the title that will be permanently shown at the top of the collapsing box Mnemonics are supported...
void setExpanded(bool expanded)
Set whether contents are shown.
QObject * child() const const
QKeySequence mnemonic(const QString &text)
virtual void setGeometry(const QRect &r) override
int bottom() const const
int left() const const
int right() const const
int top() const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
Q_EMITQ_EMIT
bool isWidgetType() const const
int shortcutId() const const
CE_CheckBoxLabel
PM_IndicatorWidth
PrimitiveElement
SH_Widget_Animation_Duration
SE_CheckBoxIndicator
virtual void drawControl(ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const const=0
virtual void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const const=0
virtual QRect subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget) const const=0
void initFrom(const QWidget *widget)
QueuedConnection
TabFocus
Key_Space
TextShowMnemonic
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void stateChanged(QTimeLine::State newState)
void valueChanged(qreal value)
QWidget(QWidget *parent, Qt::WindowFlags f)
void setAccessibleName(const QString &name)
QMargins contentsMargins() const const
virtual bool event(QEvent *event) override
bool hasFocus() const const
int grabShortcut(const QKeySequence &key, Qt::ShortcutContext context)
QLayout * layout() const const
virtual void leaveEvent(QEvent *event)
virtual void mouseMoveEvent(QMouseEvent *event)
void releaseShortcut(int id)
virtual void resizeEvent(QResizeEvent *event)
void setFixedHeight(int h)
QStyle * style() const const
void update()
void updateGeometry()
bool isVisible() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 11:54:50 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.