Browse Source
[android] Add quick settings menu to emulation fragment (#3342)
[android] Add quick settings menu to emulation fragment (#3342)
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3342 Reviewed-by: DraVee <dravee@eden-emu.dev> Reviewed-by: CamilleLaVey <camillelavey99@gmail.com> Co-authored-by: Nekle <224100951+ne-kle@users.noreply.github.com> Co-committed-by: Nekle <224100951+ne-kle@users.noreply.github.com>lizzie/audio-remove-recursive-lock
committed by
crueter
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
9 changed files with 588 additions and 7 deletions
-
208src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt
-
141src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
-
23src/android/app/src/main/res/layout/fragment_emulation.xml
-
5src/android/app/src/main/res/layout/item_quick_settings_divider.xml
-
140src/android/app/src/main/res/layout/item_quick_settings_menu.xml
-
37src/android/app/src/main/res/layout/item_quick_settings_status.xml
-
34src/android/app/src/main/res/layout/layout_quick_settings.xml
-
5src/android/app/src/main/res/menu/menu_in_game.xml
-
2src/android/app/src/main/res/values/strings.xml
@ -0,0 +1,208 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
package org.yuzu.yuzu_emu.dialogs |
|||
|
|||
import android.view.LayoutInflater |
|||
import android.view.MotionEvent |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import android.widget.RadioGroup |
|||
import android.widget.TextView |
|||
import androidx.drawerlayout.widget.DrawerLayout |
|||
import com.google.android.material.color.MaterialColors |
|||
import org.yuzu.yuzu_emu.R |
|||
import org.yuzu.yuzu_emu.YuzuApplication |
|||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting |
|||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting |
|||
import org.yuzu.yuzu_emu.fragments.EmulationFragment |
|||
import org.yuzu.yuzu_emu.utils.NativeConfig |
|||
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting |
|||
import org.yuzu.yuzu_emu.features.settings.model.AbstractShortSetting |
|||
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting |
|||
|
|||
class QuickSettings(val emulationFragment: EmulationFragment) { |
|||
// Kinda a crappy workaround to get a title from setting keys |
|||
// Idk how to do this witthout hardcoding every single one |
|||
private fun getSettingTitle(settingKey: String): String { |
|||
return settingKey.replace("_", " ").split(" ") |
|||
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } |
|||
|
|||
} |
|||
|
|||
private fun saveSettings() { |
|||
if (emulationFragment.shouldUseCustom) { |
|||
NativeConfig.savePerGameConfig() |
|||
} else { |
|||
NativeConfig.saveGlobalConfig() |
|||
} |
|||
} |
|||
|
|||
fun addPerGameConfigStatusIndicator(container: ViewGroup) { |
|||
val inflater = LayoutInflater.from(emulationFragment.requireContext()) |
|||
val statusView = inflater.inflate(R.layout.item_quick_settings_status, container, false) |
|||
|
|||
val statusIcon = statusView.findViewById<android.widget.ImageView>(R.id.status_icon) |
|||
val statusText = statusView.findViewById<TextView>(R.id.status_text) |
|||
|
|||
statusIcon.setImageResource(R.drawable.ic_settings_outline) |
|||
statusText.text = emulationFragment.getString(R.string.using_per_game_config) |
|||
statusText.setTextColor( |
|||
MaterialColors.getColor( |
|||
statusText, |
|||
com.google.android.material.R.attr.colorPrimary |
|||
) |
|||
) |
|||
|
|||
container.addView(statusView) |
|||
} |
|||
|
|||
// settings |
|||
|
|||
fun addIntSetting( |
|||
container: ViewGroup, |
|||
setting: IntSetting, |
|||
namesArrayId: Int, |
|||
valuesArrayId: Int |
|||
) { |
|||
val inflater = LayoutInflater.from(emulationFragment.requireContext()) |
|||
val itemView = inflater.inflate(R.layout.item_quick_settings_menu, container, false) |
|||
val headerView = itemView.findViewById<ViewGroup>(R.id.setting_header) |
|||
val titleView = itemView.findViewById<TextView>(R.id.setting_title) |
|||
val valueView = itemView.findViewById<TextView>(R.id.setting_value) |
|||
val expandIcon = itemView.findViewById<android.widget.ImageView>(R.id.expand_icon) |
|||
val radioGroup = itemView.findViewById<RadioGroup>(R.id.radio_group) |
|||
|
|||
titleView.text = getSettingTitle(setting.key) |
|||
|
|||
val names = emulationFragment.resources.getStringArray(namesArrayId) |
|||
val values = emulationFragment.resources.getIntArray(valuesArrayId) |
|||
val currentIndex = values.indexOf(setting.getInt()) |
|||
|
|||
valueView.text = if (currentIndex >= 0) names[currentIndex] else "Null" |
|||
headerView.visibility = View.VISIBLE |
|||
|
|||
var isExpanded = false |
|||
names.forEachIndexed { index, name -> |
|||
val radioButton = com.google.android.material.radiobutton.MaterialRadioButton(emulationFragment.requireContext()) |
|||
radioButton.text = name |
|||
radioButton.id = View.generateViewId() |
|||
radioButton.isChecked = index == currentIndex |
|||
radioButton.setPadding(16, 8, 16, 8) |
|||
|
|||
radioButton.setOnCheckedChangeListener { _, isChecked -> |
|||
if (isChecked) { |
|||
setting.setInt(values[index]) |
|||
saveSettings() |
|||
valueView.text = name |
|||
} |
|||
} |
|||
radioGroup.addView(radioButton) |
|||
} |
|||
|
|||
headerView.setOnClickListener { |
|||
isExpanded = !isExpanded |
|||
if (isExpanded) { |
|||
radioGroup.visibility = View.VISIBLE |
|||
expandIcon.animate().rotation(180f).setDuration(200).start() |
|||
} else { |
|||
radioGroup.visibility = View.GONE |
|||
expandIcon.animate().rotation(0f).setDuration(200).start() |
|||
} |
|||
} |
|||
|
|||
container.addView(itemView) |
|||
} |
|||
|
|||
fun addBooleanSetting( |
|||
container: ViewGroup, |
|||
setting: BooleanSetting |
|||
) { |
|||
val inflater = LayoutInflater.from(emulationFragment.requireContext()) |
|||
val itemView = inflater.inflate(R.layout.item_quick_settings_menu, container, false) |
|||
|
|||
val switchContainer = itemView.findViewById<ViewGroup>(R.id.switch_container) |
|||
val titleView = itemView.findViewById<TextView>(R.id.switch_title) |
|||
val switchView = itemView.findViewById<com.google.android.material.materialswitch.MaterialSwitch>(R.id.setting_switch) |
|||
|
|||
titleView.text = getSettingTitle(setting.key) |
|||
switchContainer.visibility = View.VISIBLE |
|||
switchView.isChecked = setting.getBoolean() |
|||
|
|||
switchView.setOnCheckedChangeListener { _, isChecked -> |
|||
setting.setBoolean(isChecked) |
|||
saveSettings() |
|||
} |
|||
|
|||
switchContainer.setOnClickListener { |
|||
switchView.toggle() |
|||
} |
|||
container.addView(itemView) |
|||
} |
|||
|
|||
fun addSliderSetting( |
|||
container: ViewGroup, |
|||
setting: AbstractSetting, |
|||
minValue: Int = 0, |
|||
maxValue: Int = 100, |
|||
units: String = "" |
|||
) { |
|||
val inflater = LayoutInflater.from(emulationFragment.requireContext()) |
|||
val itemView = inflater.inflate(R.layout.item_quick_settings_menu, container, false) |
|||
|
|||
val sliderContainer = itemView.findViewById<ViewGroup>(R.id.slider_container) |
|||
val titleView = itemView.findViewById<TextView>(R.id.slider_title) |
|||
val valueDisplay = itemView.findViewById<TextView>(R.id.slider_value_display) |
|||
val slider = itemView.findViewById<com.google.android.material.slider.Slider>(R.id.setting_slider) |
|||
|
|||
|
|||
titleView.text = getSettingTitle(setting.key) |
|||
sliderContainer.visibility = View.VISIBLE |
|||
|
|||
slider.valueFrom = minValue.toFloat() |
|||
slider.valueTo = maxValue.toFloat() |
|||
slider.stepSize = 1f |
|||
val currentValue = when (setting) { |
|||
is AbstractShortSetting -> setting.getShort(needsGlobal = false).toInt() |
|||
is AbstractIntSetting -> setting.getInt(needsGlobal = false) |
|||
else -> 0 |
|||
} |
|||
slider.value = currentValue.toFloat().coerceIn(minValue.toFloat(), maxValue.toFloat()) |
|||
|
|||
val displayValue = "${slider.value.toInt()}$units" |
|||
valueDisplay.text = displayValue |
|||
|
|||
slider.addOnChangeListener { _, value, chanhed -> |
|||
if (chanhed) { |
|||
val intValue = value.toInt() |
|||
when (setting) { |
|||
is AbstractShortSetting -> setting.setShort(intValue.toShort()) |
|||
is AbstractIntSetting -> setting.setInt(intValue) |
|||
} |
|||
saveSettings() |
|||
valueDisplay.text = "$intValue$units" |
|||
} |
|||
} |
|||
|
|||
slider.setOnTouchListener { _, event -> |
|||
val drawer = emulationFragment.view?.findViewById<DrawerLayout>(R.id.drawer_layout) |
|||
when (event.action) { |
|||
MotionEvent.ACTION_DOWN -> { |
|||
drawer?.requestDisallowInterceptTouchEvent(true) |
|||
} |
|||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { |
|||
drawer?.requestDisallowInterceptTouchEvent(false) |
|||
} |
|||
} |
|||
false |
|||
} |
|||
|
|||
container.addView(itemView) |
|||
} |
|||
|
|||
fun addDivider(container: ViewGroup) { |
|||
val inflater = LayoutInflater.from(emulationFragment.requireContext()) |
|||
val dividerView = inflater.inflate(R.layout.item_quick_settings_divider, container, false) |
|||
container.addView(dividerView) |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<View xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="1dp" |
|||
android:background="?android:attr/listDivider" /> |
|||
@ -0,0 +1,140 @@ |
|||
<?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"> |
|||
|
|||
<LinearLayout |
|||
android:id="@+id/setting_header" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="horizontal" |
|||
android:paddingStart="24dp" |
|||
android:paddingEnd="24dp" |
|||
android:paddingTop="12dp" |
|||
android:paddingBottom="8dp" |
|||
android:background="?attr/selectableItemBackground" |
|||
android:clickable="true" |
|||
android:focusable="true" |
|||
android:visibility="gone"> |
|||
|
|||
<LinearLayout |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_weight="1" |
|||
android:orientation="vertical"> |
|||
|
|||
<com.google.android.material.textview.MaterialTextView |
|||
android:id="@+id/setting_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/setting_value" |
|||
style="@style/TextAppearance.Material3.BodySmall" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginTop="4dp" |
|||
android:textColor="?attr/colorOnSurfaceVariant" /> |
|||
|
|||
</LinearLayout> |
|||
|
|||
<ImageView |
|||
android:id="@+id/expand_icon" |
|||
android:layout_width="24dp" |
|||
android:layout_height="24dp" |
|||
android:layout_gravity="center_vertical" |
|||
android:layout_marginStart="16dp" |
|||
android:src="@drawable/ic_dropdown_arrow" |
|||
android:contentDescription="" |
|||
app:tint="?attr/colorPrimary" /> |
|||
|
|||
</LinearLayout> |
|||
|
|||
<RadioGroup |
|||
android:id="@+id/radio_group" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:paddingStart="24dp" |
|||
android:paddingEnd="24dp" |
|||
android:paddingBottom="8dp" |
|||
android:visibility="gone" /> |
|||
|
|||
<LinearLayout |
|||
android:id="@+id/slider_container" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="vertical" |
|||
android:paddingStart="24dp" |
|||
android:paddingEnd="24dp" |
|||
android:paddingTop="12dp" |
|||
android:paddingBottom="8dp" |
|||
android:background="?attr/selectableItemBackground" |
|||
android:clickable="true" |
|||
android:focusable="true" |
|||
android:visibility="gone"> |
|||
|
|||
<LinearLayout |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="horizontal"> |
|||
|
|||
<com.google.android.material.textview.MaterialTextView |
|||
android:id="@+id/slider_title" |
|||
style="@style/TextAppearance.Material3.TitleSmall" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_weight="1" /> |
|||
|
|||
<com.google.android.material.textview.MaterialTextView |
|||
android:id="@+id/slider_value_display" |
|||
style="@style/TextAppearance.Material3.BodySmall" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:textColor="?attr/colorOnSurfaceVariant" /> |
|||
|
|||
</LinearLayout> |
|||
|
|||
<com.google.android.material.slider.Slider |
|||
android:id="@+id/setting_slider" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginTop="8dp" |
|||
android:valueFrom="0" |
|||
android:valueTo="100" |
|||
android:stepSize="1" /> |
|||
|
|||
</LinearLayout> |
|||
|
|||
<LinearLayout |
|||
android:id="@+id/switch_container" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="horizontal" |
|||
android:paddingStart="24dp" |
|||
android:paddingEnd="24dp" |
|||
android:paddingTop="12dp" |
|||
android:paddingBottom="8dp" |
|||
android:background="?attr/selectableItemBackground" |
|||
android:clickable="true" |
|||
android:focusable="true" |
|||
android:visibility="gone"> |
|||
|
|||
<com.google.android.material.textview.MaterialTextView |
|||
android:id="@+id/switch_title" |
|||
style="@style/TextAppearance.Material3.TitleSmall" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_weight="1" /> |
|||
|
|||
<com.google.android.material.materialswitch.MaterialSwitch |
|||
android:id="@+id/setting_switch" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:minHeight="48dp" /> |
|||
|
|||
</LinearLayout> |
|||
|
|||
</LinearLayout> |
|||
@ -0,0 +1,37 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<com.google.android.material.card.MaterialCardView 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_margin="16dp" |
|||
app:cardElevation="2dp" |
|||
app:cardCornerRadius="12dp" |
|||
app:strokeWidth="1dp" |
|||
app:strokeColor="?attr/colorPrimary"> |
|||
|
|||
<LinearLayout |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="horizontal" |
|||
android:gravity="center_vertical" |
|||
android:padding="16dp"> |
|||
|
|||
<ImageView |
|||
android:id="@+id/status_icon" |
|||
android:layout_width="24dp" |
|||
android:layout_height="24dp" |
|||
android:src="@drawable/ic_settings_outline" |
|||
android:contentDescription="" /> |
|||
|
|||
<com.google.android.material.textview.MaterialTextView |
|||
android:id="@+id/status_text" |
|||
style="@style/TextAppearance.Material3.TitleSmall" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_weight="1" |
|||
android:layout_marginStart="12dp" |
|||
android:textColor="?attr/colorPrimary" /> |
|||
|
|||
</LinearLayout> |
|||
|
|||
</com.google.android.material.card.MaterialCardView> |
|||
@ -0,0 +1,34 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<ScrollView 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="match_parent" |
|||
android:fillViewport="true" |
|||
android:fitsSystemWindows="true"> |
|||
|
|||
<LinearLayout |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="vertical" |
|||
android:fitsSystemWindows="true"> |
|||
|
|||
<com.google.android.material.appbar.MaterialToolbar |
|||
android:id="@+id/quick_settings_toolbar" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="?attr/actionBarSize" |
|||
android:elevation="0dp" |
|||
app:title="@string/quick_settings" |
|||
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" |
|||
android:fitsSystemWindows="true" /> |
|||
|
|||
<LinearLayout |
|||
android:id="@+id/quick_settings_container" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="vertical" |
|||
android:paddingTop="8dp" |
|||
android:paddingBottom="16dp" /> |
|||
|
|||
</LinearLayout> |
|||
|
|||
</ScrollView> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue