From 30d351e4e508dc1bcb002dd4422a67c700263cbc Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sat, 1 Aug 2026 14:18:12 +1000 Subject: [PATCH] Qt: Add CollapsibleWidget --- src/duckstation-qt/CMakeLists.txt | 2 + src/duckstation-qt/collapsiblewidget.cpp | 122 ++++++++++++++++++ src/duckstation-qt/collapsiblewidget.h | 37 ++++++ src/duckstation-qt/duckstation-qt.vcxproj | 2 + .../duckstation-qt.vcxproj.filters | 2 + 5 files changed, 165 insertions(+) create mode 100644 src/duckstation-qt/collapsiblewidget.cpp create mode 100644 src/duckstation-qt/collapsiblewidget.h diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 520a0585f..f2d98ea1d 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -40,6 +40,8 @@ set(SRCS biossettingswidget.ui cardradiobutton.cpp cardradiobutton.h + collapsiblewidget.cpp + collapsiblewidget.h colorpickerbutton.cpp colorpickerbutton.h capturesettingswidget.cpp diff --git a/src/duckstation-qt/collapsiblewidget.cpp b/src/duckstation-qt/collapsiblewidget.cpp new file mode 100644 index 000000000..0aa5b787f --- /dev/null +++ b/src/duckstation-qt/collapsiblewidget.cpp @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "collapsiblewidget.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "moc_collapsiblewidget.cpp" + +CollapsibleWidget::CollapsibleWidget(QWidget* parent) : QGroupBox(parent) +{ + init(); +} + +CollapsibleWidget::CollapsibleWidget(const QString& title, QWidget* parent) : QGroupBox(title, parent) +{ + init(); +} + +CollapsibleWidget::~CollapsibleWidget() = default; + +void CollapsibleWidget::init() +{ + setCheckable(true); + setChecked(false); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + + m_animation = new QPropertyAnimation(this, "maximumHeight", this); + m_animation->setDuration(300); + m_animation->setEasingCurve(QEasingCurve::InOutCubic); + + connect(this, &QGroupBox::toggled, this, &CollapsibleWidget::toggleCollapsed); + connect(m_animation, &QPropertyAnimation::finished, this, [this]() { + if (isChecked()) + setMaximumHeight(QWIDGETSIZE_MAX); + }); + + setMaximumHeight(collapsedHeight()); +} + +int CollapsibleWidget::collapsedHeight() const +{ + QStyleOptionGroupBox option; + initStyleOption(&option); + + const QRect label_rect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxLabel, this); + const QRect check_box_rect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxCheckBox, this); + const int header_bottom = std::max(label_rect.bottom(), check_box_rect.bottom()) + 1; + return std::max(header_bottom + contentsMargins().bottom(), fontMetrics().height()); +} + +void CollapsibleWidget::updateExpandedHeight() +{ + const int old_maximum_height = maximumHeight(); + setMaximumHeight(QWIDGETSIZE_MAX); + m_expanded_height = sizeHint().height(); + setMaximumHeight(old_maximum_height); +} + +void CollapsibleWidget::toggleCollapsed(bool expanded) +{ + m_animation->stop(); + + if (expanded) + updateExpandedHeight(); + + m_animation->setStartValue(height()); + m_animation->setEndValue(expanded ? m_expanded_height : collapsedHeight()); + m_animation->start(); +} + +void CollapsibleWidget::setCollapsed(bool collapsed) +{ + setChecked(!collapsed); +} + +void CollapsibleWidget::changeEvent(QEvent* event) +{ + QGroupBox::changeEvent(event); + + if (!isChecked() && (event->type() == QEvent::FontChange || event->type() == QEvent::StyleChange)) + setMaximumHeight(collapsedHeight()); +} + +void CollapsibleWidget::paintEvent(QPaintEvent* event) +{ + Q_UNUSED(event); + + QStylePainter painter(this); + QStyleOptionGroupBox option; + initStyleOption(&option); + + const QRect indicator_rect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxCheckBox, this); + if (!isChecked()) + option.subControls &= ~QStyle::SC_GroupBoxFrame; + + const QRegion group_box_region = QRegion(rect()).subtracted(indicator_rect.adjusted(-1, -1, 1, 1)); + painter.setClipRegion(group_box_region); + painter.drawComplexControl(QStyle::CC_GroupBox, option); + painter.setClipping(false); + + QStyleOption arrow_option; + arrow_option.initFrom(this); + arrow_option.rect = indicator_rect; + painter.drawPrimitive(isChecked() ? QStyle::PE_IndicatorArrowDown : QStyle::PE_IndicatorArrowRight, arrow_option); +} + +void CollapsibleWidget::showEvent(QShowEvent* event) +{ + QGroupBox::showEvent(event); + + if (!isChecked()) + setMaximumHeight(collapsedHeight()); +} diff --git a/src/duckstation-qt/collapsiblewidget.h b/src/duckstation-qt/collapsiblewidget.h new file mode 100644 index 000000000..ba07b8a82 --- /dev/null +++ b/src/duckstation-qt/collapsiblewidget.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include + +class QPropertyAnimation; +class QPaintEvent; +class QShowEvent; + +class CollapsibleWidget final : public QGroupBox +{ + Q_OBJECT + +public: + explicit CollapsibleWidget(QWidget* parent = nullptr); + explicit CollapsibleWidget(const QString& title, QWidget* parent = nullptr); + ~CollapsibleWidget() override; + + bool isCollapsed() const { return !isChecked(); } + void setCollapsed(bool collapsed); + +protected: + void changeEvent(QEvent* event) override; + void paintEvent(QPaintEvent* event) override; + void showEvent(QShowEvent* event) override; + +private: + void init(); + int collapsedHeight() const; + void updateExpandedHeight(); + void toggleCollapsed(bool expanded); + + QPropertyAnimation* m_animation = nullptr; + int m_expanded_height = 0; +}; diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index 68a8a39cc..cb3ec3cb4 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -14,6 +14,7 @@ + @@ -97,6 +98,7 @@ + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index 76ae44b2b..103d76d79 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -59,6 +59,7 @@ + @@ -130,6 +131,7 @@ +