File indexing completed on 2025-02-16 13:11:38
0001 /* 0002 This file is part of the KDE project 0003 SPDX-FileCopyrightText: 2015 David Edmundson <davidedmundson@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "kcollapsiblegroupbox.h" 0009 0010 #include <QLabel> 0011 #include <QLayout> 0012 #include <QMouseEvent> 0013 #include <QPainter> 0014 #include <QStyle> 0015 #include <QStyleOption> 0016 #include <QTimeLine> 0017 0018 class KCollapsibleGroupBoxPrivate 0019 { 0020 public: 0021 KCollapsibleGroupBoxPrivate(KCollapsibleGroupBox *qq); 0022 void updateChildrenFocus(bool expanded); 0023 void recalculateHeaderSize(); 0024 QSize contentSize() const; 0025 QSize contentMinimumSize() const; 0026 0027 KCollapsibleGroupBox *const q; 0028 QTimeLine *animation; 0029 QString title; 0030 bool isExpanded = false; 0031 bool headerContainsMouse = false; 0032 QSize headerSize; 0033 int shortcutId = 0; 0034 QMap<QWidget *, Qt::FocusPolicy> focusMap; // Used to restore focus policy of widgets. 0035 }; 0036 0037 KCollapsibleGroupBoxPrivate::KCollapsibleGroupBoxPrivate(KCollapsibleGroupBox *qq) 0038 : q(qq) 0039 { 0040 } 0041 0042 KCollapsibleGroupBox::KCollapsibleGroupBox(QWidget *parent) 0043 : QWidget(parent) 0044 , d(new KCollapsibleGroupBoxPrivate(this)) 0045 { 0046 d->recalculateHeaderSize(); 0047 0048 d->animation = new QTimeLine(500, this); // duration matches kmessagewidget 0049 connect(d->animation, &QTimeLine::valueChanged, this, [this](qreal value) { 0050 setFixedHeight((d->contentSize().height() * value) + d->headerSize.height()); 0051 }); 0052 connect(d->animation, &QTimeLine::stateChanged, this, [this](QTimeLine::State state) { 0053 if (state == QTimeLine::NotRunning) { 0054 d->updateChildrenFocus(d->isExpanded); 0055 } 0056 }); 0057 0058 setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 0059 setFocusPolicy(Qt::TabFocus); 0060 setMouseTracking(true); 0061 } 0062 0063 KCollapsibleGroupBox::~KCollapsibleGroupBox() 0064 { 0065 if (d->animation->state() == QTimeLine::Running) { 0066 d->animation->stop(); 0067 } 0068 } 0069 0070 void KCollapsibleGroupBox::setTitle(const QString &title) 0071 { 0072 d->title = title; 0073 d->recalculateHeaderSize(); 0074 0075 update(); 0076 updateGeometry(); 0077 0078 if (d->shortcutId) { 0079 releaseShortcut(d->shortcutId); 0080 } 0081 0082 d->shortcutId = grabShortcut(QKeySequence::mnemonic(title)); 0083 0084 #ifndef QT_NO_ACCESSIBILITY 0085 setAccessibleName(title); 0086 #endif 0087 0088 Q_EMIT titleChanged(); 0089 } 0090 0091 QString KCollapsibleGroupBox::title() const 0092 { 0093 return d->title; 0094 } 0095 0096 void KCollapsibleGroupBox::setExpanded(bool expanded) 0097 { 0098 if (expanded == d->isExpanded) { 0099 return; 0100 } 0101 0102 d->isExpanded = expanded; 0103 Q_EMIT expandedChanged(); 0104 0105 d->updateChildrenFocus(expanded); 0106 0107 d->animation->setDirection(expanded ? QTimeLine::Forward : QTimeLine::Backward); 0108 // QTimeLine::duration() must be > 0 0109 const int duration = qMax(1, style()->styleHint(QStyle::SH_Widget_Animation_Duration)); 0110 d->animation->stop(); 0111 d->animation->setDuration(duration); 0112 d->animation->start(); 0113 0114 // when going from collapsed to expanded changing the child visibility calls an updateGeometry 0115 // which calls sizeHint with expanded true before the first frame of the animation kicks in 0116 // trigger an effective frame 0 0117 if (expanded) { 0118 setFixedHeight(d->headerSize.height()); 0119 } 0120 } 0121 0122 bool KCollapsibleGroupBox::isExpanded() const 0123 { 0124 return d->isExpanded; 0125 } 0126 0127 void KCollapsibleGroupBox::collapse() 0128 { 0129 setExpanded(false); 0130 } 0131 0132 void KCollapsibleGroupBox::expand() 0133 { 0134 setExpanded(true); 0135 } 0136 0137 void KCollapsibleGroupBox::toggle() 0138 { 0139 setExpanded(!d->isExpanded); 0140 } 0141 0142 void KCollapsibleGroupBox::paintEvent(QPaintEvent *event) 0143 { 0144 QPainter p(this); 0145 0146 QStyleOptionButton baseOption; 0147 baseOption.initFrom(this); 0148 baseOption.rect = QRect(0, 0, width(), d->headerSize.height()); 0149 baseOption.text = d->title; 0150 0151 if (d->headerContainsMouse) { 0152 baseOption.state |= QStyle::State_MouseOver; 0153 } 0154 0155 QStyle::PrimitiveElement element; 0156 if (d->isExpanded) { 0157 element = QStyle::PE_IndicatorArrowDown; 0158 } else { 0159 element = isLeftToRight() ? QStyle::PE_IndicatorArrowRight : QStyle::PE_IndicatorArrowLeft; 0160 } 0161 0162 QStyleOptionButton indicatorOption = baseOption; 0163 indicatorOption.rect = style()->subElementRect(QStyle::SE_CheckBoxIndicator, &indicatorOption, this); 0164 style()->drawPrimitive(element, &indicatorOption, &p, this); 0165 0166 QStyleOptionButton labelOption = baseOption; 0167 labelOption.rect = style()->subElementRect(QStyle::SE_CheckBoxContents, &labelOption, this); 0168 style()->drawControl(QStyle::CE_CheckBoxLabel, &labelOption, &p, this); 0169 0170 Q_UNUSED(event) 0171 } 0172 0173 QSize KCollapsibleGroupBox::sizeHint() const 0174 { 0175 if (d->isExpanded) { 0176 return d->contentSize() + QSize(0, d->headerSize.height()); 0177 } else { 0178 return QSize(d->contentSize().width(), d->headerSize.height()); 0179 } 0180 } 0181 0182 QSize KCollapsibleGroupBox::minimumSizeHint() const 0183 { 0184 int minimumWidth = qMax(d->contentSize().width(), d->headerSize.width()); 0185 return QSize(minimumWidth, d->headerSize.height()); 0186 } 0187 0188 bool KCollapsibleGroupBox::event(QEvent *event) 0189 { 0190 switch (event->type()) { 0191 case QEvent::StyleChange: 0192 /*fall through*/ 0193 case QEvent::FontChange: 0194 d->recalculateHeaderSize(); 0195 break; 0196 case QEvent::Shortcut: { 0197 QShortcutEvent *se = static_cast<QShortcutEvent *>(event); 0198 if (d->shortcutId == se->shortcutId()) { 0199 toggle(); 0200 return true; 0201 } 0202 break; 0203 } 0204 case QEvent::ChildAdded: { 0205 QChildEvent *ce = static_cast<QChildEvent *>(event); 0206 if (ce->child()->isWidgetType()) { 0207 auto widget = static_cast<QWidget *>(ce->child()); 0208 // Needs to be called asynchronously because at this point the widget is likely a "real" QWidget, 0209 // i.e. the QWidget base class whose constructor sets the focus policy to NoPolicy. 0210 // But the constructor of the child class (not yet called) could set a different focus policy later. 0211 auto focusFunc = [this, widget]() { 0212 overrideFocusPolicyOf(widget); 0213 }; 0214 QMetaObject::invokeMethod(this, focusFunc, Qt::QueuedConnection); 0215 } 0216 break; 0217 } 0218 case QEvent::LayoutRequest: 0219 if (d->animation->state() == QTimeLine::NotRunning) { 0220 setFixedHeight(sizeHint().height()); 0221 } 0222 break; 0223 default: 0224 break; 0225 } 0226 0227 return QWidget::event(event); 0228 } 0229 0230 void KCollapsibleGroupBox::mousePressEvent(QMouseEvent *event) 0231 { 0232 const QRect headerRect(0, 0, width(), d->headerSize.height()); 0233 if (headerRect.contains(event->pos())) { 0234 toggle(); 0235 } 0236 event->setAccepted(true); 0237 } 0238 0239 // if mouse has changed whether it is in the top bar or not refresh to change arrow icon 0240 void KCollapsibleGroupBox::mouseMoveEvent(QMouseEvent *event) 0241 { 0242 const QRect headerRect(0, 0, width(), d->headerSize.height()); 0243 bool headerContainsMouse = headerRect.contains(event->pos()); 0244 0245 if (headerContainsMouse != d->headerContainsMouse) { 0246 d->headerContainsMouse = headerContainsMouse; 0247 update(); 0248 } 0249 0250 QWidget::mouseMoveEvent(event); 0251 } 0252 0253 void KCollapsibleGroupBox::leaveEvent(QEvent *event) 0254 { 0255 d->headerContainsMouse = false; 0256 update(); 0257 QWidget::leaveEvent(event); 0258 } 0259 0260 void KCollapsibleGroupBox::keyPressEvent(QKeyEvent *event) 0261 { 0262 // event might have just propagated up from a child, if so we don't want to react to it 0263 if (!hasFocus()) { 0264 return; 0265 } 0266 const int key = event->key(); 0267 if (key == Qt::Key_Space || key == Qt::Key_Enter || key == Qt::Key_Return) { 0268 toggle(); 0269 event->setAccepted(true); 0270 } 0271 } 0272 0273 void KCollapsibleGroupBox::resizeEvent(QResizeEvent *event) 0274 { 0275 const QMargins margins = contentsMargins(); 0276 0277 if (layout()) { 0278 // we don't want the layout trying to fit the current frame of the animation so always set it to the target height 0279 layout()->setGeometry(QRect(margins.left(), margins.top(), width() - margins.left() - margins.right(), layout()->sizeHint().height())); 0280 } 0281 0282 QWidget::resizeEvent(event); 0283 } 0284 0285 void KCollapsibleGroupBox::overrideFocusPolicyOf(QWidget *widget) 0286 { 0287 d->focusMap.insert(widget, widget->focusPolicy()); 0288 0289 if (!isExpanded()) { 0290 // Prevent tab focus if not expanded. 0291 widget->setFocusPolicy(Qt::NoFocus); 0292 } 0293 } 0294 0295 void KCollapsibleGroupBoxPrivate::recalculateHeaderSize() 0296 { 0297 QStyleOption option; 0298 option.initFrom(q); 0299 0300 QSize textSize = q->style()->itemTextRect(option.fontMetrics, QRect(), Qt::TextShowMnemonic, false, title).size(); 0301 0302 headerSize = q->style()->sizeFromContents(QStyle::CT_CheckBox, &option, textSize, q); 0303 q->setContentsMargins(q->style()->pixelMetric(QStyle::PM_IndicatorWidth), headerSize.height(), 0, 0); 0304 } 0305 0306 void KCollapsibleGroupBoxPrivate::updateChildrenFocus(bool expanded) 0307 { 0308 const auto children = q->children(); 0309 for (QObject *child : children) { 0310 QWidget *widget = qobject_cast<QWidget *>(child); 0311 if (!widget) { 0312 continue; 0313 } 0314 // Restore old focus policy if expanded, remove from focus chain otherwise. 0315 if (expanded) { 0316 widget->setFocusPolicy(focusMap.value(widget)); 0317 } else { 0318 widget->setFocusPolicy(Qt::NoFocus); 0319 } 0320 } 0321 } 0322 0323 QSize KCollapsibleGroupBoxPrivate::contentSize() const 0324 { 0325 if (q->layout()) { 0326 const QMargins margins = q->contentsMargins(); 0327 const QSize marginSize(margins.left() + margins.right(), margins.top() + margins.bottom()); 0328 return q->layout()->sizeHint() + marginSize; 0329 } 0330 return QSize(0, 0); 0331 } 0332 0333 QSize KCollapsibleGroupBoxPrivate::contentMinimumSize() const 0334 { 0335 if (q->layout()) { 0336 const QMargins margins = q->contentsMargins(); 0337 const QSize marginSize(margins.left() + margins.right(), margins.top() + margins.bottom()); 0338 return q->layout()->minimumSize() + marginSize; 0339 } 0340 return QSize(0, 0); 0341 } 0342 0343 #include "moc_kcollapsiblegroupbox.cpp"