cf12defd28
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
344 lines
9.7 KiB
C++
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"
|