OH MY GOD THIS UI TORTURE

This commit is contained in:
CamilleLaVey
2026-09-08 18:20:17 -04:00
parent a986cc8565
commit 5b51a960d5
7 changed files with 344 additions and 70 deletions
@@ -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<ViewGroup>(R.id.shader_header)
val titleView = itemView.findViewById<TextView>(R.id.shader_title)
val summaryView = itemView.findViewById<TextView>(R.id.shader_summary)
val removeView = itemView.findViewById<android.widget.ImageView>(R.id.shader_remove)
val expandIcon = itemView.findViewById<android.widget.ImageView>(R.id.shader_expand)
val bodyView = itemView.findViewById<ViewGroup>(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<String>,
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<com.google.android.material.button.MaterialButton>(
R.id.add_button
)
val choiceGroup = itemView.findViewById<RadioGroup>(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<String>()
val files = mutableListOf<String>()
val techniques = mutableListOf<String>()
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<String>,
techniques: List<String>,
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(
@@ -48,6 +48,8 @@ class SettingsFragmentPresenter(
) {
private var settingsList = ArrayList<SettingsItem>()
private val expandedShaderSlots = mutableSetOf<Int>()
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<String>,
techniques: List<String>
): 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,
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/colorSurfaceVariant" />
<corners android:radius="16dp" />
<stroke
android:width="1dp"
android:color="?attr/colorOutline" />
</shape>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/add_button"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="4dp"
android:text="@string/post_processing_add"
app:icon="@drawable/ic_add" />
<RadioGroup
android:id="@+id/add_choices"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingTop="4dp"
android:paddingBottom="8dp"
android:visibility="gone" />
</LinearLayout>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:background="@drawable/shader_card_background"
android:orientation="vertical">
<LinearLayout
android:id="@+id/shader_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/shader_title"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/shader_summary"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<ImageView
android:id="@+id/shader_remove"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:padding="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_delete"
android:contentDescription="@string/post_processing_remove"
app:tint="?attr/colorOnSurfaceVariant" />
<ImageView
android:id="@+id/shader_expand"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:src="@drawable/ic_dropdown_arrow"
android:contentDescription=""
app:tint="?attr/colorPrimary" />
</LinearLayout>
<LinearLayout
android:id="@+id/shader_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="8dp"
android:visibility="gone" />
</LinearLayout>
@@ -302,7 +302,6 @@
<string name="post_processing_description">ReShade FX effects applied after rendering</string>
<string name="post_processing_per_game_description">Configure the effect chain for this game</string>
<string name="post_processing_effect">Effect</string>
<string name="post_processing_none">None</string>
<string name="post_processing_add">Add effect</string>
<string name="post_processing_remove">Remove</string>
<string name="post_processing_move_up">Move up</string>
@@ -5,13 +5,15 @@
#include <cmath>
#include <string>
#include <QAction>
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFrame>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QPushButton>
#include <QScrollArea>
#include <QSlider>
@@ -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;