File indexing completed on 2024-04-28 03:59:01

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"