From a026a0d5e49627b28d3a960915e7ccbba399b1a5 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 17 Oct 2025 22:38:52 +0200 Subject: [PATCH 1/2] Android: Treat EmulationActivity dialog dismiss the same as OK In the dialog where you can choose what controller the input overlay should be controlling, there's an OK button. If you change controller but don't press OK, your selection will be saved, but the input overlay won't refresh to show the new controller unless you perform some other action that would cause it to refresh. This is not good UX. This commit changes the behavior not only of this dialog but also other dialogs spawned by EmulationActivity so that everything is properly updated when dismissing a dialog, as if you had pressed OK. --- .../activities/EmulationActivity.kt | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt index 184392f75f6..60d4311ee5d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt @@ -561,6 +561,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting.valueOf(gcSettingBase + indexSelected) .setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } InputOverlay.OVERLAY_WIIMOTE_CLASSIC -> { @@ -575,6 +576,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting.valueOf(classicSettingBase + indexSelected) .setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } InputOverlay.OVERLAY_WIIMOTE_NUNCHUK -> { @@ -596,6 +598,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting.valueOf(nunchukSettingBase + translateToSettingsIndex(indexSelected)) .setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } else -> { @@ -611,14 +614,13 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting.valueOf(wiimoteSettingBase + indexSelected) .setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } } builder - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> - emulationFragment?.refreshInputOverlay() - } + .setPositiveButton(R.string.ok, null) .show() } @@ -640,6 +642,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting .valueOf(gcSettingBase + indexSelected).setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } else if (currentController == InputOverlay.OVERLAY_WIIMOTE_CLASSIC) { val wiiClassicEnabledButtons = BooleanArray(14) @@ -653,6 +656,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting.valueOf(classicSettingBase + indexSelected) .setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } else { val wiiEnabledButtons = BooleanArray(11) @@ -667,6 +671,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting .valueOf(wiiSettingBase + indexSelected).setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } else { builder.setMultiChoiceItems( @@ -674,6 +679,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting .valueOf(wiiSettingBase + indexSelected).setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } } @@ -682,7 +688,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { .setNeutralButton(R.string.emulation_toggle_all) { _: DialogInterface?, _: Int -> emulationFragment!!.toggleInputOverlayVisibility(settings) } - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> emulationFragment?.refreshInputOverlay() } + .setPositiveButton(R.string.ok, null) .show() } @@ -707,10 +713,9 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { settings, InputOverlayPointer.DOUBLE_TAP_OPTIONS[which] ) - } - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> emulationFragment?.initInputPointer() } + .setPositiveButton(R.string.ok, null) .show() } @@ -721,9 +726,11 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { valueTo = 150f value = IntSetting.MAIN_CONTROL_SCALE.int.toFloat() stepSize = 1f - addOnChangeListener(Slider.OnChangeListener { _: Slider?, progress: Float, _: Boolean -> - dialogBinding.inputScaleValue.text = "${(progress.toInt() + 50)}%" - }) + addOnChangeListener { _: Slider?, value: Float, _: Boolean -> + dialogBinding.inputScaleValue.text = "${(value.toInt() + 50)}%" + IntSetting.MAIN_CONTROL_SCALE.setInt(settings, value.toInt()) + emulationFragment?.refreshInputOverlay() + } } inputScaleValue.text = "${(dialogBinding.inputScaleSlider.value.toInt() + 50)}%" @@ -732,9 +739,11 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { valueTo = 100f value = IntSetting.MAIN_CONTROL_OPACITY.int.toFloat() stepSize = 1f - addOnChangeListener(Slider.OnChangeListener { _: Slider?, progress: Float, _: Boolean -> - inputOpacityValue.text = progress.toInt().toString() + "%" - }) + addOnChangeListener { _: Slider?, value: Float, _: Boolean -> + inputOpacityValue.text = value.toInt().toString() + "%" + IntSetting.MAIN_CONTROL_OPACITY.setInt(settings, value.toInt()) + emulationFragment?.refreshInputOverlay() + } } inputOpacityValue.text = inputOpacitySlider.value.toInt().toString() + "%" } @@ -742,17 +751,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { MaterialAlertDialogBuilder(this) .setTitle(R.string.emulation_control_adjustments) .setView(dialogBinding.root) - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> - IntSetting.MAIN_CONTROL_SCALE.setInt( - settings, - dialogBinding.inputScaleSlider.value.toInt() - ) - IntSetting.MAIN_CONTROL_OPACITY.setInt( - settings, - dialogBinding.inputOpacitySlider.value.toInt() - ) - emulationFragment?.refreshInputOverlay() - } + .setPositiveButton(R.string.ok, null) .setNeutralButton(R.string.default_values) { _: DialogInterface?, _: Int -> IntSetting.MAIN_CONTROL_SCALE.delete(settings) IntSetting.MAIN_CONTROL_OPACITY.delete(settings) @@ -858,10 +857,9 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { entries.toArray(arrayOf()), checkedItem ) { _: DialogInterface?, indexSelected: Int -> controllerSetting.setInt(settings, values[indexSelected]) - } - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> emulationFragment?.refreshInputOverlay() } + .setPositiveButton(R.string.ok, null) .setNeutralButton( R.string.emulation_more_controller_settings ) { _: DialogInterface?, _: Int -> SettingsActivity.launch(this, MenuTag.SETTINGS) } @@ -876,10 +874,9 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { IntSetting.MAIN_IR_MODE.int ) { _: DialogInterface?, indexSelected: Int -> IntSetting.MAIN_IR_MODE.setInt(settings, indexSelected) - } - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> emulationFragment?.refreshOverlayPointer() } + .setPositiveButton(R.string.ok, null) .show() } From 060f7925603440f36e59ffbb355143be80f15add Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 9 Nov 2025 14:25:42 +0100 Subject: [PATCH 2/2] Android: Rate limit refreshInputOverlay calls from sliders Sliders can trigger change listeners very rapidly, so let's add some rate limiting so dragging a slider doesn't cause the whole UI to lag. (Now the input overlay looks laggy when dragging a slider, though.) --- .../activities/EmulationActivity.kt | 11 +++++-- .../dolphinemu/utils/RateLimiter.kt | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/RateLimiter.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt index 60d4311ee5d..1b23118de39 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt @@ -8,6 +8,8 @@ import android.graphics.Rect import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.util.SparseIntArray import android.view.KeyEvent import android.view.MenuItem @@ -61,6 +63,7 @@ import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner import org.dolphinemu.dolphinemu.utils.DirectoryInitialization import org.dolphinemu.dolphinemu.utils.FileBrowserHelper +import org.dolphinemu.dolphinemu.utils.RateLimiter import org.dolphinemu.dolphinemu.utils.ThemeHelper import kotlin.math.roundToInt @@ -88,6 +91,10 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { private lateinit var binding: ActivityEmulationBinding + private val refreshInputOverlayRateLimiter = RateLimiter(Handler(Looper.getMainLooper()), 100) { + emulationFragment?.refreshInputOverlay() + } + private val requestChangeDisc = registerForActivityResult( ActivityResultContracts.GetContent() ) { uri: Uri? -> @@ -729,7 +736,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { addOnChangeListener { _: Slider?, value: Float, _: Boolean -> dialogBinding.inputScaleValue.text = "${(value.toInt() + 50)}%" IntSetting.MAIN_CONTROL_SCALE.setInt(settings, value.toInt()) - emulationFragment?.refreshInputOverlay() + refreshInputOverlayRateLimiter.run() } } inputScaleValue.text = @@ -742,7 +749,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { addOnChangeListener { _: Slider?, value: Float, _: Boolean -> inputOpacityValue.text = value.toInt().toString() + "%" IntSetting.MAIN_CONTROL_OPACITY.setInt(settings, value.toInt()) - emulationFragment?.refreshInputOverlay() + refreshInputOverlayRateLimiter.run() } } inputOpacityValue.text = inputOpacitySlider.value.toInt().toString() + "%" diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/RateLimiter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/RateLimiter.kt new file mode 100644 index 00000000000..498e128875c --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/RateLimiter.kt @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.utils + +import android.os.Handler +import android.os.SystemClock + +class RateLimiter( + private val handler: Handler, + private val delayBetweenRunsMs: Int, + private val runnable: Runnable +) { + private var nextAllowedRun = 0L + private var pendingRun = false + + fun run() { + if (!pendingRun) { + val time = SystemClock.uptimeMillis() + if (time >= nextAllowedRun) { + runnable.run() + nextAllowedRun = time + delayBetweenRunsMs + } else { + pendingRun = true + handler.postAtTime({ + runnable.run() + nextAllowedRun = SystemClock.uptimeMillis() + delayBetweenRunsMs + pendingRun = false + }, nextAllowedRun) + } + } + } +}