Skip to content

Commit 6a3454e

Browse files
committed
Merge branch 'master' into feature/migrate-touch-controls
2 parents 4c042a3 + 86d4d1b commit 6a3454e

17 files changed

+377
-116
lines changed

lemuroid-app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@
8787
android:name="com.swordfish.lemuroid.app.mobile.feature.input.GamePadBindingActivity"
8888
android:theme="@style/LemuroidMaterialTheme.Invisible" />
8989

90+
<activity
91+
android:name="com.swordfish.lemuroid.app.mobile.feature.input.GamePadShortcutBindingActivity"
92+
android:theme="@style/LemuroidMaterialTheme.Invisible" />
93+
9094
<activity
9195
android:name="com.swordfish.lemuroid.app.shared.settings.StorageFrameworkPickerLauncher"
9296
android:theme="@style/LemuroidMaterialTheme.Invisible" />

lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplicationModule.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.room.Room
2525
import com.swordfish.lemuroid.app.mobile.feature.game.GameActivity
2626
import com.swordfish.lemuroid.app.mobile.feature.gamemenu.GameMenuActivity
2727
import com.swordfish.lemuroid.app.mobile.feature.input.GamePadBindingActivity
28+
import com.swordfish.lemuroid.app.mobile.feature.input.GamePadShortcutBindingActivity
2829
import com.swordfish.lemuroid.app.mobile.feature.main.MainActivity
2930
import com.swordfish.lemuroid.app.mobile.feature.settings.SettingsManager
3031
import com.swordfish.lemuroid.app.mobile.feature.shortcuts.ShortcutsGenerator
@@ -113,6 +114,10 @@ abstract class LemuroidApplicationModule {
113114
@ContributesAndroidInjector(modules = [GamePadBindingActivity.Module::class])
114115
abstract fun gamepadBindingActivity(): GamePadBindingActivity
115116

117+
@PerActivity
118+
@ContributesAndroidInjector(modules = [GamePadShortcutBindingActivity.Module::class])
119+
abstract fun gamepadShortcutBindingActivity(): GamePadShortcutBindingActivity
120+
116121
@Module
117122
companion object {
118123
@Provides
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.swordfish.lemuroid.app.mobile.feature.input
2+
3+
import android.os.Bundle
4+
import android.view.KeyEvent
5+
import androidx.activity.compose.setContent
6+
import androidx.compose.foundation.focusable
7+
import androidx.compose.material3.AlertDialog
8+
import androidx.compose.material3.Text
9+
import androidx.compose.runtime.remember
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.focus.FocusRequester
12+
import androidx.compose.ui.focus.focusRequester
13+
import androidx.compose.ui.input.key.onKeyEvent
14+
import androidx.compose.ui.layout.onGloballyPositioned
15+
import com.swordfish.lemuroid.app.mobile.shared.compose.ui.AppTheme
16+
import com.swordfish.lemuroid.app.shared.input.InputDeviceManager
17+
import com.swordfish.lemuroid.app.shared.input.ShortcutBindingUpdater
18+
import com.swordfish.lemuroid.lib.android.RetrogradeActivity
19+
import timber.log.Timber
20+
import javax.inject.Inject
21+
22+
class GamePadShortcutBindingActivity : RetrogradeActivity() {
23+
@Inject
24+
lateinit var inputDeviceManager: InputDeviceManager
25+
26+
private lateinit var shortcutBindingUpdater: ShortcutBindingUpdater
27+
28+
override fun onCreate(savedInstanceState: Bundle?) {
29+
super.onCreate(savedInstanceState)
30+
31+
shortcutBindingUpdater = ShortcutBindingUpdater(inputDeviceManager, intent)
32+
33+
setContent {
34+
AppTheme {
35+
val focusRequester = remember { FocusRequester() }
36+
37+
AlertDialog(
38+
modifier =
39+
Modifier
40+
.focusRequester(focusRequester)
41+
.focusable()
42+
.onKeyEvent { handleKeyEvent(it.nativeKeyEvent) }
43+
.onGloballyPositioned { focusRequester.requestFocus() },
44+
title = { Text(text = shortcutBindingUpdater.getTitle(applicationContext)) },
45+
text = { Text(text = shortcutBindingUpdater.getMessage(applicationContext)) },
46+
onDismissRequest = { finish() },
47+
confirmButton = {},
48+
)
49+
}
50+
}
51+
}
52+
53+
private fun handleKeyEvent(event: KeyEvent): Boolean {
54+
Timber.i("Received key event: $event")
55+
val result = shortcutBindingUpdater.handleKeyEvent(event)
56+
57+
if (event.action == KeyEvent.ACTION_UP && result) {
58+
finish()
59+
}
60+
61+
return result
62+
}
63+
64+
@dagger.Module
65+
abstract class Module
66+
}

lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/inputdevices/InputDevicesSettingsScreen.kt

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.swordfish.lemuroid.app.mobile.feature.settings.inputdevices
22

3+
import android.content.Context
34
import android.content.Intent
45
import android.view.InputDevice
56
import android.view.KeyEvent
@@ -12,17 +13,17 @@ import androidx.compose.ui.platform.LocalContext
1213
import androidx.compose.ui.res.stringResource
1314
import com.swordfish.lemuroid.R
1415
import com.swordfish.lemuroid.app.mobile.feature.input.GamePadBindingActivity
16+
import com.swordfish.lemuroid.app.mobile.feature.input.GamePadShortcutBindingActivity
1517
import com.swordfish.lemuroid.app.shared.input.InputBindingUpdater
16-
import com.swordfish.lemuroid.app.shared.input.InputDeviceManager
1718
import com.swordfish.lemuroid.app.shared.input.InputKey
19+
import com.swordfish.lemuroid.app.shared.input.ShortcutBindingUpdater
1820
import com.swordfish.lemuroid.app.shared.input.lemuroiddevice.getLemuroidInputDevice
21+
import com.swordfish.lemuroid.app.shared.settings.GameShortcut
1922
import com.swordfish.lemuroid.app.utils.android.settings.LemuroidCardSettingsGroup
20-
import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsList
2123
import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsMenuLink
2224
import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsPage
2325
import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsSwitch
2426
import com.swordfish.lemuroid.app.utils.android.settings.booleanPreferenceState
25-
import com.swordfish.lemuroid.app.utils.android.settings.indexPreferenceState
2627

2728
@Composable
2829
fun InputDevicesSettingsScreen(
@@ -69,31 +70,29 @@ private fun DeviceBindingCategory(
6970
)
7071
}
7172

72-
DeviceMenuShortcut(device, bindings.menuShortcuts, bindings.defaultShortcut)
73+
bindings.shortcuts.forEach {
74+
DeviceShortcutBinding(context, device, it)
75+
}
7376
}
7477
}
7578

7679
@Composable
77-
private fun DeviceMenuShortcut(
80+
private fun DeviceShortcutBinding(
81+
context: Context,
7882
device: InputDevice,
79-
values: List<String>,
80-
defaultShortcut: String?,
83+
shortcut: GameShortcut,
8184
) {
82-
if (values.isEmpty() || defaultShortcut == null) {
83-
return
84-
}
85-
86-
val state =
87-
indexPreferenceState(
88-
InputDeviceManager.computeGameMenuShortcutPreference(device),
89-
defaultShortcut,
90-
values,
91-
)
92-
93-
LemuroidSettingsList(
94-
state = state,
95-
title = { Text(text = stringResource(R.string.settings_gamepad_title_game_menu)) },
96-
items = values,
85+
LemuroidSettingsMenuLink(
86+
title = { Text(text = shortcut.type.displayName()) },
87+
subtitle = { Text(text = shortcut.name) },
88+
onClick = {
89+
val intent =
90+
Intent(context, GamePadShortcutBindingActivity::class.java).apply {
91+
putExtra(ShortcutBindingUpdater.REQUEST_DEVICE, device)
92+
putExtra(ShortcutBindingUpdater.REQUEST_SHORTCUT_TYPE, shortcut.type.name)
93+
}
94+
context.startActivity(intent)
95+
},
9796
)
9897
}
9998

lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/inputdevices/InputDevicesSettingsViewModel.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import com.swordfish.lemuroid.app.shared.input.InputDeviceManager
99
import com.swordfish.lemuroid.app.shared.input.InputKey
1010
import com.swordfish.lemuroid.app.shared.input.RetroKey
1111
import com.swordfish.lemuroid.app.shared.input.lemuroiddevice.getLemuroidInputDevice
12-
import com.swordfish.lemuroid.app.shared.settings.GameMenuShortcut
12+
import com.swordfish.lemuroid.app.shared.settings.GameShortcut
1313
import com.swordfish.lemuroid.common.kotlin.reverseLookup
1414
import kotlinx.coroutines.flow.Flow
1515
import kotlinx.coroutines.flow.SharingStarted
@@ -39,8 +39,7 @@ class InputDevicesSettingsViewModel(
3939

4040
data class BindingsView(
4141
val keys: Map<RetroKey, InputKey> = emptyMap(),
42-
val menuShortcuts: List<String> = emptyList(),
43-
val defaultShortcut: String? = null,
42+
val shortcuts: List<GameShortcut> = emptyList(),
4443
)
4544

4645
data class State(
@@ -83,17 +82,17 @@ class InputDevicesSettingsViewModel(
8382
private fun getDevicesBindingViews(): Flow<Map<InputDevice, BindingsView>> {
8483
val devicesFlow = inputDeviceManager.getEnabledInputsObservable()
8584
val bindingsFlow = inputDeviceManager.getInputBindingsObservable()
85+
val shortcutsFlow = inputDeviceManager.getGameShortcutsObservable()
8686

87-
return combine(devicesFlow, bindingsFlow) { devices, allBindings ->
87+
return combine(devicesFlow, bindingsFlow, shortcutsFlow) { devices, allBindings, allShortcuts ->
8888
devices.associateWith { device ->
89-
val keys = allBindings(device).reverseLookup()
90-
val defaultShortcut = GameMenuShortcut.getDefault(device)?.name
9189
val shortcuts =
92-
device.getLemuroidInputDevice()
93-
.getSupportedShortcuts()
94-
.map { it.name }
90+
allShortcuts[device]?.filter {
91+
it.type in device.getLemuroidInputDevice().getSupportedShortcuts()
92+
} ?: emptyList()
93+
val keys = allBindings(device).reverseLookup()
9594

96-
BindingsView(keys, shortcuts, defaultShortcut)
95+
BindingsView(keys, shortcuts)
9796
}
9897
}
9998
}

lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputDeviceManager.kt

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import android.content.SharedPreferences
55
import android.hardware.input.InputManager
66
import android.view.InputDevice
77
import android.view.KeyEvent
8+
import androidx.core.content.edit
89
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
910
import com.swordfish.lemuroid.app.shared.input.lemuroiddevice.getLemuroidInputDevice
10-
import com.swordfish.lemuroid.app.shared.settings.GameMenuShortcut
11+
import com.swordfish.lemuroid.app.shared.settings.GameShortcut
12+
import com.swordfish.lemuroid.app.shared.settings.GameShortcutType
1113
import dagger.Lazy
1214
import kotlinx.coroutines.Dispatchers
1315
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -18,10 +20,12 @@ import kotlinx.coroutines.flow.flatMapLatest
1820
import kotlinx.coroutines.flow.flowOf
1921
import kotlinx.coroutines.flow.flowOn
2022
import kotlinx.coroutines.flow.map
23+
import kotlinx.coroutines.flow.mapNotNull
2124
import kotlinx.coroutines.flow.onCompletion
2225
import kotlinx.coroutines.flow.onSubscription
2326
import kotlinx.coroutines.withContext
2427
import kotlinx.serialization.builtins.MapSerializer
28+
import kotlinx.serialization.builtins.PairSerializer
2529
import kotlinx.serialization.json.Json
2630

2731
@OptIn(ExperimentalCoroutinesApi::class)
@@ -38,29 +42,17 @@ class InputDeviceManager(
3842
fun getInputBindingsObservable(): Flow<(InputDevice?) -> Map<InputKey, RetroKey>> {
3943
return getEnabledInputsObservable()
4044
.flatMapLatest { devices ->
41-
val allDeviceBindingsFlows =
42-
devices.map { device ->
43-
getBindingsFlow(device).map { device to it }
44-
}
45-
combine(allDeviceBindingsFlows) { allDeviceBindings ->
46-
allDeviceBindings.associate { (inputKey, retroKey) -> inputKey to retroKey }
47-
}
45+
val allDeviceBindingsFlows = devices.map { device -> getBindingsFlow(device).map { device to it } }
46+
combine(allDeviceBindingsFlows) { it.toMap() }
4847
}
4948
.map { bindings -> { bindings[it] ?: mapOf() } }
5049
}
5150

52-
fun getInputMenuShortCutObservable(): Flow<GameMenuShortcut?> {
51+
fun getGameShortcutsObservable(): Flow<Map<InputDevice, List<GameShortcut>>> {
5352
return getEnabledInputsObservable()
54-
.map { devices ->
55-
val device = devices.firstOrNull()
56-
device
57-
?.let {
58-
sharedPreferences.getString(
59-
computeGameMenuShortcutPreference(it),
60-
GameMenuShortcut.getDefault(it)?.name,
61-
)
62-
}
63-
?.let { GameMenuShortcut.findByName(device, it) }
53+
.flatMapLatest { devices ->
54+
val allShortcutFlows = devices.map { device -> getShortcutBindingsFlow(device).map { device to it } }
55+
combine(allShortcutFlows) { it.toMap() }
6456
}
6557
}
6658

@@ -81,6 +73,25 @@ class InputDeviceManager(
8173
.flowOn(Dispatchers.IO)
8274
}
8375

76+
private fun getShortcutBindingsFlow(device: InputDevice): Flow<List<GameShortcut>> {
77+
val flows =
78+
GameShortcutType.values().map { type ->
79+
flowSharedPreferences.getString(computeGameShortcutPreference(device, type))
80+
.asFlow()
81+
.mapNotNull { preference ->
82+
if (preference.isEmpty()) return@mapNotNull GameShortcut.getDefault(device, type)
83+
val decoded = runCatching { Json.decodeFromString(bindingsComboSerializer, preference) }
84+
val combo = decoded.getOrNull() ?: return@mapNotNull GameShortcut.getDefault(device, type)
85+
GameShortcut(type = type, keys = setOf(combo.first.keyCode, combo.second.keyCode))
86+
}
87+
}
88+
return if (flows.isEmpty()) {
89+
flowOf(emptyList())
90+
} else {
91+
combine(flows) { it.toList() }
92+
}.flowOn(Dispatchers.IO)
93+
}
94+
8495
suspend fun getCurrentBindings(inputDevice: InputDevice): Map<InputKey, RetroKey> {
8596
return withContext(Dispatchers.IO) {
8697
val preference =
@@ -124,6 +135,18 @@ class InputDeviceManager(
124135
.commit()
125136
}
126137

138+
suspend fun updateShortcutBinding(
139+
inputDevice: InputDevice,
140+
shortcutType: GameShortcutType,
141+
inputKeys: Pair<InputKey, InputKey>,
142+
) = withContext(Dispatchers.IO) {
143+
sharedPreferences.edit(commit = true) {
144+
val key = computeGameShortcutPreference(inputDevice, shortcutType)
145+
val value = Json.encodeToString(bindingsComboSerializer, inputKeys)
146+
putString(key, value)
147+
}
148+
}
149+
127150
suspend fun resetAllBindings() =
128151
withContext(Dispatchers.IO) {
129152
val editor = sharedPreferences.edit()
@@ -208,6 +231,7 @@ class InputDeviceManager(
208231
private const val GAME_PAD_ENABLED_PREFERENCE_BASE_KEY = "pref_key_gamepad_enabled"
209232

210233
private val bindingsMapSerializer = MapSerializer(InputKey.serializer(), RetroKey.serializer())
234+
private val bindingsComboSerializer = PairSerializer(InputKey.serializer(), InputKey.serializer())
211235

212236
private fun getSharedPreferencesId(inputDevice: InputDevice) = inputDevice.descriptor
213237

@@ -221,8 +245,10 @@ class InputDeviceManager(
221245
fun computeEnabledGamePadPreference(inputDevice: InputDevice) =
222246
"${GAME_PAD_ENABLED_PREFERENCE_BASE_KEY}_${getSharedPreferencesId(inputDevice)}"
223247

224-
fun computeGameMenuShortcutPreference(inputDevice: InputDevice) =
225-
"${GAME_PAD_BINDING_PREFERENCE_BASE_KEY}_${getSharedPreferencesId(inputDevice)}_gamemenu"
248+
fun computeGameShortcutPreference(
249+
inputDevice: InputDevice,
250+
type: GameShortcutType,
251+
) = "${GAME_PAD_BINDING_PREFERENCE_BASE_KEY}_${getSharedPreferencesId(inputDevice)}_shortcut_$type."
226252

227253
fun computeKeyBindingGamePadPreference(inputDevice: InputDevice) =
228254
"${GAME_PAD_BINDING_PREFERENCE_BASE_KEY}_${getSharedPreferencesId(inputDevice)}"

0 commit comments

Comments
 (0)