Files
RedBear-OS/local/recipes/kde/kf6-kwidgetsaddons/source/src/kcollapsiblegroupbox.cpp
T
2026-04-14 10:51:06 +01:00

344 lines
9.7 KiB
C++

/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2015 David Edmundson <davidedmundson@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kcollapsiblegroupbox.h"
#include <QLabel>
#include <QLayout>
#include <QMouseEvent>
#include <QPainter>
#include <QStyle>
#include <QStyleOption>
#include <QTimeLine>
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<QWidget *, Qt::FocusPolicy> 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<QShortcutEvent *>(event);
if (d->shortcutId == se->shortcutId()) {
toggle();
return true;
}
break;
}
case QEvent::ChildAdded: {
QChildEvent *ce = static_cast<QChildEvent *>(event);
if (ce->child()->isWidgetType()) {
auto widget = static_cast<QWidget *>(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<QWidget *>(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"