/* This file is part of the KDE project SPDX-FileCopyrightText: 2015 David Edmundson SPDX-License-Identifier: LGPL-2.0-or-later */ #include "kcollapsiblegroupbox.h" #include #include #include #include #include #include #include class KCollapsibleGroupBoxPrivate { public: KCollapsibleGroupBoxPrivate(KCollapsibleGroupBox *qq); void updateChildrenFocus(bool expanded); void recalculateHeaderSize(); QSize contentSize() const; QSize contentMinimumSize() const; KCollapsibleGroupBox *const q; QTimeLine *animation; QString title; bool isExpanded = false; bool headerContainsMouse = false; QSize headerSize; int shortcutId = 0; QMap focusMap; // Used to restore focus policy of widgets. }; KCollapsibleGroupBoxPrivate::KCollapsibleGroupBoxPrivate(KCollapsibleGroupBox *qq) : q(qq) { } KCollapsibleGroupBox::KCollapsibleGroupBox(QWidget *parent) : QWidget(parent) , d(new KCollapsibleGroupBoxPrivate(this)) { d->recalculateHeaderSize(); d->animation = new QTimeLine(500, this); // duration matches kmessagewidget connect(d->animation, &QTimeLine::valueChanged, this, [this](qreal value) { setFixedHeight((d->contentSize().height() * value) + d->headerSize.height()); }); connect(d->animation, &QTimeLine::stateChanged, this, [this](QTimeLine::State state) { if (state == QTimeLine::NotRunning) { d->updateChildrenFocus(d->isExpanded); } }); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); setFocusPolicy(Qt::TabFocus); setMouseTracking(true); } KCollapsibleGroupBox::~KCollapsibleGroupBox() { if (d->animation->state() == QTimeLine::Running) { d->animation->stop(); } } void KCollapsibleGroupBox::setTitle(const QString &title) { d->title = title; d->recalculateHeaderSize(); update(); updateGeometry(); if (d->shortcutId) { releaseShortcut(d->shortcutId); } d->shortcutId = grabShortcut(QKeySequence::mnemonic(title)); #ifndef QT_NO_ACCESSIBILITY setAccessibleName(title); #endif Q_EMIT titleChanged(); } QString KCollapsibleGroupBox::title() const { return d->title; } void KCollapsibleGroupBox::setExpanded(bool expanded) { if (expanded == d->isExpanded) { return; } d->isExpanded = expanded; Q_EMIT expandedChanged(); d->updateChildrenFocus(expanded); d->animation->setDirection(expanded ? QTimeLine::Forward : QTimeLine::Backward); // QTimeLine::duration() must be > 0 const int duration = qMax(1, style()->styleHint(QStyle::SH_Widget_Animation_Duration)); d->animation->stop(); d->animation->setDuration(duration); d->animation->start(); // when going from collapsed to expanded changing the child visibility calls an updateGeometry // which calls sizeHint with expanded true before the first frame of the animation kicks in // trigger an effective frame 0 if (expanded) { setFixedHeight(d->headerSize.height()); } } bool KCollapsibleGroupBox::isExpanded() const { return d->isExpanded; } void KCollapsibleGroupBox::collapse() { setExpanded(false); } void KCollapsibleGroupBox::expand() { setExpanded(true); } void KCollapsibleGroupBox::toggle() { setExpanded(!d->isExpanded); } void KCollapsibleGroupBox::paintEvent(QPaintEvent *event) { QPainter p(this); QStyleOptionButton baseOption; baseOption.initFrom(this); baseOption.rect = QRect(0, 0, width(), d->headerSize.height()); baseOption.text = d->title; if (d->headerContainsMouse) { baseOption.state |= QStyle::State_MouseOver; } QStyle::PrimitiveElement element; if (d->isExpanded) { element = QStyle::PE_IndicatorArrowDown; } else { element = isLeftToRight() ? QStyle::PE_IndicatorArrowRight : QStyle::PE_IndicatorArrowLeft; } QStyleOptionButton indicatorOption = baseOption; indicatorOption.rect = style()->subElementRect(QStyle::SE_CheckBoxIndicator, &indicatorOption, this); style()->drawPrimitive(element, &indicatorOption, &p, this); QStyleOptionButton labelOption = baseOption; labelOption.rect = style()->subElementRect(QStyle::SE_CheckBoxContents, &labelOption, this); style()->drawControl(QStyle::CE_CheckBoxLabel, &labelOption, &p, this); Q_UNUSED(event) } QSize KCollapsibleGroupBox::sizeHint() const { if (d->isExpanded) { return d->contentSize() + QSize(0, d->headerSize.height()); } else { return QSize(d->contentSize().width(), d->headerSize.height()); } } QSize KCollapsibleGroupBox::minimumSizeHint() const { int minimumWidth = qMax(d->contentSize().width(), d->headerSize.width()); return QSize(minimumWidth, d->headerSize.height()); } bool KCollapsibleGroupBox::event(QEvent *event) { switch (event->type()) { case QEvent::StyleChange: /*fall through*/ case QEvent::FontChange: d->recalculateHeaderSize(); break; case QEvent::Shortcut: { QShortcutEvent *se = static_cast(event); if (d->shortcutId == se->shortcutId()) { toggle(); return true; } break; } case QEvent::ChildAdded: { QChildEvent *ce = static_cast(event); if (ce->child()->isWidgetType()) { auto widget = static_cast(ce->child()); // Needs to be called asynchronously because at this point the widget is likely a "real" QWidget, // i.e. the QWidget base class whose constructor sets the focus policy to NoPolicy. // But the constructor of the child class (not yet called) could set a different focus policy later. auto focusFunc = [this, widget]() { overrideFocusPolicyOf(widget); }; QMetaObject::invokeMethod(this, focusFunc, Qt::QueuedConnection); } break; } case QEvent::LayoutRequest: if (d->animation->state() == QTimeLine::NotRunning) { setFixedHeight(sizeHint().height()); } break; default: break; } return QWidget::event(event); } void KCollapsibleGroupBox::mousePressEvent(QMouseEvent *event) { const QRect headerRect(0, 0, width(), d->headerSize.height()); if (headerRect.contains(event->pos())) { toggle(); } event->setAccepted(true); } // if mouse has changed whether it is in the top bar or not refresh to change arrow icon void KCollapsibleGroupBox::mouseMoveEvent(QMouseEvent *event) { const QRect headerRect(0, 0, width(), d->headerSize.height()); bool headerContainsMouse = headerRect.contains(event->pos()); if (headerContainsMouse != d->headerContainsMouse) { d->headerContainsMouse = headerContainsMouse; update(); } QWidget::mouseMoveEvent(event); } void KCollapsibleGroupBox::leaveEvent(QEvent *event) { d->headerContainsMouse = false; update(); QWidget::leaveEvent(event); } void KCollapsibleGroupBox::keyPressEvent(QKeyEvent *event) { // event might have just propagated up from a child, if so we don't want to react to it if (!hasFocus()) { return; } const int key = event->key(); if (key == Qt::Key_Space || key == Qt::Key_Enter || key == Qt::Key_Return) { toggle(); event->setAccepted(true); } } void KCollapsibleGroupBox::resizeEvent(QResizeEvent *event) { const QMargins margins = contentsMargins(); if (layout()) { // we don't want the layout trying to fit the current frame of the animation so always set it to the target height layout()->setGeometry(QRect(margins.left(), margins.top(), width() - margins.left() - margins.right(), layout()->sizeHint().height())); } QWidget::resizeEvent(event); } void KCollapsibleGroupBox::overrideFocusPolicyOf(QWidget *widget) { d->focusMap.insert(widget, widget->focusPolicy()); if (!isExpanded()) { // Prevent tab focus if not expanded. widget->setFocusPolicy(Qt::NoFocus); } } void KCollapsibleGroupBoxPrivate::recalculateHeaderSize() { QStyleOption option; option.initFrom(q); QSize textSize = q->style()->itemTextRect(option.fontMetrics, QRect(), Qt::TextShowMnemonic, false, title).size(); headerSize = q->style()->sizeFromContents(QStyle::CT_CheckBox, &option, textSize, q); q->setContentsMargins(q->style()->pixelMetric(QStyle::PM_IndicatorWidth), headerSize.height(), 0, 0); } void KCollapsibleGroupBoxPrivate::updateChildrenFocus(bool expanded) { const auto children = q->children(); for (QObject *child : children) { QWidget *widget = qobject_cast(child); if (!widget) { continue; } // Restore old focus policy if expanded, remove from focus chain otherwise. if (expanded) { widget->setFocusPolicy(focusMap.value(widget)); } else { widget->setFocusPolicy(Qt::NoFocus); } } } QSize KCollapsibleGroupBoxPrivate::contentSize() const { if (q->layout()) { const QMargins margins = q->contentsMargins(); const QSize marginSize(margins.left() + margins.right(), margins.top() + margins.bottom()); return q->layout()->sizeHint() + marginSize; } return QSize(0, 0); } QSize KCollapsibleGroupBoxPrivate::contentMinimumSize() const { if (q->layout()) { const QMargins margins = q->contentsMargins(); const QSize marginSize(margins.left() + margins.right(), margins.top() + margins.bottom()); return q->layout()->minimumSize() + marginSize; } return QSize(0, 0); } #include "moc_kcollapsiblegroupbox.cpp"