diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt index 0d523a4e18..fa11d0e894 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt @@ -341,17 +341,99 @@ class QuickSettings(val emulationFragment: EmulationFragment) { container.addView(itemView) } + fun addShaderCard( + title: String, + summary: String, + container: ViewGroup, + onRemove: () -> Unit + ): ViewGroup { + val inflater = LayoutInflater.from(emulationFragment.requireContext()) + val itemView = inflater.inflate(R.layout.item_quick_settings_shader, container, false) + + val headerView = itemView.findViewById(R.id.shader_header) + val titleView = itemView.findViewById(R.id.shader_title) + val summaryView = itemView.findViewById(R.id.shader_summary) + val removeView = itemView.findViewById(R.id.shader_remove) + val expandIcon = itemView.findViewById(R.id.shader_expand) + val bodyView = itemView.findViewById(R.id.shader_body) + + titleView.text = title + if (summary.isEmpty()) { + summaryView.visibility = View.GONE + } else { + summaryView.text = summary + } + + var isExpanded = false + headerView.setOnClickListener { + isExpanded = !isExpanded + if (isExpanded) { + bodyView.visibility = View.VISIBLE + expandIcon.animate().rotation(180f).setDuration(200).start() + } else { + bodyView.visibility = View.GONE + expandIcon.animate().rotation(0f).setDuration(200).start() + } + } + + removeView.setOnClickListener { + onRemove() + } + + container.addView(itemView) + return bodyView + } + + fun addEffectPicker( + container: ViewGroup, + choices: List, + onPicked: (Int) -> Unit + ) { + val inflater = LayoutInflater.from(emulationFragment.requireContext()) + val itemView = inflater.inflate(R.layout.item_quick_settings_add, container, false) + + val button = itemView.findViewById( + R.id.add_button + ) + val choiceGroup = itemView.findViewById(R.id.add_choices) + + choices.forEachIndexed { index, name -> + val radioButton = com.google.android.material.radiobutton.MaterialRadioButton( + emulationFragment.requireContext() + ) + radioButton.text = name + radioButton.id = View.generateViewId() + radioButton.setPadding(16, 8, 16, 8) + radioButton.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + onPicked(index) + } + } + choiceGroup.addView(radioButton) + } + + var isOpen = false + button.setOnClickListener { + isOpen = !isOpen + if (isOpen) { + choiceGroup.visibility = View.VISIBLE + } else { + choiceGroup.visibility = View.GONE + } + } + + container.addView(itemView) + } + fun addPostProcessing(container: ViewGroup, onStructureChanged: () -> Unit) { val usable = NativePostProcessing.catalog().filter { it.valid } if (usable.isEmpty()) { return } - val labels = mutableListOf( - YuzuApplication.appContext.getString(R.string.post_processing_none) - ) - val files = mutableListOf("") - val techniques = mutableListOf("") + val labels = mutableListOf() + val files = mutableListOf() + val techniques = mutableListOf() for (effect in usable) { for (technique in effect.techniques) { @@ -368,61 +450,37 @@ class QuickSettings(val emulationFragment: EmulationFragment) { addDivider(container) val chain = NativePostProcessing.chain() - for (index in 0..chain.size) { - var selected = 0 - var effect: NativePostProcessing.Effect? = null + chain.forEachIndexed { index, entry -> + val effect = usable.firstOrNull { it.file == entry.file } - if (index < chain.size) { - val entry = chain[index] - effect = usable.firstOrNull { it.file == entry.file } - for (i in files.indices) { - if (files[i] == entry.file && techniques[i] == entry.technique) { - selected = i - } + var title = entry.file + var summary = "" + if (effect != null) { + title = effect.label + if (effect.techniques.size > 1) { + title = effect.label + " \u00b7 " + entry.technique } + summary = effect.description } - var title = YuzuApplication.appContext.getString(R.string.post_processing_add) - if (index < chain.size) { - title = YuzuApplication.appContext.getString(R.string.post_processing_effect) - } - - addChoice(title, container, labels, selected) { picked -> - applyEffectPick(index, picked, files, techniques, chain.size) + val body = addShaderCard(title, summary, container) { + NativePostProcessing.remove(index) + NativePostProcessing.persist() onStructureChanged() } if (effect != null) { for (uniform in effect.uniforms) { - addUniformSliders(container, index, uniform) + addUniformSliders(body, index, uniform) } } } - } - private fun applyEffectPick( - index: Int, - picked: Int, - files: List, - techniques: List, - chainSize: Int - ) { - if (index >= chainSize) { - if (picked > 0) { - NativePostProcessing.append(files[picked], techniques[picked]) - NativePostProcessing.persist() - } - return - } - - if (picked == 0) { - NativePostProcessing.remove(index) + addEffectPicker(container, labels) { picked -> + NativePostProcessing.append(files[picked], techniques[picked]) NativePostProcessing.persist() - return + onStructureChanged() } - - NativePostProcessing.replace(index, files[picked], techniques[picked]) - NativePostProcessing.persist() } private fun addUniformSliders( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 44ac01301b..48c0c4b093 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -48,6 +48,8 @@ class SettingsFragmentPresenter( ) { private var settingsList = ArrayList() + private val expandedShaderSlots = mutableSetOf() + private val context get() = YuzuApplication.appContext // Extension for altering settings list based on each setting's properties @@ -234,15 +236,42 @@ class SettingsFragmentPresenter( var summary = "" if (effect != null) { header = effect.label + if (effect.techniques.size > 1) { + header = effect.label + " \u00b7 " + entry.technique + } summary = effect.description } - add(HeaderSetting(titleString = header)) + + val isOpen = expandedShaderSlots.contains(index) + var chevron = R.drawable.ic_arrow_forward + if (isOpen) { + chevron = R.drawable.ic_dropdown_arrow + } + + add( + RunnableSetting( + titleString = header, + descriptionString = summary, + isRunnable = true, + iconId = chevron + ) { + if (isOpen) { + expandedShaderSlots.remove(index) + } else { + expandedShaderSlots.add(index) + } + settingsViewModel.setReloadListAndNotifyDataset(true) + } + ) + + if (!isOpen) { + continue + } add( IntSingleChoiceSetting( buildSlotSelector(index, entry, files, techniques), titleId = R.string.post_processing_effect, - descriptionString = summary, choices = labels.toTypedArray(), values = labels.indices.toList().toTypedArray() ) @@ -262,6 +291,7 @@ class SettingsFragmentPresenter( ) { NativePostProcessing.move(index, -1) NativePostProcessing.persist() + expandedShaderSlots.clear() settingsViewModel.setReloadListAndNotifyDataset(true) } ) @@ -274,6 +304,7 @@ class SettingsFragmentPresenter( ) { NativePostProcessing.move(index, 1) NativePostProcessing.persist() + expandedShaderSlots.clear() settingsViewModel.setReloadListAndNotifyDataset(true) } ) @@ -295,24 +326,52 @@ class SettingsFragmentPresenter( ) { NativePostProcessing.remove(index) NativePostProcessing.persist() + expandedShaderSlots.clear() settingsViewModel.setReloadListAndNotifyDataset(true) } ) } add( - RunnableSetting( + IntSingleChoiceSetting( + buildAddSelector(files, techniques), titleId = R.string.post_processing_add, - isRunnable = true - ) { - NativePostProcessing.append(files[0], techniques[0]) - NativePostProcessing.persist() - settingsViewModel.setReloadListAndNotifyDataset(true) - } + choices = labels.toTypedArray(), + values = labels.indices.toList().toTypedArray() + ) ) } } + private fun buildAddSelector( + files: List, + techniques: List + ): AbstractIntSetting = object : AbstractIntSetting { + override val key = "fx_add" + + override fun getInt(needsGlobal: Boolean): Int = -1 + + override fun setInt(value: Int) { + if (value < 0 || value >= files.size) { + return + } + NativePostProcessing.append(files[value], techniques[value]) + NativePostProcessing.persist() + settingsViewModel.setReloadListAndNotifyDataset(true) + } + + override val defaultValue = -1 + override fun getValueAsString(needsGlobal: Boolean): String = "" + override fun reset() {} + override val isRuntimeModifiable = true + override val pairedSettingKey = "" + override val isSwitchable = false + override val isSaveable = true + override var global: Boolean + get() = true + set(_) {} + } + private fun buildSlotSelector( index: Int, entry: NativePostProcessing.ChainEntry, diff --git a/src/android/app/src/main/res/drawable/shader_card_background.xml b/src/android/app/src/main/res/drawable/shader_card_background.xml new file mode 100644 index 0000000000..40e840695a --- /dev/null +++ b/src/android/app/src/main/res/drawable/shader_card_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/src/android/app/src/main/res/layout/item_quick_settings_add.xml b/src/android/app/src/main/res/layout/item_quick_settings_add.xml new file mode 100644 index 0000000000..66b89cf576 --- /dev/null +++ b/src/android/app/src/main/res/layout/item_quick_settings_add.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/src/android/app/src/main/res/layout/item_quick_settings_shader.xml b/src/android/app/src/main/res/layout/item_quick_settings_shader.xml new file mode 100644 index 0000000000..dac3bb4de8 --- /dev/null +++ b/src/android/app/src/main/res/layout/item_quick_settings_shader.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 65dbf2e7e6..ab9aaf106e 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -302,7 +302,6 @@ ReShade FX effects applied after rendering Configure the effect chain for this game Effect - None Add effect Remove Move up diff --git a/src/yuzu/configuration/configure_post_processing.cpp b/src/yuzu/configuration/configure_post_processing.cpp index 79b43ccec5..8b1d4c756b 100644 --- a/src/yuzu/configuration/configure_post_processing.cpp +++ b/src/yuzu/configuration/configure_post_processing.cpp @@ -5,13 +5,15 @@ #include #include +#include #include #include #include +#include #include -#include #include #include +#include #include #include #include @@ -83,16 +85,23 @@ ConfigurePostProcessing::ConfigurePostProcessing(QWidget* parent) : QDialog(pare auto* actions = new QHBoxLayout(); auto* add_button = new QPushButton(tr("Add Effect"), this); - connect(add_button, &QPushButton::clicked, this, [this]() { - for (const auto& effect : VideoCore::GetFxCatalog()) { - if (!effect.Valid()) { - continue; - } - VideoCore::FxChain::Instance().Append(effect.file, effect.techniques.front()); - ApplyStructuralChange(); - return; + auto* add_menu = new QMenu(add_button); + for (const auto& effect : VideoCore::GetFxCatalog()) { + if (!effect.Valid()) { + continue; } - }); + for (const auto& technique : effect.techniques) { + QAction* action = add_menu->addAction(SlotLabel(effect, technique)); + action->setToolTip(QString::fromStdString(effect.description)); + const std::string file = effect.file; + const std::string name = technique; + connect(action, &QAction::triggered, this, [this, file, name]() { + VideoCore::FxChain::Instance().Append(file, name); + ApplyStructuralChange(); + }); + } + } + add_button->setMenu(add_menu); actions->addWidget(add_button); actions->addStretch(); @@ -235,11 +244,21 @@ void ConfigurePostProcessing::BuildUniformWidget(QWidget* parent, QVBoxLayout* l } QWidget* ConfigurePostProcessing::BuildSlot(int index, const VideoCore::FxChainEntry& entry) { - auto* group = new QGroupBox(slots_container); + auto* group = new QFrame(slots_container); + group->setObjectName(QStringLiteral("fxSlot")); + group->setStyleSheet(QStringLiteral( + "QFrame#fxSlot { background-color: palette(alternate-base);" + " border: 1px solid palette(mid); border-radius: 6px; }")); auto* layout = new QVBoxLayout(group); auto* header = new QHBoxLayout(); + auto* toggle = new QToolButton(group); + toggle->setArrowType(Qt::RightArrow); + toggle->setAutoRaise(true); + toggle->setCheckable(true); + header->addWidget(toggle); + auto* combo = new QComboBox(group); PopulateEffectCombo(combo, entry); connect(combo, &QComboBox::currentIndexChanged, this, [this, index, combo](int) { @@ -292,13 +311,28 @@ QWidget* ConfigurePostProcessing::BuildSlot(int index, const VideoCore::FxChainE layout->addLayout(header); + auto* body = new QWidget(group); + auto* body_layout = new QVBoxLayout(body); + body_layout->setContentsMargins(0, 0, 0, 0); + body->setVisible(false); + layout->addWidget(body); + + connect(toggle, &QToolButton::toggled, this, [toggle, body](bool open) { + body->setVisible(open); + if (open) { + toggle->setArrowType(Qt::DownArrow); + } else { + toggle->setArrowType(Qt::RightArrow); + } + }); + const VideoCore::FxEffectDesc* effect = VideoCore::FindFxEffect(entry.file); if (effect == nullptr) { auto* missing = new QLabel(tr("Effect '%1' was not found.").arg(QString::fromStdString(entry.file)), group); missing->setWordWrap(true); - layout->addWidget(missing); + body_layout->addWidget(missing); return group; } @@ -306,7 +340,7 @@ QWidget* ConfigurePostProcessing::BuildSlot(int index, const VideoCore::FxChainE auto* failed = new QLabel( tr("Effect failed to compile:\n%1").arg(QString::fromStdString(effect->error)), group); failed->setWordWrap(true); - layout->addWidget(failed); + body_layout->addWidget(failed); return group; } @@ -317,10 +351,10 @@ QWidget* ConfigurePostProcessing::BuildSlot(int index, const VideoCore::FxChainE if (!current_category.empty()) { auto* category = new QLabel(QString::fromStdString(current_category), group); category->setStyleSheet(QStringLiteral("font-weight: bold;")); - layout->addWidget(category); + body_layout->addWidget(category); } } - BuildUniformWidget(group, layout, index, uniform); + BuildUniformWidget(body, body_layout, index, uniform); } return group;