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 d->animation->setDirection(expanded ? QTimeLine::Forward : QTimeLine::Backward);
108 // QTimeLine::duration() must be > 0
109 const int duration = qMax(1, style()->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
123{
124 return d->isExpanded;
125}
126
128{
129 setExpanded(false);
130}
131
133{
134 setExpanded(true);
135}
136
138{
139 setExpanded(!d->isExpanded);
140}
141
142void KCollapsibleGroupBox::paintEvent(QPaintEvent *event)
143{
144 QPainter p(this);
145
146 QStyleOptionButton baseOption;
147 baseOption.initFrom(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
156 if (d->isExpanded) {
158 } else {
160 }
161
162 QStyleOptionButton indicatorOption = baseOption;
163 indicatorOption.rect = style()->subElementRect(QStyle::SE_CheckBoxIndicator, &indicatorOption, this);
164 style()->drawPrimitive(element, &indicatorOption, &p, this);
165
166 QStyleOptionButton labelOption = baseOption;
167 labelOption.rect = style()->subElementRect(QStyle::SE_CheckBoxContents, &labelOption, this);
168 style()->drawControl(QStyle::CE_CheckBoxLabel, &labelOption, &p, 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(d->contentSize().width(), d->headerSize.width());
185 return QSize(minimumWidth, d->headerSize.height());
186}
187
188bool KCollapsibleGroupBox::event(QEvent *event)
189{
190 switch (event->type()) {
192 /*fall through*/
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 };
215 }
216 break;
217 }
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(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(event->pos());
244
245 if (headerContainsMouse != d->headerContainsMouse) {
246 d->headerContainsMouse = headerContainsMouse;
247 update();
248 }
249
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
283}
284
285void KCollapsibleGroupBox::overrideFocusPolicyOf(QWidget *widget)
286{
287 d->focusMap.insert(widget, widget->focusPolicy());
288
289 if (!isExpanded()) {
290 // Prevent tab focus if not expanded.
292 }
293}
294
295void KCollapsibleGroupBoxPrivate::recalculateHeaderSize()
296{
297 QStyleOption option;
298 option.initFrom(q);
299
300 QSize textSize = q->style()->itemTextRect(option.fontMetrics, QRect(), Qt::TextShowMnemonic, false, title).size();
301
302 headerSize = q->style()->sizeFromContents(QStyle::CT_CheckBox, &option, textSize, q);
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 *>(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(widget));
317 } else {
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"
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.
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
QObject * child() const const
QKeySequence mnemonic(const QString &text)
virtual QSize minimumSize() const const override
virtual void setGeometry(const QRect &r) override
virtual QSize sizeHint() const const=0
T value(const Key &key, const T &defaultValue) const const
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
const QObjectList & children() const const
bool isWidgetType() const const
QSize size() const const
int shortcutId() const const
int height() 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 itemTextRect(const QFontMetrics &metrics, const QRect &rectangle, int alignment, bool enabled, const QString &text) const const
virtual int pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const const=0
virtual QSize sizeFromContents(ContentsType type, const QStyleOption *option, const QSize &contentsSize, 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)
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 setContentsMargins(const QMargins &margins)
void setFixedHeight(int h)
QStyle * style() const const
void update()
void updateGeometry()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 19 2024 11:51:03 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.