KWidgetsAddons

kcollapsiblegroupbox.cpp
1 /*
2  This file is part of the KDE project
3  SPDX-FileCopyrightText: 2015 David Edmundson <[email protected]>
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 <QStyle>
13 #include <QPainter>
14 #include <QStyleOption>
15 #include <QTimeLine>
16 #include <QMouseEvent>
17 
18 class KCollapsibleGroupBoxPrivate {
19 public:
20  KCollapsibleGroupBoxPrivate(KCollapsibleGroupBox *q);
21  void updateChildrenFocus(bool expanded);
22  void recalculateHeaderSize();
23  QSize contentSize() const;
24  QSize contentMinimumSize() const;
25 
27  QTimeLine *animation;
28  QString title;
29  bool isExpanded;
30  bool headerContainsMouse;
31  QSize headerSize;
32  int shortcutId;
33  QMap<QWidget*, Qt::FocusPolicy> focusMap; // Used to restore focus policy of widgets.
34 };
35 
36 KCollapsibleGroupBoxPrivate::KCollapsibleGroupBoxPrivate(KCollapsibleGroupBox* q):
37  q(q),
38  isExpanded(false),
39  headerContainsMouse(false),
40  shortcutId(0)
41 {}
42 
43 KCollapsibleGroupBox::KCollapsibleGroupBox(QWidget* parent):
44  QWidget(parent),
45  d(new KCollapsibleGroupBoxPrivate(this))
46 {
47  d->recalculateHeaderSize();
48 
49  d->animation = new QTimeLine(500, this); //duration matches kmessagewidget
50  connect(d->animation, &QTimeLine::valueChanged, this, [this](qreal value) {
51  setFixedHeight((d->contentSize().height() * value) + d->headerSize.height());
52  });
53  connect(d->animation, &QTimeLine::stateChanged, this, [this](QTimeLine::State state) {
54  if (state == QTimeLine::NotRunning) {
55  d->updateChildrenFocus(d->isExpanded);
56  }
57  });
58 
60  setFocusPolicy(Qt::TabFocus);
61  setMouseTracking(true);
62 }
63 
64 KCollapsibleGroupBox::~KCollapsibleGroupBox()
65 {
66  if (d->animation->state() == QTimeLine::Running) {
67  d->animation->stop();
68  }
69  delete d;
70 }
71 
73 {
74  d->title = title;
75  d->recalculateHeaderSize();
76 
77  update();
79 
80  if (d->shortcutId) {
81  releaseShortcut(d->shortcutId);
82  }
83 
84  d->shortcutId = grabShortcut(QKeySequence::mnemonic(title));
85 
86 #ifndef QT_NO_ACCESSIBILITY
87  setAccessibleName(title);
88 #endif
89 
90  emit titleChanged();
91 }
92 
94 {
95  return d->title;
96 }
97 
99 {
100  if (expanded == d->isExpanded) {
101  return;
102  }
103 
104  d->isExpanded = expanded;
105  emit expandedChanged();
106 
107  d->updateChildrenFocus(expanded);
108 
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 }
123 
125 {
126  return d->isExpanded;
127 }
128 
130 {
131  setExpanded(false);
132 }
133 
135 {
136  setExpanded(true);
137 }
138 
140 {
141  setExpanded(!d->isExpanded);
142 }
143 
144 void KCollapsibleGroupBox::paintEvent(QPaintEvent *event)
145 {
146  QPainter p(this);
147 
148  QStyleOptionButton baseOption;
149  baseOption.initFrom(this);
150  baseOption.rect = QRect(0, 0, width(), d->headerSize.height());
151  baseOption.text = d->title;
152 
153  if (d->headerContainsMouse) {
154  baseOption.state |= QStyle::State_MouseOver;
155  }
156 
157  QStyle::PrimitiveElement element;
158  if (d->isExpanded) {
160  } else {
162  }
163 
164  QStyleOptionButton indicatorOption = baseOption;
165  indicatorOption.rect = style()->subElementRect(QStyle::SE_CheckBoxIndicator, &indicatorOption, this);
166  style()->drawPrimitive(element, &indicatorOption, &p, this);
167 
168  QStyleOptionButton labelOption = baseOption;
169  labelOption.rect = style()->subElementRect(QStyle::SE_CheckBoxContents, &labelOption, this);
170  style()->drawControl(QStyle::CE_CheckBoxLabel, &labelOption, &p, this);
171 
172  Q_UNUSED(event)
173 }
174 
175 QSize KCollapsibleGroupBox::sizeHint() const
176 {
177  if (d->isExpanded) {
178  return d->contentSize() + QSize(0, d->headerSize.height());
179  } else {
180  return QSize(d->contentSize().width(), d->headerSize.height());
181  }
182 }
183 
184 QSize KCollapsibleGroupBox::minimumSizeHint() const
185 {
186  int minimumWidth = qMax(d->contentSize().width(), d->headerSize.width());
187  return QSize(minimumWidth, d->headerSize.height());
188 }
189 
190 bool KCollapsibleGroupBox::event(QEvent *event)
191 {
192  switch (event->type()) {
193  case QEvent::StyleChange:
194  /*fall through*/
195  case QEvent::FontChange:
196  d->recalculateHeaderSize();
197  break;
198  case QEvent::Shortcut:
199  {
200  QShortcutEvent *se = static_cast<QShortcutEvent*>(event);
201  if(d->shortcutId == se->shortcutId()) {
202  toggle();
203  return true;
204  }
205  break;
206  }
207  case QEvent::ChildAdded:
208  {
209  QChildEvent *ce = static_cast<QChildEvent*>(event);
210  if (ce->child()->isWidgetType()) {
211  auto widget = static_cast<QWidget*>(ce->child());
212  // Needs to be called asynchronously because at this point the widget is likely a "real" QWidget,
213  // i.e. the QWidget base class whose constructor sets the focus policy to NoPolicy.
214  // But the constructor of the child class (not yet called) could set a different focus policy later.
215  QMetaObject::invokeMethod(this, "overrideFocusPolicyOf", Qt::QueuedConnection, Q_ARG(QWidget*, widget));
216  }
217  break;
218  }
220  if (d->animation->state() == QTimeLine::NotRunning) {
221  setFixedHeight(sizeHint().height());
222  }
223  break;
224  default:
225  break;
226  }
227 
228  return QWidget::event(event);
229 }
230 
231 void KCollapsibleGroupBox::mousePressEvent(QMouseEvent *event)
232 {
233  const QRect headerRect(0, 0, width(), d->headerSize.height());
234  if (headerRect.contains(event->pos())) {
235  toggle();
236  }
237  event->setAccepted(true);
238 }
239 
240 //if mouse has changed whether it is in the top bar or not refresh to change arrow icon
241 void KCollapsibleGroupBox::mouseMoveEvent(QMouseEvent *event)
242 {
243  const QRect headerRect(0, 0, width(), d->headerSize.height());
244  bool headerContainsMouse = headerRect.contains(event->pos());
245 
246  if (headerContainsMouse != d->headerContainsMouse) {
247  d->headerContainsMouse = headerContainsMouse;
248  update();
249  }
250 
252 }
253 
254 void KCollapsibleGroupBox::leaveEvent(QEvent *event)
255 {
256  d->headerContainsMouse = false;
257  update();
258  QWidget::leaveEvent(event);
259 }
260 
261 void KCollapsibleGroupBox::keyPressEvent(QKeyEvent *event)
262 {
263  //event might have just propagated up from a child, if so we don't want to react to it
264  if (!hasFocus()) {
265  return;
266  }
267  const int key = event->key();
268  if (key == Qt::Key_Space || key == Qt::Key_Enter || key == Qt::Key_Return) {
269  toggle();
270  event->setAccepted(true);
271  }
272 }
273 
274 void KCollapsibleGroupBox::resizeEvent(QResizeEvent *event)
275 {
276  const QMargins margins = contentsMargins();
277 
278  if (layout()) {
279  //we don't want the layout trying to fit the current frame of the animation so always set it to the target height
280  layout()->setGeometry(QRect(margins.left(), margins.top(), width() - margins.left() - margins.right(), layout()->sizeHint().height()));
281  }
282 
283  QWidget::resizeEvent(event);
284 }
285 
286 void KCollapsibleGroupBox::overrideFocusPolicyOf(QWidget *widget)
287 {
288  // https://bugs.kde.org/show_bug.cgi?id=396450
289  // A label with word-wrapping enabled will break positioning of the groupbox in the layout.
290  // The cause seems to be the setFocusPolicy() call below, but it's not clear why.
291  // Until a proper fix is found, as workaround we toggle twice the groupbox which fixes the issue.
292  if (auto label = qobject_cast<QLabel*>(widget)) {
293  if (label->wordWrap()) {
294  toggle();
295  toggle();
296  }
297  }
298 
299  d->focusMap.insert(widget, widget->focusPolicy());
300 
301  if (!isExpanded()) {
302  // Prevent tab focus if not expanded.
303  widget->setFocusPolicy(Qt::NoFocus);
304  }
305 }
306 
307 void KCollapsibleGroupBoxPrivate::recalculateHeaderSize()
308 {
309  QStyleOption option;
310  option.initFrom(q);
311 
312  QSize textSize = q->style()->itemTextRect(option.fontMetrics, QRect(), Qt::TextShowMnemonic, false,
313  title).size();
314 
315  headerSize = q->style()->sizeFromContents(QStyle::CT_CheckBox, &option, textSize, q);
316  q->setContentsMargins(q->style()->pixelMetric(QStyle::PM_IndicatorWidth), headerSize.height(), 0, 0);
317 }
318 
319 void KCollapsibleGroupBoxPrivate::updateChildrenFocus(bool expanded)
320 {
321  const auto children = q->children();
322  for (QObject *child : children) {
323  QWidget *widget = qobject_cast<QWidget*>(child);
324  if (!widget) {
325  continue;
326  }
327  // Restore old focus policy if expanded, remove from focus chain otherwise.
328  if (expanded) {
329  widget->setFocusPolicy(focusMap.value(widget));
330  } else {
331  widget->setFocusPolicy(Qt::NoFocus);
332  }
333  }
334 }
335 
336 QSize KCollapsibleGroupBoxPrivate::contentSize() const
337 {
338  if (q->layout()) {
339  const QMargins margins = q->contentsMargins();
340  const QSize marginSize(margins.left() + margins.right(), margins.top() + margins.bottom());
341  return q->layout()->sizeHint() + marginSize;
342  }
343  return QSize(0,0);
344 }
345 
346 QSize KCollapsibleGroupBoxPrivate::contentMinimumSize() const
347 {
348  if (q->layout()) {
349  const QMargins margins = q->contentsMargins();
350  const QSize marginSize(margins.left() + margins.right(), margins.top() + margins.bottom());
351  return q->layout()->minimumSize() + marginSize;
352  }
353  return QSize(0,0);
354 }
QLayout * layout() const const
int grabShortcut(const QKeySequence &key, Qt::ShortcutContext context)
TabFocus
QEvent::Type type() const const
CE_CheckBoxLabel
void updateGeometry()
int right() const const
int left() const const
QStyle * style() const const
void expandedChanged()
Emitted when the widget expands or collapsed.
int minimumWidth() const const
const QObjectList & children() const const
SE_CheckBoxIndicator
SH_Widget_Animation_Duration
bool hasFocus() const const
QObject * child() const const
QMargins contentsMargins() const const
void update()
void setAccessibleName(const QString &name)
void initFrom(const QWidget *widget)
int width() const const
int shortcutId() const const
QKeySequence mnemonic(const QString &text)
void setExpanded(bool expanded)
Set whether contents are shown.
int top() const const
void stateChanged(QTimeLine::State newState)
virtual void mouseMoveEvent(QMouseEvent *event)
int bottom() const const
PrimitiveElement
void releaseShortcut(int id)
bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType type, QGenericReturnArgument ret, QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3, QGenericArgument val4, QGenericArgument val5, QGenericArgument val6, QGenericArgument val7, QGenericArgument val8, QGenericArgument val9)
void valueChanged(qreal value)
bool contains(const QRect &rectangle, bool proper) const const
PM_IndicatorWidth
void setFixedHeight(int h)
virtual void drawControl(QStyle::ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const const =0
QString title() const
The title.
void titleChanged()
Emitted when the title is changed.
int height() const const
TextShowMnemonic
void collapse()
Equivalent to setExpanded(false)
virtual void drawPrimitive(QStyle::PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const const =0
void expand()
Equivalent to setExpanded(true)
virtual QRect subElementRect(QStyle::SubElement element, const QStyleOption *option, const QWidget *widget) const const =0
QPoint pos() const const
virtual void resizeEvent(QResizeEvent *event)
bool isWidgetType() const const
QueuedConnection
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
T qobject_cast(QObject *object)
QObject * parent() const const
A groupbox featuring a clickable header and arrow indicator that can be expanded and collapsed to rev...
virtual void leaveEvent(QEvent *event)
virtual void setGeometry(const QRect &r) override
virtual bool event(QEvent *event) override
virtual QSize sizeHint() const const =0
bool isExpanded() const
Whether contents are shown During animations, this will reflect the target state at the end of the an...
void setTitle(const QString &title)
Set the title that will be permanently shown at the top of the collapsing box Mnemonics are supported...
Key_Space
int height() const const
void toggle()
Expands if collapsed and vice versa.
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Fri Aug 7 2020 22:42:21 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.