diff --git a/src/citron/configuration/configure_dialog.cpp b/src/citron/configuration/configure_dialog.cpp index 3e60d1cae..81becbc23 100644 --- a/src/citron/configuration/configure_dialog.cpp +++ b/src/citron/configuration/configure_dialog.cpp @@ -7,11 +7,16 @@ #include #include #include +#include +#include #include +#include +#include #include #include #include #include +#include #include #include #include @@ -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(&QButtonGroup::idClicked), this, [this](int id) { - ui->stackedWidget->setCurrentIndex(id); - }); + connect(tab_button_group.get(), qOverload(&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(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); +} diff --git a/src/citron/configuration/configure_dialog.h b/src/citron/configuration/configure_dialog.h index 45bbd0073..e99c6f85f 100644 --- a/src/citron/configuration/configure_dialog.h +++ b/src/citron/configuration/configure_dialog.h @@ -57,6 +57,7 @@ signals: private slots: void SetUIPositioning(const QString& positioning); + void AnimateTabSwitch(int id); private: void SetConfiguration(); diff --git a/src/citron/configuration/configure_per_game.cpp b/src/citron/configuration/configure_per_game.cpp index 22bfccb9a..9eb2324ad 100644 --- a/src/citron/configuration/configure_per_game.cpp +++ b/src/citron/configuration/configure_per_game.cpp @@ -20,6 +20,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include "citron/configuration/style_animation_event_filter.h" #include #include #include @@ -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(button_group->buttons().first())) { + connect(button_group, qOverload(&QButtonGroup::idClicked), this, &ConfigurePerGame::AnimateTabSwitch); + + if (auto first_button = qobject_cast(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); +} diff --git a/src/citron/configuration/configure_per_game.h b/src/citron/configuration/configure_per_game.h index ac6c2ac17..b40d64a3e 100644 --- a/src/citron/configuration/configure_per_game.h +++ b/src/citron/configuration/configure_per_game.h @@ -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(); diff --git a/src/citron/configuration/configure_per_game.ui b/src/citron/configuration/configure_per_game.ui index af1acd59a..01ad1aa66 100644 --- a/src/citron/configuration/configure_per_game.ui +++ b/src/citron/configuration/configure_per_game.ui @@ -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 { diff --git a/src/citron/configuration/configure_ui.cpp b/src/citron/configuration/configure_ui.cpp index 7db2f9958..9c4b16fd7 100644 --- a/src/citron/configuration/configure_ui.cpp +++ b/src/citron/configuration/configure_ui.cpp @@ -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(); } } diff --git a/src/citron/configuration/configure_ui.ui b/src/citron/configuration/configure_ui.ui index 1fa071c51..d2efbdcd2 100644 --- a/src/citron/configuration/configure_ui.ui +++ b/src/citron/configuration/configure_ui.ui @@ -131,44 +131,27 @@ - - - - - Choose Color... - - - - - - - Enable Rainbow Mode - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + Choose Color... + + - + + + + Enable Rainbow Mode + + + + UI Positioning - + @@ -410,14 +393,14 @@ - + TextLabel - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true