Added "Small Screen Position" feature

* error checking for layout value from older config

* rename enum and update aspect ratio code

* rewrite LargeFrameLayout to support multiple positions

* add settings for smallscreenposition, fix minsize function

* fixed framebuffer from res scale (screenshots)

* add desktop UI for small screen position

* small screen position submenu on desktop

* fix int-float conversion warning

* rename Above and Below to hopefully fix linux issue

* Add Small Screen Position Setting to android settings menu

* fix sliders to work with floats, mostly

* fix android slider textinput ui

* change None enums in settings and cam_params

* Apply clang-format-18

* SettingsAdapter.kt: Make more null pointer exception resistant

* Updated license headers

* Code formatting nitpicks

* fix bug in main.ui that was hiding menu

* replace default layout with a special call to LargeFrame (like SideBySide does)

* fix bug when "large screen" is actually narrower

* edit documentation for LargeScreenLayout

* update PortraitTopFullFrameLayout to use LargeFrameLayout

* fix unary minus on unsigned int bug

* Applied formatting correction

* Added `const`s where appropriate

* android: Add mention of the bottom-right small screen position being the default

* review fixes + more constants

* refactor all Upright calculations to a reverseLayout method, simplifying code and reducing bugs

* Removed stray extra newline

* SettingsAdapter.kt: Fixed some strange indentation

* Removed unnecessary `if` in favour of direct value usage

---------

Co-authored-by: Reg Tiangha <rtiangha@users.noreply.github.com>
Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
This commit is contained in:
David Griswold
2024-10-29 14:22:51 -07:00
committed by OpenSauce04
parent 0a3cb3a4dc
commit 43c4d3981d
29 changed files with 780 additions and 454 deletions

View File

@@ -21,6 +21,23 @@ enum class ScreenLayout(val int: Int) {
}
}
enum class SmallScreenPosition(val int: Int) {
TOP_RIGHT(0),
MIDDLE_RIGHT(1),
BOTTOM_RIGHT(2),
TOP_LEFT(3),
MIDDLE_LEFT(4),
BOTTOM_LEFT(5),
ABOVE(6),
BELOW(7);
companion object {
fun from(int: Int): SmallScreenPosition {
return entries.firstOrNull { it.int == int } ?: TOP_RIGHT
}
}
}
enum class PortraitScreenLayout(val int: Int) {
// These must match what is defined in src/common/settings.h
TOP_FULL_WIDTH(0),
@@ -28,7 +45,7 @@ enum class PortraitScreenLayout(val int: Int) {
companion object {
fun from(int: Int): PortraitScreenLayout {
return entries.firstOrNull { it.int == int } ?: TOP_FULL_WIDTH;
return entries.firstOrNull { it.int == int } ?: TOP_FULL_WIDTH
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Lime3DS Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@@ -9,7 +9,7 @@ enum class FloatSetting(
override val section: String,
override val defaultValue: Float
) : AbstractFloatSetting {
// There are no float settings currently
LARGE_SCREEN_PROPORTION("large_screen_proportion",Settings.SECTION_LAYOUT,2.25f),
EMPTY_SETTING("", "", 0.0f);
override var float: Float = defaultValue

View File

@@ -24,6 +24,7 @@ enum class IntSetting(
CARDBOARD_X_SHIFT("cardboard_x_shift", Settings.SECTION_LAYOUT, 0),
CARDBOARD_Y_SHIFT("cardboard_y_shift", Settings.SECTION_LAYOUT, 0),
SCREEN_LAYOUT("layout_option", Settings.SECTION_LAYOUT, 0),
SMALL_SCREEN_POSITION("small_screen_position",Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_X("custom_top_x",Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_Y("custom_top_y",Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_WIDTH("custom_top_width",Settings.SECTION_LAYOUT,800),

View File

@@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Lime3DS Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@@ -39,5 +39,6 @@ abstract class SettingsItem(
const val TYPE_RUNNABLE = 7
const val TYPE_INPUT_BINDING = 8
const val TYPE_STRING_INPUT = 9
const val TYPE_FLOAT_INPUT = 10
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Lime3DS Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@@ -23,17 +23,16 @@ class SliderSetting(
val defaultValue: Float? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SLIDER
val selectedValue: Int
val selectedFloat: Float
get() {
val setting = setting ?: return defaultValue!!.toInt()
val setting = setting ?: return defaultValue!!.toFloat()
return when (setting) {
is AbstractIntSetting -> setting.int
is FloatSetting -> setting.float.roundToInt()
is ScaledFloatSetting -> setting.float.roundToInt()
is AbstractIntSetting -> setting.int.toFloat()
is FloatSetting -> setting.float
is ScaledFloatSetting -> setting.float
else -> {
Log.error("[SliderSetting] Error casting setting type.")
-1
-1f
}
}
}

View File

@@ -12,6 +12,7 @@ import android.icu.util.Calendar
import android.icu.util.TimeZone
import android.text.Editable
import android.text.InputFilter
import android.text.InputType
import android.text.TextWatcher
import android.text.format.DateFormat
import android.view.LayoutInflater
@@ -68,6 +69,7 @@ import org.citra.citra_emu.utils.SystemSaveGame
import java.lang.IllegalStateException
import java.lang.NumberFormatException
import java.text.SimpleDateFormat
import kotlin.math.roundToInt
class SettingsAdapter(
private val fragmentView: SettingsFragmentView,
@@ -77,7 +79,7 @@ class SettingsAdapter(
private var clickedItem: SettingsItem? = null
private var clickedPosition: Int
private var dialog: AlertDialog? = null
private var sliderProgress = 0
private var sliderProgress = 0f
private var textSliderValue: TextInputEditText? = null
private var textInputLayout: TextInputLayout? = null
private var textInputValue: String = ""
@@ -136,27 +138,23 @@ class SettingsAdapter(
}
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
holder.bind(getItem(position))
getItem(position)?.let { holder.bind(it) }
}
private fun getItem(position: Int): SettingsItem {
return settings!![position]
private fun getItem(position: Int): SettingsItem? {
return settings?.get(position)
}
override fun getItemCount(): Int {
return if (settings != null) {
settings!!.size
} else {
0
}
return settings?.size ?: 0
}
override fun getItemViewType(position: Int): Int {
return getItem(position).type
return getItem(position)?.type ?: -1
}
fun setSettingsList(settings: ArrayList<SettingsItem>?) {
this.settings = settings
this.settings = settings ?: arrayListOf()
notifyDataSetChanged()
}
@@ -182,10 +180,12 @@ class SettingsAdapter(
private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) {
clickedItem = item
dialog = MaterialAlertDialogBuilder(context)
.setTitle(item.nameId)
.setSingleChoiceItems(item.choices, item.selectValueIndex, this)
.show()
dialog = context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(item.nameId)
.setSingleChoiceItems(item.choices, item.selectValueIndex, this)
.show()
}
}
fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
@@ -231,10 +231,10 @@ class SettingsAdapter(
.build()
datePicker.addOnPositiveButtonClickListener {
timePicker.show(
(fragmentView.activityView as AppCompatActivity).supportFragmentManager,
"TimePicker"
)
val activity = fragmentView.activityView as? AppCompatActivity
activity?.supportFragmentManager?.let { fragmentManager ->
timePicker.show(fragmentManager, "TimePicker")
}
}
timePicker.addOnPositiveButtonClickListener {
var epochTime: Long = datePicker.selection!! / 1000
@@ -258,38 +258,62 @@ class SettingsAdapter(
fun onSliderClick(item: SliderSetting, position: Int) {
clickedItem = item
clickedPosition = position
sliderProgress = item.selectedValue
sliderProgress = (item.selectedFloat * 100f).roundToInt() / 100f
val inflater = LayoutInflater.from(context)
val sliderBinding = DialogSliderBinding.inflate(inflater)
textInputLayout = sliderBinding.textInput
textSliderValue = sliderBinding.textValue
textSliderValue!!.setText(sliderProgress.toString())
textInputLayout!!.suffixText = item.units
if (item.setting is FloatSetting) {
textSliderValue?.let {
it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
it.setText(sliderProgress.toString())
}
} else {
textSliderValue?.setText(sliderProgress.roundToInt().toString())
}
textInputLayout?.suffixText = item.units
sliderBinding.slider.apply {
valueFrom = item.min.toFloat()
valueTo = item.max.toFloat()
value = sliderProgress.toFloat()
textSliderValue!!.addTextChangedListener( object : TextWatcher {
override fun afterTextChanged(s: Editable) {
val textValue = s.toString().toIntOrNull();
if (textValue == null || textValue < valueFrom || textValue > valueTo) {
textInputLayout!!.error ="Inappropriate value"
} else {
textInputLayout!!.error = null
value = textValue.toFloat();
}
value = sliderProgress
textSliderValue?.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable) {
var textValue = s.toString().toFloatOrNull();
if (item.setting !is FloatSetting) {
textValue = textValue?.roundToInt()?.toFloat();
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
})
if (textValue == null || textValue < valueFrom || textValue > valueTo) {
textInputLayout?.error = "Inappropriate value"
} else {
textInputLayout?.error = null
value = textValue
}
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
})
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
sliderProgress = value.toInt()
if (textSliderValue!!.text.toString() != value.toInt().toString()) {
textSliderValue!!.setText(value.toInt().toString())
textSliderValue!!.setSelection(textSliderValue!!.length())
sliderProgress = (value * 100).roundToInt().toFloat() / 100f
var sliderString = sliderProgress.toString()
if (item.setting !is FloatSetting) {
sliderString = sliderProgress.roundToInt().toString()
if (textSliderValue?.text.toString() != sliderString) {
textSliderValue?.setText(sliderString)
textSliderValue?.setSelection(textSliderValue?.length() ?: 0 )
}
} else {
val currentText = textSliderValue?.text.toString()
val currentTextValue = currentText.toFloat()
if (currentTextValue != sliderProgress) {
textSliderValue?.setText(sliderString)
textSliderValue?.setSelection(textSliderValue?.length() ?: 0 )
}
}
}
}
@@ -300,14 +324,14 @@ class SettingsAdapter(
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.setNeutralButton(R.string.slider_default) { dialog: DialogInterface, which: Int ->
sliderBinding.slider.value = when (item.setting) {
sliderBinding.slider?.value = when (item.setting) {
is ScaledFloatSetting -> {
val scaledSetting = item.setting as ScaledFloatSetting
scaledSetting.defaultValue * scaledSetting.scale
}
is FloatSetting -> (item.setting as FloatSetting).defaultValue
else -> item.defaultValue!!
else -> item.defaultValue ?: 0f
}
onClick(dialog, which)
}
@@ -358,85 +382,89 @@ class SettingsAdapter(
override fun onClick(dialog: DialogInterface, which: Int) {
when (clickedItem) {
is SingleChoiceSetting -> {
val scSetting = clickedItem as SingleChoiceSetting
val setting = when (scSetting.setting) {
is AbstractIntSetting -> {
val value = getValueForSingleChoiceSelection(scSetting, which)
if (scSetting.selectedValue != value) {
fragmentView.onSettingChanged()
val scSetting = clickedItem as? SingleChoiceSetting
scSetting?.let {
val setting = when (it.setting) {
is AbstractIntSetting -> {
val value = getValueForSingleChoiceSelection(it, which)
if (it.selectedValue != value) {
fragmentView?.onSettingChanged()
}
it.setSelectedValue(value)
}
scSetting.setSelectedValue(value)
}
is AbstractShortSetting -> {
val value = getValueForSingleChoiceSelection(scSetting, which).toShort()
if (scSetting.selectedValue.toShort() != value) {
fragmentView.onSettingChanged()
is AbstractShortSetting -> {
val value = getValueForSingleChoiceSelection(it, which).toShort()
if (it.selectedValue.toShort() != value) {
fragmentView?.onSettingChanged()
}
it.setSelectedValue(value)
}
scSetting.setSelectedValue(value)
else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!")
}
else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!")
fragmentView?.putSetting(setting)
closeDialog()
}
fragmentView.putSetting(setting)
closeDialog()
}
is StringSingleChoiceSetting -> {
val scSetting = clickedItem as StringSingleChoiceSetting
val setting = when (scSetting.setting) {
is AbstractStringSetting -> {
val value = scSetting.getValueAt(which)
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
scSetting.setSelectedValue(value!!)
val scSetting = clickedItem as? StringSingleChoiceSetting
scSetting?.let {
val setting = when (it.setting) {
is AbstractStringSetting -> {
val value = it.getValueAt(which)
if (it.selectedValue != value) fragmentView?.onSettingChanged()
it.setSelectedValue(value ?: "")
}
is AbstractShortSetting -> {
if (it.selectValueIndex != which) fragmentView?.onSettingChanged()
it.setSelectedValue(it.getValueAt(which)?.toShort() ?: 1)
}
else -> throw IllegalStateException("Unrecognized type used for StringSingleChoiceSetting!")
}
is AbstractShortSetting -> {
if (scSetting.selectValueIndex != which) fragmentView.onSettingChanged()
scSetting.setSelectedValue(scSetting.getValueAt(which)?.toShort() ?: 1)
}
else -> throw IllegalStateException("Unrecognized type used for StringSingleChoiceSetting!")
fragmentView?.putSetting(setting)
closeDialog()
}
fragmentView.putSetting(setting)
closeDialog()
}
is SliderSetting -> {
val sliderSetting = clickedItem as SliderSetting
if (sliderSetting.selectedValue != sliderProgress) {
fragmentView.onSettingChanged()
}
when (sliderSetting.setting) {
is FloatSetting,
is ScaledFloatSetting -> {
val value = sliderProgress.toFloat()
val setting = sliderSetting.setSelectedValue(value)
fragmentView.putSetting(setting)
}
else -> {
val setting = sliderSetting.setSelectedValue(sliderProgress)
fragmentView.putSetting(setting)
val sliderSetting = clickedItem as? SliderSetting
sliderSetting?.let {
val sliderval = (it.selectedFloat * 100).roundToInt().toFloat() / 100
if (sliderval != sliderProgress) {
fragmentView?.onSettingChanged()
}
when (it.setting) {
is AbstractIntSetting -> {
val value = sliderProgress.roundToInt()
val setting = it.setSelectedValue(value)
fragmentView?.putSetting(setting)
}
else -> {
val setting = it.setSelectedValue(sliderProgress)
fragmentView?.putSetting(setting)
}
}
closeDialog()
}
closeDialog()
}
is StringInputSetting -> {
val inputSetting = clickedItem as StringInputSetting
if (inputSetting.selectedValue != textInputValue) {
fragmentView.onSettingChanged()
}
val setting = inputSetting.setSelectedValue(textInputValue)
fragmentView.putSetting(setting)
closeDialog()
val inputSetting = clickedItem as? StringInputSetting
inputSetting?.let {
if (it.selectedValue != textInputValue) {
fragmentView?.onSettingChanged()
}
val setting = it.setSelectedValue(textInputValue ?: "")
fragmentView?.putSetting(setting)
closeDialog()
}
}
}
clickedItem = null
sliderProgress = -1
sliderProgress = -1f
textInputValue = ""
}

View File

@@ -16,16 +16,19 @@ import androidx.preference.PreferenceManager
import kotlin.math.min
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.R
import org.citra.citra_emu.display.PortraitScreenLayout
import org.citra.citra_emu.display.ScreenLayout
import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting
import org.citra.citra_emu.features.settings.model.AbstractIntSetting
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.AbstractShortSetting
import org.citra.citra_emu.features.settings.model.AbstractStringSetting
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.FloatSetting
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.model.StringSetting
import org.citra.citra_emu.features.settings.model.AbstractShortSetting
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting
import org.citra.citra_emu.features.settings.model.view.HeaderSetting
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
@@ -962,6 +965,29 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.PORTRAIT_SCREEN_LAYOUT.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.SMALL_SCREEN_POSITION,
R.string.emulation_small_screen_position,
R.string.small_screen_position_description,
R.array.smallScreenPositions,
R.array.smallScreenPositionValues,
IntSetting.SMALL_SCREEN_POSITION.key,
IntSetting.SMALL_SCREEN_POSITION.defaultValue
)
)
add(
SliderSetting(
FloatSetting.LARGE_SCREEN_PROPORTION,
R.string.large_screen_proportion,
R.string.large_screen_proportion_description,
1,
5,
"",
FloatSetting.LARGE_SCREEN_PROPORTION.key,
FloatSetting.LARGE_SCREEN_PROPORTION.defaultValue
)
)
add(
SubmenuSetting(
R.string.emulation_landscape_custom_layout,
@@ -978,8 +1004,6 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
Settings.SECTION_CUSTOM_PORTRAIT
)
)
}
}

View File

@@ -182,10 +182,11 @@ void Config::ReadValues() {
layoutInt = static_cast<int>(Settings::LayoutOption::LargeScreen);
}
Settings::values.layout_option = static_cast<Settings::LayoutOption>(layoutInt);
Settings::values.large_screen_proportion =
static_cast<float>(sdl2_config->GetReal("Layout", "large_screen_proportion", 2.25));
Settings::values.small_screen_position = static_cast<Settings::SmallScreenPosition>(
sdl2_config->GetInteger("Layout", "small_screen_position",
static_cast<int>(Settings::SmallScreenPosition::TopRight)));
ReadSetting("Layout", Settings::values.custom_top_x);
ReadSetting("Layout", Settings::values.custom_top_y);
ReadSetting("Layout", Settings::values.custom_top_width);

View File

@@ -184,15 +184,26 @@ delay_game_render_thread_us =
[Layout]
# Layout for the screen inside the render window, landscape mode
# 0: Top/Bottom *currently unsupported on android*
# 0: Original (screens vertically aligned)
# 1: Single Screen Only,
# 2: *currently unsupported on android*
# 2: Large Screen (Default on android)
# 3: Side by Side
# 4: Hybrid
# 5: Custom Layout
# 6: (default) Large screen / small screen
layout_option =
# Large Screen Proportion - Relative size of large:small in large screen mode
# Default value is 2.25
large_screen_proportion =
# Small Screen Position - where is the small screen relative to the large
# Default value is 0
# 0: Top Right 1: Middle Right 2: Bottom Right
# 3: Top Left 4: Middle left 5: Bottom Left
# 6: Above the large screen 7: Below the large screen
small_screen_position =
# Screen placement when using Custom layout option
# 0x, 0y is the top left corner of the render window.
# suggested aspect ratio for top screen is 5:3

View File

@@ -39,6 +39,28 @@
<item>1</item>
</integer-array>
<string-array name="smallScreenPositions">
<item>@string/small_screen_position_top_right</item>
<item>@string/small_screen_position_middle_right</item>
<item>@string/small_screen_position_bottom_right</item>
<item>@string/small_screen_position_top_left</item>
<item>@string/small_screen_position_middle_left</item>
<item>@string/small_screen_position_bottom_left</item>
<item>@string/small_screen_position_above</item>
<item>@string/small_screen_position_below</item>
</string-array>
<integer-array name="smallScreenPositionValues">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
</integer-array>
<string-array name="regionNames">
<item>@string/auto_select</item>
<item>@string/system_region_jpn</item>

View File

@@ -384,6 +384,18 @@
<string name="emulation_screen_layout_original">Original</string>
<string name="emulation_portrait_layout_top_full">Default</string>
<string name="emulation_screen_layout_custom">Custom Layout</string>
<string name="emulation_small_screen_position">Small Screen Position</string>
<string name="small_screen_position_description">Where should the small screen appear relative to the large one in Large Screen Layout?</string>
<string name="small_screen_position_top_right">Top Right</string>
<string name="small_screen_position_middle_right">Middle Right</string>
<string name="small_screen_position_bottom_right">Bottom Right (Default)</string>
<string name="small_screen_position_top_left">Top Left</string>
<string name="small_screen_position_middle_left">Middle Left</string>
<string name="small_screen_position_bottom_left">Bottom Left</string>
<string name="small_screen_position_above">Above</string>
<string name="small_screen_position_below">Below</string>
<string name="large_screen_proportion">Large Screen Proportion</string>
<string name="large_screen_proportion_description">How many times larger is the large screen than the small screen in Large Screen layout?</string>
<string name="emulation_adjust_custom_layout">Adjust Custom Layout in Settings</string>
<string name="emulation_landscape_custom_layout">Landscape Custom Layout</string>
<string name="emulation_portrait_custom_layout">Portrait Custom Layout</string>