mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-19 18:53:32 +00:00
feat(ui): add tab switch animations and improve responsive layout
- Add AnimateTabSwitch method to configure dialogs with slide/fade animations - Update responsive layout breakpoint from 550px to 850px - Improve configure UI layout structure (accent color, rainbow mode positioning) - Update button stylesheets for horizontal/vertical navigation modes - Add animation includes (QPropertyAnimation, QGraphicsOpacityEffect, etc.) Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -7,11 +7,16 @@
|
||||
#include <memory>
|
||||
#include <QApplication>
|
||||
#include <QButtonGroup>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QParallelAnimationGroup>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QScrollArea>
|
||||
#include <QScrollBar>
|
||||
#include <QSequentialAnimationGroup>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
@@ -106,7 +111,6 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
|
||||
nav_layout->setSpacing(4);
|
||||
for (QPushButton* button : tab_buttons) {
|
||||
button->setParent(ui->topButtonWidget);
|
||||
// Buttons are added to a layout in SetUIPositioning
|
||||
if (button->property("class").toString() == QStringLiteral("tabButton")) {
|
||||
button->installEventFilter(animation_filter);
|
||||
}
|
||||
@@ -154,9 +158,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
|
||||
ui->stackedWidget->addWidget(CreateScrollArea(applets_tab.get()));
|
||||
ui->stackedWidget->addWidget(CreateScrollArea(debug_tab_tab.get()));
|
||||
|
||||
connect(tab_button_group.get(), qOverload<int>(&QButtonGroup::idClicked), this, [this](int id) {
|
||||
ui->stackedWidget->setCurrentIndex(id);
|
||||
});
|
||||
connect(tab_button_group.get(), qOverload<int>(&QButtonGroup::idClicked), this, &ConfigureDialog::AnimateTabSwitch);
|
||||
connect(ui_tab.get(), &ConfigureUi::themeChanged, this, &ConfigureDialog::UpdateTheme);
|
||||
connect(ui_tab.get(), &ConfigureUi::UIPositioningChanged, this, &ConfigureDialog::SetUIPositioning);
|
||||
connect(rainbow_timer, &QTimer::timeout, this, &ConfigureDialog::UpdateTheme);
|
||||
@@ -241,7 +243,6 @@ void ConfigureDialog::SetUIPositioning(const QString& positioning) {
|
||||
if (positioning == QStringLiteral("Horizontal")) {
|
||||
ui->nav_container->hide();
|
||||
ui->horizontalNavScrollArea->show();
|
||||
// Remove stretch from vertical layout if it exists
|
||||
if (v_layout->count() > 0) {
|
||||
if (auto* item = v_layout->itemAt(v_layout->count() - 1); item && item->spacerItem()) {
|
||||
v_layout->takeAt(v_layout->count() - 1);
|
||||
@@ -251,13 +252,12 @@ void ConfigureDialog::SetUIPositioning(const QString& positioning) {
|
||||
for (QPushButton* button : tab_buttons) {
|
||||
v_layout->removeWidget(button);
|
||||
h_layout->addWidget(button);
|
||||
button->setStyleSheet(QStringLiteral("text-align: center;"));
|
||||
button->setStyleSheet(QStringLiteral("text-align: left center; padding-left: 15px;"));
|
||||
}
|
||||
h_layout->addStretch(1);
|
||||
} else { // Vertical
|
||||
ui->horizontalNavScrollArea->hide();
|
||||
ui->nav_container->show();
|
||||
// Remove stretch from horizontal layout if it exists
|
||||
if (h_layout->count() > 0) {
|
||||
if (auto* item = h_layout->itemAt(h_layout->count() - 1); item && item->spacerItem()) {
|
||||
h_layout->takeAt(h_layout->count() - 1);
|
||||
@@ -267,7 +267,8 @@ void ConfigureDialog::SetUIPositioning(const QString& positioning) {
|
||||
for (QPushButton* button : tab_buttons) {
|
||||
h_layout->removeWidget(button);
|
||||
v_layout->addWidget(button);
|
||||
button->setStyleSheet(QStringLiteral("")); // Reset to parent stylesheet
|
||||
// Reset the inline stylesheet so it uses the main template's style.
|
||||
button->setStyleSheet(QStringLiteral(""));
|
||||
}
|
||||
v_layout->addStretch(1);
|
||||
}
|
||||
@@ -327,3 +328,94 @@ void ConfigureDialog::OnLanguageChanged(const QString& locale) {
|
||||
RetranslateUI();
|
||||
SetConfiguration();
|
||||
}
|
||||
|
||||
void ConfigureDialog::AnimateTabSwitch(int id) {
|
||||
static bool is_animating = false;
|
||||
if (is_animating) {
|
||||
return;
|
||||
}
|
||||
|
||||
QWidget* current_widget = ui->stackedWidget->currentWidget();
|
||||
QWidget* next_widget = ui->stackedWidget->widget(id);
|
||||
|
||||
if (current_widget == next_widget || !current_widget || !next_widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int duration = 400;
|
||||
|
||||
// Prepare Widgets for Live Animation
|
||||
next_widget->setGeometry(0, 0, ui->stackedWidget->width(), ui->stackedWidget->height());
|
||||
next_widget->move(0, 0);
|
||||
next_widget->show();
|
||||
next_widget->raise();
|
||||
|
||||
// Animation Logic
|
||||
auto anim_old_pos = new QPropertyAnimation(current_widget, "pos");
|
||||
anim_old_pos->setEndValue(QPoint(-ui->stackedWidget->width(), 0));
|
||||
anim_old_pos->setDuration(duration);
|
||||
anim_old_pos->setEasingCurve(QEasingCurve::InOutQuart);
|
||||
|
||||
auto anim_new_pos = new QPropertyAnimation(next_widget, "pos");
|
||||
anim_new_pos->setStartValue(QPoint(ui->stackedWidget->width(), 0));
|
||||
anim_new_pos->setEndValue(QPoint(0, 0));
|
||||
anim_new_pos->setDuration(duration);
|
||||
anim_new_pos->setEasingCurve(QEasingCurve::InOutQuart);
|
||||
|
||||
auto new_opacity_effect = new QGraphicsOpacityEffect(next_widget);
|
||||
next_widget->setGraphicsEffect(new_opacity_effect);
|
||||
auto anim_new_opacity = new QPropertyAnimation(new_opacity_effect, "opacity");
|
||||
anim_new_opacity->setStartValue(0.0);
|
||||
anim_new_opacity->setEndValue(1.0);
|
||||
anim_new_opacity->setDuration(duration);
|
||||
anim_new_opacity->setEasingCurve(QEasingCurve::InQuad);
|
||||
|
||||
auto* button_opacity_effect = qobject_cast<QGraphicsOpacityEffect*>(ui->buttonBox->graphicsEffect());
|
||||
if (!button_opacity_effect) {
|
||||
button_opacity_effect = new QGraphicsOpacityEffect(ui->buttonBox);
|
||||
ui->buttonBox->setGraphicsEffect(button_opacity_effect);
|
||||
}
|
||||
auto* button_anim_sequence = new QSequentialAnimationGroup(this);
|
||||
|
||||
auto* anim_buttons_fade_out = new QPropertyAnimation(button_opacity_effect, "opacity");
|
||||
anim_buttons_fade_out->setDuration(duration / 2);
|
||||
anim_buttons_fade_out->setStartValue(1.0);
|
||||
anim_buttons_fade_out->setEndValue(0.0);
|
||||
anim_buttons_fade_out->setEasingCurve(QEasingCurve::OutCubic);
|
||||
|
||||
auto* anim_buttons_fade_in = new QPropertyAnimation(button_opacity_effect, "opacity");
|
||||
anim_buttons_fade_in->setDuration(duration / 2);
|
||||
anim_buttons_fade_in->setStartValue(0.0);
|
||||
anim_buttons_fade_in->setEndValue(1.0);
|
||||
anim_buttons_fade_in->setEasingCurve(QEasingCurve::InCubic);
|
||||
|
||||
button_anim_sequence->addAnimation(anim_buttons_fade_out);
|
||||
button_anim_sequence->addAnimation(anim_buttons_fade_in);
|
||||
|
||||
auto animation_group = new QParallelAnimationGroup(this);
|
||||
animation_group->addAnimation(anim_old_pos);
|
||||
animation_group->addAnimation(anim_new_pos);
|
||||
animation_group->addAnimation(anim_new_opacity);
|
||||
animation_group->addAnimation(button_anim_sequence);
|
||||
|
||||
connect(animation_group, &QAbstractAnimation::finished, this, [this, current_widget, next_widget, id]() {
|
||||
ui->stackedWidget->setCurrentIndex(id);
|
||||
|
||||
// Clean up graphics effects to return control to the stylesheet
|
||||
next_widget->setGraphicsEffect(nullptr);
|
||||
// Ensure the old widget is hidden and reset for next time
|
||||
current_widget->hide();
|
||||
current_widget->move(0, 0);
|
||||
|
||||
is_animating = false;
|
||||
for (auto button : tab_button_group->buttons()) {
|
||||
button->setEnabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
is_animating = true;
|
||||
for (auto button : tab_button_group->buttons()) {
|
||||
button->setEnabled(false);
|
||||
}
|
||||
animation_group->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ signals:
|
||||
|
||||
private slots:
|
||||
void SetUIPositioning(const QString& positioning);
|
||||
void AnimateTabSwitch(int id);
|
||||
|
||||
private:
|
||||
void SetConfiguration();
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QGraphicsPixmapItem>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QParallelAnimationGroup>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QSequentialAnimationGroup>
|
||||
#include <QTimer>
|
||||
#include "citron/configuration/style_animation_event_filter.h"
|
||||
#include <QMessageBox>
|
||||
#include <QMetaObject>
|
||||
#include <QProgressDialog>
|
||||
@@ -127,47 +133,52 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st
|
||||
UpdateTheme();
|
||||
connect(rainbow_timer, &QTimer::timeout, this, &ConfigurePerGame::UpdateTheme);
|
||||
|
||||
auto* animation_filter = new StyleAnimationEventFilter(this);
|
||||
|
||||
button_group = new QButtonGroup(this);
|
||||
button_group->setExclusive(true);
|
||||
|
||||
const auto add_tab = [&](QWidget* widget, const QString& title) {
|
||||
const auto add_tab = [&](QWidget* widget, const QString& title, int id) {
|
||||
auto button = new QPushButton(title, this);
|
||||
button->setCheckable(true);
|
||||
button->setObjectName(QStringLiteral("tabButton"));
|
||||
button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
|
||||
// This object name matches the stylesheet ID selector `QPushButton#aestheticTabButton`
|
||||
button->setObjectName(QStringLiteral("aestheticTabButton"));
|
||||
// This custom property is used by the event filter for the animated style
|
||||
button->setProperty("class", QStringLiteral("tabButton")); // Keep class for animation
|
||||
button->installEventFilter(animation_filter);
|
||||
|
||||
ui->tabButtonsLayout->addWidget(button);
|
||||
button_group->addButton(button);
|
||||
button_group->addButton(button, id);
|
||||
|
||||
QScrollArea* scroll_area = new QScrollArea(this);
|
||||
scroll_area->setWidgetResizable(true);
|
||||
scroll_area->setWidget(widget);
|
||||
ui->stackedWidget->addWidget(scroll_area);
|
||||
|
||||
connect(button, &QPushButton::clicked, this, [this, scroll_area]() {
|
||||
ui->stackedWidget->setCurrentWidget(scroll_area);
|
||||
});
|
||||
};
|
||||
|
||||
add_tab(addons_tab.get(), tr("Add-Ons"));
|
||||
add_tab(cheats_tab.get(), tr("Cheats"));
|
||||
add_tab(system_tab.get(), tr("System"));
|
||||
add_tab(cpu_tab.get(), tr("CPU"));
|
||||
add_tab(graphics_tab.get(), tr("Graphics"));
|
||||
add_tab(graphics_advanced_tab.get(), tr("Adv. Graphics"));
|
||||
add_tab(audio_tab.get(), tr("Audio"));
|
||||
add_tab(input_tab.get(), tr("Input Profiles"));
|
||||
int tab_id = 0;
|
||||
add_tab(addons_tab.get(), tr("Add-Ons"), tab_id++);
|
||||
add_tab(cheats_tab.get(), tr("Cheats"), tab_id++);
|
||||
add_tab(system_tab.get(), tr("System"), tab_id++);
|
||||
add_tab(cpu_tab.get(), tr("CPU"), tab_id++);
|
||||
add_tab(graphics_tab.get(), tr("Graphics"), tab_id++);
|
||||
add_tab(graphics_advanced_tab.get(), tr("Adv. Graphics"), tab_id++);
|
||||
add_tab(audio_tab.get(), tr("Audio"), tab_id++);
|
||||
add_tab(input_tab.get(), tr("Input Profiles"), tab_id++);
|
||||
#ifdef __unix__
|
||||
add_tab(linux_tab.get(), tr("Linux"));
|
||||
add_tab(linux_tab.get(), tr("Linux"), tab_id++);
|
||||
#endif
|
||||
|
||||
ui->tabButtonsLayout->addStretch();
|
||||
|
||||
if (auto first_button = qobject_cast<QPushButton*>(button_group->buttons().first())) {
|
||||
connect(button_group, qOverload<int>(&QButtonGroup::idClicked), this, &ConfigurePerGame::AnimateTabSwitch);
|
||||
|
||||
if (auto first_button = qobject_cast<QPushButton*>(button_group->button(0))) {
|
||||
first_button->setChecked(true);
|
||||
first_button->click();
|
||||
ui->stackedWidget->setCurrentIndex(0);
|
||||
}
|
||||
|
||||
|
||||
setFocusPolicy(Qt::ClickFocus);
|
||||
setWindowTitle(tr("Properties"));
|
||||
addons_tab->SetTitleId(title_id);
|
||||
@@ -731,3 +742,72 @@ void ConfigurePerGame::OnTrimXCI() {
|
||||
tr("Failed to trim XCI file:\n%1").arg(error_message));
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurePerGame::AnimateTabSwitch(int id) {
|
||||
static bool is_animating = false;
|
||||
if (is_animating) {
|
||||
return;
|
||||
}
|
||||
|
||||
QWidget* current_widget = ui->stackedWidget->currentWidget();
|
||||
QWidget* next_widget = ui->stackedWidget->widget(id);
|
||||
|
||||
if (current_widget == next_widget || !current_widget || !next_widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int duration = 350;
|
||||
|
||||
// Prepare Widgets for Live Animation
|
||||
next_widget->setGeometry(0, 0, ui->stackedWidget->width(), ui->stackedWidget->height());
|
||||
next_widget->move(0, 0);
|
||||
next_widget->show();
|
||||
next_widget->raise();
|
||||
|
||||
// Animate OLD widget: SLIDE LEFT
|
||||
auto anim_old_pos = new QPropertyAnimation(current_widget, "pos");
|
||||
anim_old_pos->setEndValue(QPoint(-ui->stackedWidget->width(), 0));
|
||||
anim_old_pos->setDuration(duration);
|
||||
anim_old_pos->setEasingCurve(QEasingCurve::InOutQuart);
|
||||
|
||||
// Animate NEW widget: SLIDE IN FROM RIGHT and FADE IN
|
||||
auto anim_new_pos = new QPropertyAnimation(next_widget, "pos");
|
||||
anim_new_pos->setStartValue(QPoint(ui->stackedWidget->width(), 0));
|
||||
anim_new_pos->setEndValue(QPoint(0, 0));
|
||||
anim_new_pos->setDuration(duration);
|
||||
anim_new_pos->setEasingCurve(QEasingCurve::InOutQuart);
|
||||
|
||||
auto new_opacity_effect = new QGraphicsOpacityEffect(next_widget);
|
||||
next_widget->setGraphicsEffect(new_opacity_effect);
|
||||
auto anim_new_opacity = new QPropertyAnimation(new_opacity_effect, "opacity");
|
||||
anim_new_opacity->setStartValue(0.0);
|
||||
anim_new_opacity->setEndValue(1.0);
|
||||
anim_new_opacity->setDuration(duration);
|
||||
anim_new_opacity->setEasingCurve(QEasingCurve::InQuad);
|
||||
|
||||
// Group, Run, and Clean Up
|
||||
auto animation_group = new QParallelAnimationGroup(this);
|
||||
animation_group->addAnimation(anim_old_pos);
|
||||
animation_group->addAnimation(anim_new_pos);
|
||||
animation_group->addAnimation(anim_new_opacity);
|
||||
|
||||
// Use a context-aware connection to prevent crashes
|
||||
connect(animation_group, &QAbstractAnimation::finished, this, [this, current_widget, next_widget, id]() {
|
||||
ui->stackedWidget->setCurrentIndex(id);
|
||||
|
||||
next_widget->setGraphicsEffect(nullptr);
|
||||
current_widget->hide();
|
||||
current_widget->move(0, 0);
|
||||
|
||||
is_animating = false;
|
||||
for (auto button : button_group->buttons()) {
|
||||
button->setEnabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
is_animating = true;
|
||||
for (auto button : button_group->buttons()) {
|
||||
button->setEnabled(false);
|
||||
}
|
||||
animation_group->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ protected:
|
||||
void changeEvent(QEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
private slots:
|
||||
void AnimateTabSwitch(int id);
|
||||
|
||||
private:
|
||||
void RetranslateUI();
|
||||
void HandleApplyButtonClicked();
|
||||
|
||||
@@ -96,34 +96,33 @@
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
QPushButton#tabButton {
|
||||
background-color: %%BUTTON_BG_COLOR%%;
|
||||
QPushButton#aestheticTabButton {
|
||||
background-color: transparent;
|
||||
color: %%TEXT_COLOR%%;
|
||||
padding: 8px 18px;
|
||||
margin-right: 2px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom: none;
|
||||
min-width: 100px;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
padding-left: 20px; /* Indent text from the left */
|
||||
border-radius: 8px;
|
||||
font-weight: bold;
|
||||
font-size: 10pt;
|
||||
border: 1px solid %%SECONDARY_BG_COLOR%%;
|
||||
text-align: left center; /* Align text to the left like the original */
|
||||
min-width: 110px; /* Stricter minimum width to prevent smudging */
|
||||
max-height: 25px; /* Enforce a consistent height */
|
||||
}
|
||||
|
||||
QPushButton#tabButton:checked {
|
||||
background-color: %%ACCENT_COLOR%%;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
border-color: %%ACCENT_COLOR%%;
|
||||
QPushButton#aestheticTabButton:checked {
|
||||
background-color: %%SECONDARY_BG_COLOR%%; /* Add subtle background when checked */
|
||||
color: %%TEXT_COLOR%%; /* Keep text color consistent when checked */
|
||||
border-left: 3px solid %%ACCENT_COLOR%%; /* Accent color bar on the LEFT */
|
||||
padding-left: 17px; /* Adjust padding to keep text aligned perfectly */
|
||||
}
|
||||
|
||||
QPushButton#tabButton:hover:!checked {
|
||||
QPushButton#aestheticTabButton:hover:!checked {
|
||||
background-color: %%HOVER_BG_COLOR%%;
|
||||
border-color: %%TERTIARY_BG_COLOR%%;
|
||||
}
|
||||
|
||||
QPushButton#tabButton:pressed {
|
||||
background-color: %%ACCENT_COLOR_PRESSED%%;
|
||||
QPushButton#aestheticTabButton:pressed {
|
||||
background-color: %%TERTIARY_BG_COLOR%%;
|
||||
}
|
||||
|
||||
QGroupBox {
|
||||
|
||||
@@ -231,9 +231,9 @@ void ConfigureUi::resizeEvent(QResizeEvent* event) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (width() < 550 && !isCompact) {
|
||||
if (width() < 850 && !isCompact) {
|
||||
switchToCompactLayout();
|
||||
} else if (width() >= 550 && isCompact) {
|
||||
} else if (width() >= 850 && isCompact) {
|
||||
switchToWideLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,44 +131,27 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="accentColorLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="accentColorButton">
|
||||
<property name="text">
|
||||
<string>Choose Color...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QCheckBox" name="rainbowModeCheckBox">
|
||||
<property name="text">
|
||||
<string>Enable Rainbow Mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>UI Positioning</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<item row="5" column="1">
|
||||
<widget class="QComboBox" name="ui_positioning_combo">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
@@ -410,14 +393,14 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<layout class="QHBoxLayout" name="resolutionLayout">
|
||||
<layout class="QVBoxLayout" name="resolutionLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="screenshot_width">
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
|
||||
Reference in New Issue
Block a user