summaryrefslogtreecommitdiff
path: root/Source/Android
diff options
context:
space:
mode:
Diffstat (limited to 'Source/Android')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt29
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/ui/FigureSlotAdapter.kt2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt12
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt31
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ControllerMapping.kt24
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt46
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt6
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt406
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt3
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsItem.kt1
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsSearchResult.kt19
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt225
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt124
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt34
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt232
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt165
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt763
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt14
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt43
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt28
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/skylanders/ui/SkylanderSlotAdapter.kt2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt10
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt76
-rw-r--r--Source/Android/app/src/main/res/anim/anim_settings_search_pop_in.xml7
-rw-r--r--Source/Android/app/src/main/res/anim/anim_settings_search_pop_out.xml6
-rw-r--r--Source/Android/app/src/main/res/color/settings_search_outline.xml4
-rw-r--r--Source/Android/app/src/main/res/drawable/ic_search.xml10
-rw-r--r--Source/Android/app/src/main/res/layout/activity_settings.xml145
-rw-r--r--Source/Android/app/src/main/res/layout/fragment_settings.xml15
-rw-r--r--Source/Android/app/src/main/res/layout/list_item_search_result.xml36
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml13
-rw-r--r--Source/Android/jni/NetPlay/Netplay.cpp75
32 files changed, 1883 insertions, 723 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 8a1d8addbd..23ffe96cd6 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
@@ -43,6 +43,7 @@ import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlotAdapter
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
import org.dolphinemu.dolphinemu.features.input.model.DolphinSensorEventListener
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
+import org.dolphinemu.dolphinemu.utils.ContentHandler
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
@@ -104,17 +105,21 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
}
val requestSkylanderFile = registerForActivityResult(
- ActivityResultContracts.GetContent()
+ ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
if (uri != null) {
val slot = SkylanderConfig.loadSkylander(
skylanderSlots[skylanderSlot].portalSlot,
uri.toString()
- )!!
- clearSkylander(skylanderSlot)
- skylanderSlots[skylanderSlot].portalSlot = slot.first!!
- skylanderSlots[skylanderSlot].label = slot.second!!
- skylandersBinding.figureManager.adapter!!.notifyItemChanged(skylanderSlot)
+ )
+ if (slot != null && slot.first != null && slot.second != null) {
+ clearSkylander(skylanderSlot)
+ skylanderSlots[skylanderSlot].portalSlot = slot.first!!
+ skylanderSlots[skylanderSlot].label = slot.second!!
+ skylandersBinding.figureManager.adapter?.notifyItemChanged(skylanderSlot)
+ } else {
+ Toast.makeText(this, R.string.skylander_load_failed, Toast.LENGTH_SHORT).show()
+ }
skylanderSlot = -1
skylanderData = Skylander.BLANK_SKYLANDER
}
@@ -131,10 +136,12 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
uri.toString(),
skylanderSlots[skylanderSlot].portalSlot
)
- clearSkylander(skylanderSlot)
- skylanderSlots[skylanderSlot].portalSlot = slot.first
- skylanderSlots[skylanderSlot].label = slot.second
- skylandersBinding.figureManager.adapter?.notifyItemChanged(skylanderSlot)
+ if (slot.first != -1) {
+ clearSkylander(skylanderSlot)
+ skylanderSlots[skylanderSlot].portalSlot = slot.first
+ skylanderSlots[skylanderSlot].label = slot.second
+ skylandersBinding.figureManager.adapter?.notifyItemChanged(skylanderSlot)
+ }
skylanderSlot = -1
skylanderData = Skylander.BLANK_SKYLANDER
}
@@ -142,7 +149,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
}
val requestInfinityFigureFile = registerForActivityResult(
- ActivityResultContracts.GetContent()
+ ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
if (uri != null) {
val label = InfinityConfig.loadFigure(infinityPosition, uri.toString())
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/ui/FigureSlotAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/ui/FigureSlotAdapter.kt
index 0613274c30..6839d6f803 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/ui/FigureSlotAdapter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/infinitybase/ui/FigureSlotAdapter.kt
@@ -51,7 +51,7 @@ class FigureSlotAdapter(
holder.binding.buttonLoadFigure.setOnClickListener {
activity.setInfinityFigureData(0, "", figure.position, position)
- activity.requestInfinityFigureFile.launch("*/*")
+ activity.requestInfinityFigureFile.launch(arrayOf("*/*"))
}
val inflater = LayoutInflater.from(activity)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt
index b412b6ef9b..bcd47781a0 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt
@@ -186,7 +186,7 @@ object ControllerInterface {
private fun vibrate(vibrator: Vibrator) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
vibrator.vibrate(
- VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE),
+ VibrationEffect.createOneShot(10000, VibrationEffect.DEFAULT_AMPLITUDE),
VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_MEDIA).build()
)
} else {
@@ -194,16 +194,22 @@ object ControllerInterface {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@Suppress("DEPRECATION")
vibrator.vibrate(
- VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE),
+ VibrationEffect.createOneShot(10000, VibrationEffect.DEFAULT_AMPLITUDE),
attributes
)
} else {
@Suppress("DEPRECATION")
- vibrator.vibrate(100, attributes)
+ vibrator.vibrate(10000, attributes)
}
}
}
+ @Keep
+ @JvmStatic
+ private fun cancelVibration(vibrator: Vibrator) {
+ vibrator.cancel()
+ }
+
private class InputDeviceListener : InputManager.InputDeviceListener {
override external fun onInputDeviceAdded(deviceId: Int)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt
index 56676c4d3f..d44e70ba09 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
+import org.dolphinemu.dolphinemu.features.netplay.model.ControllerMapping
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
import org.dolphinemu.dolphinemu.features.netplay.model.Player
@@ -87,6 +88,12 @@ class NetplaySession(
)
val players = _players.asSharedFlow().distinctUntilChanged()
+ private val _controllerMapping = MutableSharedFlow<ControllerMapping>(
+ replay = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ val controllerMapping = _controllerMapping.asSharedFlow().distinctUntilChanged()
+
private val _chatMessages = MutableSharedFlow<String>(
extraBufferCapacity = 32,
onBufferOverflow = BufferOverflow.DROP_OLDEST
@@ -185,6 +192,11 @@ class NetplaySession(
fun reconnectTraversal() = nativeReconnectTraversal()
+ fun setControllerMapping(mapping: ControllerMapping) {
+ nativeSetPadMapping(mapping.gamecubePorts.map { it?.pid ?: 0 }.toIntArray())
+ nativeSetWiimoteMapping(mapping.wiiRemotes.map { it?.pid ?: 0 }.toIntArray())
+ }
+
fun consumeBootSessionData(): Long {
return bootSessionDataPointer.also {
bootSessionDataPointer = 0
@@ -278,6 +290,14 @@ class NetplaySession(
private external fun nativeReconnectTraversal()
+ private external fun nativeGetPadMapping(): IntArray
+
+ private external fun nativeGetWiimoteMapping(): IntArray
+
+ private external fun nativeSetPadMapping(mapping: IntArray)
+
+ private external fun nativeSetWiimoteMapping(mapping: IntArray)
+
// NetPlayUI callbacks
@Keep
@@ -304,7 +324,16 @@ class NetplaySession(
@Keep
fun onUpdate(players: Array<Player>) {
- _players.tryEmit(players.toList())
+ val playersList = players.toList()
+ _players.tryEmit(playersList)
+
+ fun findPlayer(playerId: Int) = playersList.find { it.pid == playerId }
+ _controllerMapping.tryEmit(
+ ControllerMapping(
+ gamecubePorts = nativeGetPadMapping().map(::findPlayer).toList(),
+ wiiRemotes = nativeGetWiimoteMapping().map(::findPlayer).toList(),
+ )
+ )
}
@Keep
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ControllerMapping.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ControllerMapping.kt
new file mode 100644
index 0000000000..d7cfae0ab3
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ControllerMapping.kt
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.netplay.model
+
+data class ControllerMapping(
+ val gamecubePorts: List<Player?>,
+ val wiiRemotes: List<Player?>,
+) {
+ fun withGamecubePort(port: Int, player: Player?) =
+ copy(gamecubePorts = gamecubePorts.replace(port, player))
+
+ fun withWiiRemote(port: Int, player: Player?) =
+ copy(wiiRemotes = wiiRemotes.replace(port, player))
+
+ companion object {
+ fun emptyControllerMapping(): ControllerMapping = ControllerMapping(
+ gamecubePorts = emptyList(),
+ wiiRemotes = emptyList(),
+ )
+ }
+}
+
+private fun <T> List<T>.replace(index: Int, value: T) =
+ toMutableList().also { it[index] = value }
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
index adea1a13a5..1e3aea26a2 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
@@ -11,15 +11,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.dolphinemu.dolphinemu.features.netplay.NetplaySession
+import org.dolphinemu.dolphinemu.features.netplay.model.ControllerMapping.Companion.emptyControllerMapping
+import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
@@ -58,6 +60,9 @@ class NetplayViewModel(
val players = netplaySession.players
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
+ val controllerMapping = netplaySession.controllerMapping
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyControllerMapping())
+
val messages = netplaySession.messages
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
@@ -104,18 +109,37 @@ class NetplayViewModel(
private val _notAllPlayersHaveGame = Channel<Unit>(Channel.CONFLATED)
val notAllPlayersHaveGame = _notAllPlayersHaveGame.receiveAsFlow()
+ private val _dualCoreWarning = Channel<Unit>(Channel.CONFLATED)
+ val dualCoreWarning = _dualCoreWarning.receiveAsFlow()
+
fun startGame() {
- if (netplaySession.doAllPlayersHaveGame()) {
- netplaySession.startGame()
- } else {
+ if (!netplaySession.doAllPlayersHaveGame()) {
_notAllPlayersHaveGame.trySend(Unit)
+ return
}
+
+ if (BooleanSetting.MAIN_CPU_THREAD.boolean &&
+ !BooleanSetting.NETPLAY_SKIP_DUAL_CORE_WARNING.boolean
+ ) {
+ _dualCoreWarning.trySend(Unit)
+ return
+ }
+
+ confirmStartGame()
}
fun confirmStartGame() {
netplaySession.startGame()
}
+ fun setDualCoreEnabled(enabled: Boolean) {
+ BooleanSetting.MAIN_CPU_THREAD.setBoolean(NativeConfig.LAYER_BASE, enabled)
+ }
+
+ fun skipDualCoreWarning() {
+ BooleanSetting.NETPLAY_SKIP_DUAL_CORE_WARNING.setBoolean(NativeConfig.LAYER_BASE, true)
+ }
+
fun sendMessage(message: String) {
val trimmedMessage = message.trim()
if (trimmedMessage.isEmpty()) {
@@ -148,6 +172,18 @@ class NetplayViewModel(
netplaySession.changeGame(gameFile)
}
+ fun setGamecubePort(portNumber: Int, player: Player?) {
+ netplaySession.setControllerMapping(
+ controllerMapping.value.withGamecubePort(portNumber, player)
+ )
+ }
+
+ fun setWiiRemote(remoteNumber: Int, player: Player?) {
+ netplaySession.setControllerMapping(
+ controllerMapping.value.withWiiRemote(remoteNumber, player)
+ )
+ }
+
private fun getLocalIp(): JoinAddress {
val localIp = networkHelper.getLocalIpString()
?: return JoinAddress.Unknown { _joinAddresses.value += JoinInfoType.LOCAL to getLocalIp() }
@@ -176,12 +212,14 @@ class NetplayViewModel(
JoinInfoType.EXTERNAL to JoinAddress.Loading,
)
}
+
is TraversalState.Connected -> {
_joinAddresses.value += mapOf(
JoinInfoType.ROOM_ID to JoinAddress.Loaded(state.hostCode),
JoinInfoType.EXTERNAL to JoinAddress.Loaded(state.externalAddress),
)
}
+
is TraversalState.Failure -> {
_joinAddresses.value += mapOf(
JoinInfoType.ROOM_ID to JoinAddress.Unknown(retry),
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
index eb9cecec20..ee1098956d 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
@@ -58,6 +58,9 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
onGameSelected = viewModel::changeGame,
gameFiles = viewModel.gameFiles.collectAsState().value,
notAllPlayersHaveGame = viewModel.notAllPlayersHaveGame,
+ dualCoreWarning = viewModel.dualCoreWarning,
+ onSetDualCoreEnabled = viewModel::setDualCoreEnabled,
+ onSkipDualCoreWarning = viewModel::skipDualCoreWarning,
onConfirmStartGame = viewModel::confirmStartGame,
players = viewModel.players.collectAsState().value,
hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value,
@@ -70,6 +73,9 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
saveTransferProgress = viewModel.saveTransferProgress.collectAsState().value,
gameDigestProgress = viewModel.gameDigestProgress.collectAsState().value,
joinAddresses = viewModel.joinAddresses.collectAsState().value,
+ controllerMapping = viewModel.controllerMapping.collectAsState().value,
+ onGamecubePortChanged = viewModel::setGamecubePort,
+ onWiiRemoteChanged = viewModel::setWiiRemote,
)
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
index 789a3cef17..aebac3f5bc 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
@@ -4,7 +4,9 @@ package org.dolphinemu.dolphinemu.features.netplay.ui
import android.content.Intent
import android.content.res.Configuration
+import androidx.annotation.StringRes
import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -19,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
@@ -59,6 +62,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SheetValue
+import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
@@ -72,11 +76,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.font.FontWeight
@@ -86,8 +91,8 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import androidx.window.core.layout.WindowSizeClass
import coil.compose.AsyncImage
@@ -95,6 +100,7 @@ import coil.request.ImageRequest
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import org.dolphinemu.dolphinemu.R
+import org.dolphinemu.dolphinemu.features.netplay.model.ControllerMapping
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
import org.dolphinemu.dolphinemu.features.netplay.model.JoinAddress
import org.dolphinemu.dolphinemu.features.netplay.model.JoinInfoType
@@ -129,6 +135,9 @@ fun NetplayScreen(
onGameSelected: (GameFile) -> Unit,
gameFiles: List<GameFile>,
notAllPlayersHaveGame: Flow<Unit>,
+ dualCoreWarning: Flow<Unit>,
+ onSetDualCoreEnabled: (Boolean) -> Unit,
+ onSkipDualCoreWarning: () -> Unit,
onConfirmStartGame: () -> Unit,
hostInputAuthorityEnabled: Boolean,
networkMode: NetworkMode,
@@ -141,6 +150,9 @@ fun NetplayScreen(
saveTransferProgress: SaveTransferProgress?,
gameDigestProgress: GameDigestProgress?,
joinAddresses: Map<JoinInfoType, JoinAddress>,
+ controllerMapping: ControllerMapping,
+ onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
+ onWiiRemoteChanged: (port: Int, player: Player?) -> Unit,
) {
val scrollState = rememberScrollState()
@@ -171,6 +183,7 @@ fun NetplayScreen(
// State which must live above the landscape/portrait split.
var showChat by rememberSaveable { mutableStateOf(false) }
var showGamePicker by rememberSaveable { mutableStateOf(false) }
+ var showControllerMapping by rememberSaveable { mutableStateOf(false) }
var selectedJoinInfoType by rememberSaveable {
mutableStateOf(joinAddresses.keys.firstOrNull() ?: JoinInfoType.EXTERNAL)
}
@@ -194,6 +207,11 @@ fun NetplayScreen(
showGamePicker = showGamePicker,
onShowGamePickerChanged = { showGamePicker = it },
players = players,
+ controllerMapping = controllerMapping,
+ onGamecubePortChanged = onGamecubePortChanged,
+ onWiimotePortChanged = onWiiRemoteChanged,
+ showControllerMapping = showControllerMapping,
+ onShowControllerMappingChanged = { showControllerMapping = it },
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
networkMode = networkMode,
onNetworkModeChanged = onNetworkModeChanged,
@@ -221,6 +239,11 @@ fun NetplayScreen(
showGamePicker = showGamePicker,
onShowGamePickerChanged = { showGamePicker = it },
players = players,
+ controllerMapping = controllerMapping,
+ onGamecubePortChanged = onGamecubePortChanged,
+ onWiimotePortChanged = onWiiRemoteChanged,
+ showControllerMapping = showControllerMapping,
+ onShowControllerMappingChanged = { showControllerMapping = it },
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
networkMode = networkMode,
onNetworkModeChanged = onNetworkModeChanged,
@@ -252,6 +275,11 @@ fun NetplayScreen(
notAllPlayersHaveGame.collect { showNotAllPlayersHaveGame = true }
}
+ var showDualCoreWarning by rememberSaveable { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ dualCoreWarning.collect { showDualCoreWarning = true }
+ }
+
var dismissSaveTransferProgressDialog by rememberSaveable { mutableStateOf(false) }
if (saveTransferProgress == null) {
dismissSaveTransferProgressDialog = false
@@ -323,6 +351,21 @@ fun NetplayScreen(
onDismissRequest = { showNotAllPlayersHaveGame = false },
)
}
+
+ showDualCoreWarning -> {
+ DualCoreWarningDialog(
+ onSetDualCoreEnabled = onSetDualCoreEnabled,
+ onStartGame = {
+ showDualCoreWarning = false
+ onConfirmStartGame()
+ },
+ onSkipWarning = {
+ showDualCoreWarning = false
+ onSkipDualCoreWarning()
+ },
+ onDismiss = { showDualCoreWarning = false },
+ )
+ }
}
}
}
@@ -340,6 +383,11 @@ private fun PortraitContent(
showGamePicker: Boolean,
onShowGamePickerChanged: (Boolean) -> Unit,
players: List<Player>,
+ controllerMapping: ControllerMapping,
+ onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
+ onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
+ showControllerMapping: Boolean,
+ onShowControllerMappingChanged: (Boolean) -> Unit,
hostInputAuthorityEnabled: Boolean,
networkMode: NetworkMode,
onNetworkModeChanged: (NetworkMode) -> Unit,
@@ -379,6 +427,11 @@ private fun PortraitContent(
showGamePicker = showGamePicker,
onShowGamePickerChanged = onShowGamePickerChanged,
players = players,
+ controllerMapping = controllerMapping,
+ onGamecubePortChanged = onGamecubePortChanged,
+ onWiimotePortChanged = onWiimotePortChanged,
+ showControllerMapping = showControllerMapping,
+ onShowControllerMappingChanged = onShowControllerMappingChanged,
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
networkMode = networkMode,
onNetworkModeChanged = onNetworkModeChanged,
@@ -413,6 +466,11 @@ private fun LandscapeContent(
showGamePicker: Boolean,
onShowGamePickerChanged: (Boolean) -> Unit,
players: List<Player>,
+ controllerMapping: ControllerMapping,
+ onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
+ onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
+ showControllerMapping: Boolean,
+ onShowControllerMappingChanged: (Boolean) -> Unit,
hostInputAuthorityEnabled: Boolean,
networkMode: NetworkMode,
onNetworkModeChanged: (NetworkMode) -> Unit,
@@ -445,7 +503,6 @@ private fun LandscapeContent(
.fillMaxHeight()
)
- val scrollState = rememberScrollState()
Column(
modifier = Modifier
.weight(1f)
@@ -464,6 +521,11 @@ private fun LandscapeContent(
showGamePicker = showGamePicker,
onShowGamePickerChanged = onShowGamePickerChanged,
players = players,
+ controllerMapping = controllerMapping,
+ onGamecubePortChanged = onGamecubePortChanged,
+ onWiimotePortChanged = onWiimotePortChanged,
+ showControllerMapping = showControllerMapping,
+ onShowControllerMappingChanged = onShowControllerMappingChanged,
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
networkMode = networkMode,
onNetworkModeChanged = onNetworkModeChanged,
@@ -485,6 +547,7 @@ private fun LandscapeContent(
}
}
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PlayersAndSettings(
game: String,
@@ -493,6 +556,11 @@ private fun PlayersAndSettings(
showGamePicker: Boolean,
onShowGamePickerChanged: (Boolean) -> Unit,
players: List<Player>,
+ controllerMapping: ControllerMapping,
+ onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
+ onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
+ showControllerMapping: Boolean,
+ onShowControllerMappingChanged: (Boolean) -> Unit,
hostInputAuthorityEnabled: Boolean,
networkMode: NetworkMode,
onNetworkModeChanged: (NetworkMode) -> Unit,
@@ -530,25 +598,15 @@ private fun PlayersAndSettings(
MenuSpacer()
- OutlinedBox(
- label = { Text(stringResource(R.string.netplay_players_label)) },
- ) {
- PlayersTable(
- rows = buildList {
- add(
- listOf(
- stringResource(R.string.netplay_players_name),
- stringResource(R.string.netplay_players_ping),
- stringResource(R.string.netplay_players_mapping),
- )
- )
- addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) })
- repeat(4 - players.size) { add(listOf("", "", "")) }
- },
- modifier = Modifier
- .fillMaxWidth()
- )
- }
+ Players(
+ players = players,
+ isHosting = isHosting,
+ controllerMapping = controllerMapping,
+ onGamecubePortChanged = onGamecubePortChanged,
+ onWiimotePortChanged = onWiimotePortChanged,
+ showControllerMapping = showControllerMapping,
+ onShowControllerMappingChanged = onShowControllerMappingChanged,
+ )
if (isHosting) {
MenuSpacer()
@@ -932,6 +990,64 @@ private fun AddressRow(
)
}
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun Players(
+ players: List<Player>,
+ isHosting: Boolean,
+ controllerMapping: ControllerMapping,
+ onGamecubePortChanged: (Int, Player?) -> Unit,
+ onWiimotePortChanged: (Int, Player?) -> Unit,
+ showControllerMapping: Boolean,
+ onShowControllerMappingChanged: (Boolean) -> Unit,
+) {
+ val sheetState = rememberSheetState(
+ skipPartiallyExpanded = true,
+ initialValue = if (showControllerMapping) SheetValue.Expanded else SheetValue.Hidden,
+ )
+
+ if (showControllerMapping) {
+ ModalBottomSheet(
+ onDismissRequest = { onShowControllerMappingChanged(false) },
+ sheetState = sheetState,
+ modifier = Modifier
+ .statusBarsPadding()
+ ) {
+ ControllerMapping(
+ mapping = controllerMapping,
+ players = players,
+ onGamecubePortChanged = onGamecubePortChanged,
+ onWiimotePortChanged = onWiimotePortChanged,
+ )
+ }
+ }
+
+ OutlinedBox(
+ label = { Text(stringResource(R.string.netplay_players_label)) },
+ onClick = if (isHosting) {
+ { onShowControllerMappingChanged(true) }
+ } else {
+ null
+ },
+ ) {
+ PlayersTable(
+ rows = buildList {
+ add(
+ listOf(
+ stringResource(R.string.netplay_players_name),
+ stringResource(R.string.netplay_players_ping),
+ stringResource(R.string.netplay_players_mapping),
+ )
+ )
+ addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) })
+ repeat(4 - players.size) { add(listOf("", "", "")) }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ }
+}
+
/**
* A table arranged into columns sized to wrap the largest item. Except the
* first column which takes up the remaining space left by the other columns.
@@ -944,7 +1060,6 @@ private fun PlayersTable(
) {
rows.zipWithNext { a, b -> if (a.size != b.size) throw IllegalArgumentException("Rows must all contain the same number of elements.") }
val maxWidths = remember { List(rows.first().size) { mutableIntStateOf(0) } }
- val density = LocalDensity.current
Column(
verticalArrangement = Arrangement.spacedBy(6.dp),
@@ -956,23 +1071,21 @@ private fun PlayersTable(
) {
row.forEachIndexed { itemIndex, text ->
Box(
- modifier = Modifier
- .then(
- when {
- itemIndex == 0 -> Modifier.weight(1f)
-
- maxWidths[itemIndex].intValue > 0 -> Modifier
- .width(with(density) { maxWidths[itemIndex].intValue.toDp() })
-
- else -> Modifier
+ modifier = if (itemIndex == 0) {
+ Modifier.weight(1f)
+ } else {
+ val maxWidth = maxWidths[itemIndex]
+ Modifier.layout { measurable, constraints ->
+ val placeable =
+ measurable.measure(constraints.copy(maxWidth = Constraints.Infinity))
+ if (placeable.width > maxWidth.intValue) {
+ maxWidth.intValue = placeable.width
}
- )
- .onGloballyPositioned { coordinates ->
- val width = coordinates.size.width
- if (width > maxWidths[itemIndex].intValue) {
- maxWidths[itemIndex].intValue = width
+ layout(maxWidth.intValue, placeable.height) {
+ placeable.place(x = 0, y = 0)
}
}
+ }
) {
Text(
text = text,
@@ -989,6 +1102,150 @@ private fun PlayersTable(
}
}
+private val PORT_DROPDOWN_MIN_WIDTH = 140.dp
+private val PORT_DROPDOWN_SPACING = 10.dp
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ControllerMapping(
+ mapping: ControllerMapping,
+ players: List<Player>,
+ onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
+ onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
+) {
+ BoxWithConstraints(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = DolphinTheme.scaffoldPadding)
+ .padding(bottom = 24.dp)
+ .navigationBarsPadding()
+ ) {
+ val columns =
+ ((maxWidth + PORT_DROPDOWN_SPACING) / (PORT_DROPDOWN_MIN_WIDTH + PORT_DROPDOWN_SPACING))
+ .toInt()
+ .coerceAtLeast(1)
+ .let { if (it == 3) 2 else it } // Three per row would look strange so bump down to two.
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(20.dp),
+ ) {
+ PortsSection(
+ titleId = R.string.netplay_controllers_gamecube,
+ portLabelId = R.string.netplay_controllers_port,
+ mapping = mapping.gamecubePorts,
+ players = players,
+ onPortChanged = onGamecubePortChanged,
+ columns = columns,
+ )
+
+ HorizontalDivider()
+
+ PortsSection(
+ titleId = R.string.netplay_controllers_wii_remotes,
+ portLabelId = R.string.netplay_controllers_remote,
+ mapping = mapping.wiiRemotes,
+ players = players,
+ onPortChanged = onWiimotePortChanged,
+ columns = columns,
+ )
+ }
+ }
+}
+
+@Composable
+private fun PortsSection(
+ @StringRes titleId: Int,
+ @StringRes portLabelId: Int,
+ mapping: List<Player?>,
+ players: List<Player>,
+ onPortChanged: (port: Int, player: Player?) -> Unit,
+ columns: Int,
+) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(PORT_DROPDOWN_SPACING),
+ ) {
+ Text(
+ text = stringResource(titleId),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+
+ mapping.indices.chunked(columns).forEach { row ->
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(PORT_DROPDOWN_SPACING),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ ) {
+ row.forEach { port ->
+ PortDropdown(
+ label = stringResource(portLabelId, port + 1),
+ selectedPlayer = mapping[port],
+ players = players,
+ onPlayerSelected = { onPortChanged(port, it) },
+ modifier = Modifier
+ .weight(1f)
+ )
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun PortDropdown(
+ label: String,
+ selectedPlayer: Player?,
+ players: List<Player>,
+ onPlayerSelected: (player: Player?) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ var expanded by remember { mutableStateOf(false) }
+ fun Player.displayName() = "$name ($pid)"
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ modifier = modifier,
+ ) {
+ OutlinedTextField(
+ value = selectedPlayer?.displayName()
+ ?: stringResource(R.string.netplay_controllers_none),
+ onValueChange = {},
+ readOnly = true,
+ singleLine = true,
+ label = { Text(label) },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
+ colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth(),
+ )
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+ DropdownMenuItem(
+ text = { Text(stringResource(R.string.netplay_controllers_none)) },
+ onClick = {
+ onPlayerSelected(null)
+ expanded = false
+ },
+ )
+ players.forEach { player ->
+ DropdownMenuItem(
+ text = { Text(player.displayName()) },
+ onClick = {
+ onPlayerSelected(player)
+ expanded = false
+ },
+ )
+ }
+ }
+ }
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun NetworkModeDropdown(
@@ -1274,6 +1531,73 @@ private fun GameDigestPlayerRow(
}
@Composable
+private fun DualCoreWarningDialog(
+ onSetDualCoreEnabled: (Boolean) -> Unit,
+ onStartGame: () -> Unit,
+ onSkipWarning: () -> Unit,
+ onDismiss: () -> Unit,
+) {
+ var dualCoreEnabled by rememberSaveable { mutableStateOf(true) }
+ AlertDialog(
+ title = { Text(stringResource(R.string.netplay_dual_core_warning_title)) },
+ text = {
+ Column {
+ Text(
+ text = stringResource(R.string.netplay_dual_core_warning_message),
+ )
+ val ripplePadding = 12.dp
+ Spacer(Modifier.height(12.dp))
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .layout { measurable, constraints ->
+ // Enlarge the ripple area beyond the dialog's padding
+ val hPx = ripplePadding.roundToPx()
+ val placeable = measurable.measure(
+ constraints.copy(
+ minWidth = constraints.maxWidth + hPx * 2,
+ maxWidth = constraints.maxWidth + hPx * 2
+ )
+ )
+ layout(constraints.maxWidth, placeable.height) {
+ placeable.place(-hPx, 0)
+ }
+ }
+ .clip(MaterialTheme.shapes.small)
+ .clickable {
+ dualCoreEnabled = !dualCoreEnabled
+ onSetDualCoreEnabled(dualCoreEnabled)
+ }
+ .padding(ripplePadding),
+ ) {
+ Text(
+ text = stringResource(R.string.dual_core),
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.weight(1f),
+ )
+ Switch(
+ checked = dualCoreEnabled,
+ onCheckedChange = null,
+ )
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onStartGame) {
+ Text(stringResource(R.string.netplay_start))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onSkipWarning) {
+ Text(stringResource(R.string.netplay_dont_warn_again))
+ }
+ },
+ onDismissRequest = onDismiss,
+ )
+}
+
+@Composable
private fun NetplayMessage.color(): Color {
val isDark = isSystemInDarkTheme()
return when (this) {
@@ -1361,6 +1685,9 @@ private fun PreviewNetplayScreen() {
onGameSelected = {},
gameFiles = emptyList(),
notAllPlayersHaveGame = emptyFlow(),
+ dualCoreWarning = emptyFlow(),
+ onSetDualCoreEnabled = {},
+ onSkipDualCoreWarning = {},
onConfirmStartGame = {},
hostInputAuthorityEnabled = true,
networkMode = NetworkMode.HOST_INPUT_AUTHORITY,
@@ -1375,6 +1702,9 @@ private fun PreviewNetplayScreen() {
JoinInfoType.EXTERNAL to JoinAddress.Loaded("203.0.113.1:2626"),
JoinInfoType.LOCAL to JoinAddress.Loaded("192.168.1.5:2626"),
),
+ controllerMapping = ControllerMapping.emptyControllerMapping(),
+ onGamecubePortChanged = { _, _ -> },
+ onWiiRemoteChanged = { _, _ -> },
// saveTransferProgress = SaveTransferProgress(
// title = "Title",
// totalSize = 1024L,
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
index 68c859082c..47f05f14ca 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
@@ -947,7 +947,8 @@ enum class BooleanSetting(
"ProgressEnabled",
false
),
- NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false);
+ NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false),
+ NETPLAY_SKIP_DUAL_CORE_WARNING(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "SkipDualCoreWarning", false);
override val isOverridden: Boolean
get() = NativeConfig.isOverridden(file, section, key)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsItem.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsItem.kt
index 4d41676a14..20cfdcb2d0 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsItem.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsItem.kt
@@ -86,5 +86,6 @@ abstract class SettingsItem {
const val TYPE_STRING = 12
const val TYPE_HYPERLINK_HEADER = 13
const val TYPE_DATETIME_CHOICE = 14
+ const val TYPE_SEARCH_RESULT = 15
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsSearchResult.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsSearchResult.kt
new file mode 100644
index 0000000000..dc786851ac
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsSearchResult.kt
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.settings.model.view
+
+import android.os.Bundle
+import org.dolphinemu.dolphinemu.features.settings.model.AbstractSetting
+import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag
+
+class SettingsSearchResult(
+ name: CharSequence,
+ description: CharSequence,
+ val menuKey: MenuTag,
+ val settingPosition: Int,
+ val navigationExtras: Bundle?
+) : SettingsItem(name, description) {
+ override val type: Int = TYPE_SEARCH_RESULT
+
+ override val setting: AbstractSetting? = null
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt
index 68f957a626..19c08b9a05 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt
@@ -7,14 +7,17 @@ import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
-import android.view.Menu
import android.view.MotionEvent
import android.view.View
+import android.view.animation.PathInterpolator
import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.SearchView
import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
@@ -39,6 +42,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
private var dialog: AlertDialog? = null
private var toolbarLayout: CollapsingToolbarLayout? = null
private var binding: ActivitySettingsBinding? = null
+ private lateinit var searchView: SearchView
+ private var expandedToolbarHeight = 0
+ private var toolbarStateGeneration = 0
+ private var currentToolbarTitle: String? = null
+ private var currentToolbarShowsHeadline = false
+ private var currentToolbarShowsSearch = false
+ private var currentToolbarShowsSearchMode = false
+ override val settingsSearchQuery: String
+ get() = presenter!!.settingsSearchQuery
+ override val isSettingsSearchActive: Boolean
+ get() = presenter!!.isSettingsSearchActive
override var themeId: Int = 0
override var isMappingAllDevices = false
@@ -76,8 +90,11 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
presenter = SettingsActivityPresenter(this, settings)
presenter!!.onCreate(savedInstanceState, menuTag, gameID, revision, isWii, this)
toolbarLayout = binding!!.toolbarSettingsLayout
+ expandedToolbarHeight = toolbarLayout!!.layoutParams.height
setSupportActionBar(binding!!.toolbarSettings)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+ setUpSettingsSearch()
+ setUpBackNavigation()
// TODO: Remove this when CollapsingToolbarLayouts are fixed by Google
// https://github.com/material-components/material-components-android/issues/1310
@@ -86,16 +103,78 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
enableScrollTint(this, binding!!.toolbarSettings, binding!!.appbarSettings)
}
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- val inflater = menuInflater
- inflater.inflate(R.menu.menu_settings, menu)
- return true
+ private fun setUpSettingsSearch() {
+ searchView = binding!!.settingsSearch
+ searchView.setQuery(settingsSearchQuery, false)
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ searchView.clearFocus()
+ return true
+ }
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ presenter!!.onSettingsSearchQueryChanged(newText.orEmpty())
+ return true
+ }
+ })
+ binding!!.settingsSearchPreview.setOnClickListener { enterSettingsSearch() }
+ binding!!.settingsSearchToolbar.setNavigationOnClickListener { exitSettingsSearch() }
+ }
+
+ private fun enterSettingsSearch() {
+ if (!presenter!!.enterSettingsSearch()) {
+ return
+ }
+
+ refreshToolbarState()
+ val focusDelay = if (areSystemAnimationsEnabled()) SEARCH_FOCUS_DELAY_MS else 0L
+ searchView.postDelayed({
+ if (!isSettingsSearchActive) {
+ return@postDelayed
+ }
+ searchView.requestFocus()
+ WindowCompat.getInsetsController(window, searchView).show(WindowInsetsCompat.Type.ime())
+ }, focusDelay)
+ }
+
+ private fun exitSettingsSearch() {
+ if (!presenter!!.exitSettingsSearch()) {
+ return
+ }
+
+ searchView.setQuery("", false)
+ searchView.clearFocus()
+ WindowCompat.getInsetsController(window, searchView).hide(WindowInsetsCompat.Type.ime())
+ refreshToolbarState()
+ }
+
+ private fun refreshToolbarState() {
+ val title = currentToolbarTitle ?: getString(R.string.settings)
+ setToolbarState(
+ title, currentToolbarShowsHeadline, currentToolbarShowsSearch
+ )
+ }
+
+ private fun setUpBackNavigation() {
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (supportFragmentManager.backStackEntryCount == 0 && isSettingsSearchActive) {
+ exitSettingsSearch()
+ return
+ }
+
+ isEnabled = false
+ onBackPressedDispatcher.onBackPressed()
+ isEnabled = true
+ }
+ })
}
override fun onSaveInstanceState(outState: Bundle) {
// Critical: If super method is not called, rotations will be busted.
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_MAPPING_ALL_DEVICES, isMappingAllDevices)
+ presenter!!.onSaveInstanceState(outState)
}
override fun onStart() {
@@ -128,10 +207,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
}
override fun showSettingsFragment(
+ menuTag: MenuTag, extras: Bundle?, addToStack: Boolean, gameId: String
+ ) {
+ replaceSettingsFragment(menuTag, extras, addToStack, gameId, false)
+ }
+
+ private fun replaceSettingsFragment(
menuTag: MenuTag,
extras: Bundle?,
addToStack: Boolean,
- gameId: String
+ gameId: String,
+ isSearchResult: Boolean
) {
if (!addToStack && fragment != null) return
val transaction = supportFragmentManager.beginTransaction()
@@ -140,15 +226,18 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
transaction.setCustomAnimations(
R.anim.anim_settings_fragment_in,
R.anim.anim_settings_fragment_out,
- 0,
- R.anim.anim_pop_settings_fragment_out
+ if (isSearchResult) R.anim.anim_settings_search_pop_in else 0,
+ if (isSearchResult) {
+ R.anim.anim_settings_search_pop_out
+ } else {
+ R.anim.anim_pop_settings_fragment_out
+ }
)
}
transaction.addToBackStack(null)
}
transaction.replace(
- R.id.frame_content_settings,
- newInstance(menuTag, gameId, extras), FRAGMENT_TAG
+ R.id.frame_content_settings, newInstance(menuTag, gameId, extras), FRAGMENT_TAG
)
transaction.commit()
}
@@ -157,16 +246,22 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
fragment.show(supportFragmentManager, FRAGMENT_DIALOG_TAG)
}
+ override fun showSearchResult(
+ menuTag: MenuTag, settingPosition: Int, gameId: String, extras: Bundle?
+ ) {
+ val navigationExtras = extras?.let(::Bundle) ?: Bundle()
+ navigationExtras.putInt(
+ SettingsFragment.ARGUMENT_SCROLL_TO_SETTING_POSITION, settingPosition
+ )
+ replaceSettingsFragment(menuTag, navigationExtras, true, gameId, true)
+ }
+
private fun areSystemAnimationsEnabled(): Boolean {
val duration = android.provider.Settings.Global.getFloat(
- contentResolver,
- android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
- 1f
+ contentResolver, android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1f
)
val transition = android.provider.Settings.Global.getFloat(
- contentResolver,
- android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE,
- 1f
+ contentResolver, android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, 1f
)
return duration != 0f && transition != 0f
}
@@ -183,10 +278,8 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
override fun showLoading() {
if (dialog == null) {
- dialog = MaterialAlertDialogBuilder(this)
- .setTitle(getString(R.string.load_settings))
- .setView(R.layout.dialog_indeterminate_progress)
- .create()
+ dialog = MaterialAlertDialogBuilder(this).setTitle(getString(R.string.load_settings))
+ .setView(R.layout.dialog_indeterminate_progress).create()
}
dialog!!.show()
}
@@ -196,12 +289,10 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
}
override fun showGameIniJunkDeletionQuestion() {
- MaterialAlertDialogBuilder(this)
- .setTitle(getString(R.string.game_ini_junk_title))
+ MaterialAlertDialogBuilder(this).setTitle(getString(R.string.game_ini_junk_title))
.setMessage(getString(R.string.game_ini_junk_question))
.setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> presenter!!.clearGameSettings() }
- .setNegativeButton(R.string.no, null)
- .show()
+ .setNegativeButton(R.string.no, null).show()
}
override fun onSettingsFileLoaded(settings: Settings) {
@@ -229,13 +320,78 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
return presenter!!.hasMenuTagActionForValue(menuTag, value)
}
+ override fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle? {
+ return presenter!!.getMenuTagActionExtras(menuTag, value)
+ }
+
+ override fun filterSettings(query: String) {
+ fragment?.filterSettings(query)
+ }
+
override fun onSupportNavigateUp(): Boolean {
- onBackPressed()
+ onBackPressedDispatcher.onBackPressed()
return true
}
- override fun setToolbarTitle(title: String) {
- binding!!.toolbarSettingsLayout.title = title
+ override fun setToolbarState(title: String, showHeadline: Boolean, showSearch: Boolean) {
+ val appBar = binding!!.appbarSettings
+ val generation = ++toolbarStateGeneration
+ val showSearchMode = showSearch && isSettingsSearchActive
+ val stateChanged =
+ currentToolbarTitle != title || currentToolbarShowsHeadline != showHeadline || currentToolbarShowsSearch != showSearch || currentToolbarShowsSearchMode != showSearchMode
+ appBar.animate().cancel()
+
+ if (!appBar.isLaidOut || !stateChanged) {
+ applyToolbarState(title, showHeadline, showSearch)
+ appBar.alpha = 1f
+ return
+ }
+
+ if (!showSearch) {
+ searchView.clearFocus()
+ }
+
+ appBar.animate().alpha(0f).setDuration(APP_BAR_FADE_OUT_DURATION_MS)
+ .setInterpolator(APP_BAR_FADE_OUT_INTERPOLATOR).withEndAction {
+ if (generation != toolbarStateGeneration) {
+ return@withEndAction
+ }
+
+ applyToolbarState(title, showHeadline, showSearch)
+ appBar.post {
+ if (generation != toolbarStateGeneration) {
+ return@post
+ }
+
+ appBar.animate().alpha(1f).setDuration(APP_BAR_FADE_IN_DURATION_MS)
+ .setInterpolator(APP_BAR_FADE_IN_INTERPOLATOR).start()
+ }
+ }.start()
+ }
+
+ private fun applyToolbarState(title: String, showHeadline: Boolean, showSearch: Boolean) {
+ val showSearchMode = showSearch && isSettingsSearchActive
+ toolbarLayout!!.isTitleEnabled = showHeadline
+ supportActionBar!!.title = title
+ if (showHeadline) {
+ toolbarLayout!!.title = title
+ }
+ toolbarLayout!!.layoutParams = toolbarLayout!!.layoutParams.apply {
+ height = if (showHeadline) {
+ expandedToolbarHeight
+ } else {
+ binding!!.toolbarSettings.layoutParams.height
+ }
+ }
+ toolbarLayout!!.visibility = if (showSearchMode) View.GONE else View.VISIBLE
+ binding!!.settingsSearchContainer.visibility =
+ if (showSearch && !showSearchMode) View.VISIBLE else View.GONE
+ binding!!.settingsSearchModeContainer.visibility =
+ if (showSearchMode) View.VISIBLE else View.GONE
+ currentToolbarTitle = title
+ currentToolbarShowsHeadline = showHeadline
+ currentToolbarShowsSearch = showSearch
+ currentToolbarShowsSearchMode = showSearchMode
}
override fun setOldControllerSettingsWarningVisibility(visible: Boolean): Int {
@@ -274,14 +430,16 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
private const val KEY_MAPPING_ALL_DEVICES = "all_devices"
private const val FRAGMENT_TAG = "settings"
private const val FRAGMENT_DIALOG_TAG = "settings_dialog"
+ private const val APP_BAR_FADE_OUT_DURATION_MS = 90L
+ private const val APP_BAR_FADE_IN_DURATION_MS = 180L
+ private const val SEARCH_FOCUS_DELAY_MS =
+ APP_BAR_FADE_OUT_DURATION_MS + APP_BAR_FADE_IN_DURATION_MS
+ private val APP_BAR_FADE_OUT_INTERPOLATOR = PathInterpolator(0.4f, 0f, 1f, 1f)
+ private val APP_BAR_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f)
@JvmStatic
fun launch(
- context: Context,
- menuTag: MenuTag?,
- gameId: String?,
- revision: Int,
- isWii: Boolean
+ context: Context, menuTag: MenuTag?, gameId: String?, revision: Int, isWii: Boolean
) {
val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag)
@@ -296,8 +454,7 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag)
settings.putExtra(
- ARG_IS_WII,
- !NativeLibrary.IsRunning() || NativeLibrary.IsEmulatingWii()
+ ARG_IS_WII, !NativeLibrary.IsRunning() || NativeLibrary.IsEmulatingWii()
)
context.startActivity(settings)
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt
index 7627a7b5c6..5baee17f02 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt
@@ -10,14 +10,17 @@ import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner
import org.dolphinemu.dolphinemu.utils.Log
class SettingsActivityPresenter(
- private val activityView: SettingsActivityView,
- var settings: Settings?
+ private val activityView: SettingsActivityView, var settings: Settings?
) {
private var menuTag: MenuTag? = null
private var gameId: String? = null
private var revision = 0
private var isWii = false
private lateinit var activity: AppCompatActivity
+ var settingsSearchQuery = ""
+ private set
+ var isSettingsSearchActive = false
+ private set
fun onCreate(
savedInstanceState: Bundle?,
@@ -32,6 +35,43 @@ class SettingsActivityPresenter(
this.revision = revision
this.isWii = isWii
this.activity = activity
+ if (savedInstanceState != null) {
+ isSettingsSearchActive =
+ savedInstanceState.getBoolean(KEY_SETTINGS_SEARCH_ACTIVE)
+ settingsSearchQuery =
+ savedInstanceState.getString(KEY_SETTINGS_SEARCH_QUERY).orEmpty()
+ }
+ }
+
+ fun onSaveInstanceState(outState: Bundle) {
+ outState.putBoolean(KEY_SETTINGS_SEARCH_ACTIVE, isSettingsSearchActive)
+ outState.putString(KEY_SETTINGS_SEARCH_QUERY, settingsSearchQuery)
+ }
+
+ fun onSettingsSearchQueryChanged(query: String) {
+ settingsSearchQuery = query
+ activityView.filterSettings(query)
+ }
+
+ fun enterSettingsSearch(): Boolean {
+ if (isSettingsSearchActive) {
+ return false
+ }
+
+ isSettingsSearchActive = true
+ activityView.filterSettings(settingsSearchQuery)
+ return true
+ }
+
+ fun exitSettingsSearch(): Boolean {
+ if (!isSettingsSearchActive) {
+ return false
+ }
+
+ isSettingsSearchActive = false
+ settingsSearchQuery = ""
+ activityView.filterSettings("")
+ return true
}
fun onDestroy() {
@@ -85,55 +125,49 @@ class SettingsActivityPresenter(
}
fun onMenuTagAction(menuTag: MenuTag, value: Int) {
- if (menuTag.isSerialPort1Menu) {
- // Not disabled or dummy
- if (value != 0 && value != 255) {
- val bundle = Bundle()
- bundle.putInt(SettingsFragmentPresenter.ARG_SERIALPORT1_TYPE, value)
- activityView.showSettingsFragment(menuTag, bundle, true, gameId!!)
- }
- }
- if (menuTag.isGCPadMenu) {
- // Not disabled
- if (value != 0)
- {
- val bundle = Bundle()
- bundle.putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
- activityView.showSettingsFragment(menuTag, bundle, true, gameId!!)
- }
- }
- if (menuTag.isWiimoteMenu) {
- // Emulated Wii Remote
- if (value == 1) {
- activityView.showSettingsFragment(menuTag, null, true, gameId!!)
- }
- }
- if (menuTag.isWiimoteExtensionMenu) {
- // Not disabled
- if (value != 0) {
- val bundle = Bundle()
- bundle.putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
- activityView.showSettingsFragment(menuTag, bundle, true, gameId!!)
- }
- }
+ val action = getMenuTagAction(menuTag, value) ?: return
+ activityView.showSettingsFragment(action.menuTag, action.extras, true, gameId!!)
}
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean {
- if (menuTag.isSerialPort1Menu) {
+ return getMenuTagAction(menuTag, value) != null
+ }
+
+ fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle? {
+ return getMenuTagAction(menuTag, value)?.extras
+ }
+
+ private fun getMenuTagAction(menuTag: MenuTag, value: Int): MenuTagAction? {
+ return when {
// Not disabled or dummy
- return value != 0 && value != 255
- }
- if (menuTag.isGCPadMenu) {
+ menuTag.isSerialPort1Menu && value != 0 && value != 255 -> MenuTagAction(
+ menuTag, Bundle().apply {
+ putInt(SettingsFragmentPresenter.ARG_SERIALPORT1_TYPE, value)
+ })
+
// Not disabled
- return value != 0
- }
- if (menuTag.isWiimoteMenu) {
+ menuTag.isGCPadMenu && value != 0 -> MenuTagAction(
+ menuTag, Bundle().apply {
+ putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
+ })
+
// Emulated Wii Remote
- return value == 1
- }
- return if (menuTag.isWiimoteExtensionMenu) {
+ menuTag.isWiimoteMenu && value == 1 -> MenuTagAction(menuTag, null)
+
// Not disabled
- value != 0
- } else false
+ menuTag.isWiimoteExtensionMenu && value != 0 -> MenuTagAction(
+ menuTag, Bundle().apply {
+ putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
+ })
+
+ else -> null
+ }
+ }
+
+ private data class MenuTagAction(val menuTag: MenuTag, val extras: Bundle?)
+
+ companion object {
+ private const val KEY_SETTINGS_SEARCH_ACTIVE = "settings_search_active"
+ private const val KEY_SETTINGS_SEARCH_QUERY = "settings_search_query"
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt
index 1cd39f6fe4..95951f1ae9 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt
@@ -11,19 +11,36 @@ import org.dolphinemu.dolphinemu.features.settings.model.Settings
*/
interface SettingsActivityView {
/**
+ * The query currently displayed in the settings search view.
+ */
+ val settingsSearchQuery: String
+
+ /**
+ * Whether the dedicated settings search screen is active.
+ */
+ val isSettingsSearchActive: Boolean
+
+ /**
* Show a new SettingsFragment.
*
* @param menuTag Identifier for the settings group that should be displayed.
* @param addToStack Whether or not this fragment should replace a previous one.
*/
fun showSettingsFragment(
- menuTag: MenuTag,
- extras: Bundle?,
- addToStack: Boolean,
- gameId: String
+ menuTag: MenuTag, extras: Bundle?, addToStack: Boolean, gameId: String
)
/**
+ * Opens the settings screen containing a search result and scrolls to the result.
+ */
+ fun showSearchResult(menuTag: MenuTag, settingPosition: Int, gameId: String, extras: Bundle?)
+
+ /**
+ * Filters the root settings screen using the current search query.
+ */
+ fun filterSettings(query: String)
+
+ /**
* Shows a DialogFragment.
*
* Only one can be shown at a time.
@@ -87,6 +104,11 @@ interface SettingsActivityView {
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean
/**
+ * Returns the arguments used when opening a navigable setting's associated screen.
+ */
+ fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle?
+
+ /**
* Show loading dialog while loading the settings
*/
fun showLoading()
@@ -102,9 +124,9 @@ interface SettingsActivityView {
fun showGameIniJunkDeletionQuestion()
/**
- * Accesses the material toolbar layout and changes the title
+ * Updates the settings app bar as a single state change.
*/
- fun setToolbarTitle(title: String)
+ fun setToolbarState(title: String, showHeadline: Boolean, showSearch: Boolean)
/**
* Returns whether the input mapping dialog should detect inputs from all devices,
* not just the device configured for the controller.
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt
index ae236d3764..6252350d32 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt
@@ -27,14 +27,46 @@ import com.google.android.material.slider.Slider
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import org.dolphinemu.dolphinemu.R
-import org.dolphinemu.dolphinemu.databinding.*
+import org.dolphinemu.dolphinemu.databinding.DialogAdvancedMappingBinding
+import org.dolphinemu.dolphinemu.databinding.DialogInputStringBinding
+import org.dolphinemu.dolphinemu.databinding.DialogSliderBinding
+import org.dolphinemu.dolphinemu.databinding.ListItemHeaderBinding
+import org.dolphinemu.dolphinemu.databinding.ListItemMappingBinding
+import org.dolphinemu.dolphinemu.databinding.ListItemSearchResultBinding
+import org.dolphinemu.dolphinemu.databinding.ListItemSettingBinding
+import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding
+import org.dolphinemu.dolphinemu.databinding.ListItemSubmenuBinding
import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting
import org.dolphinemu.dolphinemu.features.input.ui.AdvancedMappingDialog
import org.dolphinemu.dolphinemu.features.input.ui.MotionAlertDialog
import org.dolphinemu.dolphinemu.features.input.ui.viewholder.InputMappingControlSettingViewHolder
import org.dolphinemu.dolphinemu.features.settings.model.Settings
-import org.dolphinemu.dolphinemu.features.settings.model.view.*
-import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.*
+import org.dolphinemu.dolphinemu.features.settings.model.view.DateTimeChoiceSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.DirectoryPicker
+import org.dolphinemu.dolphinemu.features.settings.model.view.FilePicker
+import org.dolphinemu.dolphinemu.features.settings.model.view.FloatSliderSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.InputStringSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.IntSliderSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
+import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsSearchResult
+import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSettingDynamicDescriptions
+import org.dolphinemu.dolphinemu.features.settings.model.view.SliderSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.StringSingleChoiceSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.SubmenuSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.SwitchSetting
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.DateTimeSettingViewHolder
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.FilePickerViewHolder
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.HeaderHyperLinkViewHolder
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.HeaderViewHolder
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.InputStringSettingViewHolder
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.RunRunnableViewHolder
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingsSearchResultViewHolder
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SingleChoiceViewHolder
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SliderViewHolder
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SubmenuViewHolder
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SwitchSettingViewHolder
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper
import org.dolphinemu.dolphinemu.utils.Log
@@ -42,14 +74,13 @@ import org.dolphinemu.dolphinemu.utils.PermissionsHandler
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
-import java.util.*
+import java.util.Calendar
+import java.util.TimeZone
import kotlin.math.roundToInt
class SettingsAdapter(
- private val fragmentView: SettingsFragmentView,
- private val context: Context
-) :
- RecyclerView.Adapter<SettingViewHolder>(), DialogInterface.OnClickListener,
+ private val fragmentView: SettingsFragmentView, private val context: Context
+) : RecyclerView.Adapter<SettingViewHolder>(), DialogInterface.OnClickListener,
Slider.OnChangeListener {
private var settingsList: ArrayList<SettingsItem>? = null
private var clickedItem: SettingsItem? = null
@@ -68,55 +99,59 @@ class SettingsAdapter(
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
SettingsItem.TYPE_HEADER -> HeaderViewHolder(
- ListItemHeaderBinding.inflate(inflater, parent, false),
- this
+ ListItemHeaderBinding.inflate(inflater, parent, false), this
)
+
SettingsItem.TYPE_SWITCH -> SwitchSettingViewHolder(
- ListItemSettingSwitchBinding.inflate(inflater, parent, false),
- this
+ ListItemSettingSwitchBinding.inflate(inflater, parent, false), this
)
- SettingsItem.TYPE_STRING_SINGLE_CHOICE,
- SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS,
- SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder(
- ListItemSettingBinding.inflate(inflater, parent, false),
- this
+
+ SettingsItem.TYPE_STRING_SINGLE_CHOICE, SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS, SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder(
+ ListItemSettingBinding.inflate(inflater, parent, false), this
)
+
SettingsItem.TYPE_SLIDER -> SliderViewHolder(
- ListItemSettingBinding.inflate(inflater, parent, false),
- this,
- context
+ ListItemSettingBinding.inflate(inflater, parent, false), this, context
)
+
SettingsItem.TYPE_SUBMENU -> SubmenuViewHolder(
- ListItemSubmenuBinding.inflate(inflater, parent, false),
- this
+ ListItemSubmenuBinding.inflate(inflater, parent, false), this
)
+
SettingsItem.TYPE_INPUT_MAPPING_CONTROL -> InputMappingControlSettingViewHolder(
- ListItemMappingBinding.inflate(inflater, parent, false),
- this
+ ListItemMappingBinding.inflate(inflater, parent, false), this
)
- SettingsItem.TYPE_FILE_PICKER,
- SettingsItem.TYPE_DIRECTORY_PICKER -> FilePickerViewHolder(
- ListItemSettingBinding.inflate(inflater, parent, false),
- this
+
+ SettingsItem.TYPE_FILE_PICKER, SettingsItem.TYPE_DIRECTORY_PICKER -> FilePickerViewHolder(
+ ListItemSettingBinding.inflate(inflater, parent, false), this
)
+
SettingsItem.TYPE_RUN_RUNNABLE -> RunRunnableViewHolder(
- ListItemSettingBinding.inflate(inflater, parent, false),
- this, context
+ ListItemSettingBinding.inflate(inflater, parent, false), this, context
)
+
SettingsItem.TYPE_STRING -> InputStringSettingViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false), this
)
+
SettingsItem.TYPE_HYPERLINK_HEADER -> HeaderHyperLinkViewHolder(
ListItemHeaderBinding.inflate(inflater, parent, false), this
)
+
SettingsItem.TYPE_DATETIME_CHOICE -> DateTimeSettingViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false), this
)
+
+ SettingsItem.TYPE_SEARCH_RESULT -> SettingsSearchResultViewHolder(
+ ListItemSearchResultBinding.inflate(inflater, parent, false), this
+ )
+
else -> throw IllegalArgumentException("Invalid view type: $viewType")
}
}
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
+ holder.clearSearchResultHighlight()
holder.bind(getItem(position))
}
@@ -143,7 +178,7 @@ class SettingsAdapter(
fun clearSetting(item: SettingsItem) {
item.clear(settings!!)
- fragmentView.onSettingChanged()
+ fragmentView.onSettingChanged(item)
}
fun notifyAllSettingsChanged() {
@@ -153,7 +188,7 @@ class SettingsAdapter(
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
item.setChecked(settings!!, checked)
- fragmentView.onSettingChanged()
+ fragmentView.onSettingChanged(item)
}
fun onInputStringClick(item: InputStringSetting, position: Int) {
@@ -161,29 +196,24 @@ class SettingsAdapter(
val binding = DialogInputStringBinding.inflate(inflater)
val input = binding.input
input.setText(item.selectedValue)
- dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
- .setView(binding.root)
+ dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setView(binding.root)
.setMessage(item.description)
.setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int ->
val editTextInput = input.text.toString()
if (item.selectedValue != editTextInput) {
notifyItemChanged(position)
- fragmentView.onSettingChanged()
+ fragmentView.onSettingChanged(item)
}
item.setSelectedValue(fragmentView.settings!!, editTextInput)
- }
- .setNegativeButton(R.string.cancel, null)
- .show()
+ }.setNegativeButton(R.string.cancel, null).show()
}
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
clickedItem = item
clickedPosition = position
val value = getSelectionForSingleChoiceValue(item)
- dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
- .setTitle(item.name)
- .setSingleChoiceItems(item.choicesId, value, this)
- .show()
+ dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
+ .setSingleChoiceItems(item.choicesId, value, this).show()
}
fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
@@ -193,35 +223,26 @@ class SettingsAdapter(
val choices = item.choices
val noChoicesAvailableString = item.noChoicesAvailableString
dialog = if (noChoicesAvailableString != 0 && choices.isEmpty()) {
- MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
- .setTitle(item.name)
- .setMessage(noChoicesAvailableString)
- .setPositiveButton(R.string.ok, null)
- .show()
+ MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
+ .setMessage(noChoicesAvailableString).setPositiveButton(R.string.ok, null).show()
} else {
- MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
- .setTitle(item.name)
+ MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
.setSingleChoiceItems(
- item.choices, item.selectedValueIndex,
- this
- )
- .show()
+ item.choices, item.selectedValueIndex, this
+ ).show()
}
}
fun onSingleChoiceDynamicDescriptionsClick(
- item: SingleChoiceSettingDynamicDescriptions,
- position: Int
+ item: SingleChoiceSettingDynamicDescriptions, position: Int
) {
clickedItem = item
clickedPosition = position
val value = getSelectionForSingleChoiceDynamicDescriptionsValue(item)
- dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
- .setTitle(item.name)
- .setSingleChoiceItems(item.choicesId, value, this)
- .show()
+ dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
+ .setSingleChoiceItems(item.choicesId, value, this).show()
}
fun onSliderClick(item: SliderSetting, position: Int) {
@@ -251,6 +272,7 @@ class SettingsAdapter(
slider.valueTo = item.max
slider.stepSize = item.stepSize
}
+
is IntSliderSetting -> {
slider.valueFrom = item.min.toFloat()
slider.valueTo = item.max.toFloat()
@@ -260,29 +282,27 @@ class SettingsAdapter(
slider.value = (seekbarProgress / slider.stepSize).roundToInt() * slider.stepSize
slider.addOnChangeListener(this)
- dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
- .setTitle(item.name)
- .setView(binding.root)
- .setPositiveButton(R.string.ok, this)
- .show()
+ dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
+ .setView(binding.root).setPositiveButton(R.string.ok, this).show()
}
fun onSubmenuClick(item: SubmenuSetting) {
fragmentView.loadSubMenu(item.menuKey)
}
+ fun onSearchResultClick(item: SettingsSearchResult) {
+ fragmentView.loadSearchResult(item.menuKey, item.settingPosition, item.navigationExtras)
+ }
+
fun onInputMappingClick(item: InputMappingControlSetting, position: Int) {
if (item.controller.getDefaultDevice().isEmpty() && !fragmentView.isMappingAllDevices) {
- MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
- .setMessage(R.string.input_binding_no_device)
- .setPositiveButton(R.string.ok, this)
- .show()
+ MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setMessage(R.string.input_binding_no_device)
+ .setPositiveButton(R.string.ok, this).show()
return
}
val dialog = MotionAlertDialog(
- fragmentView.fragmentActivity, item,
- fragmentView.isMappingAllDevices
+ fragmentView.fragmentActivity, item, fragmentView.isMappingAllDevices
)
val background = ContextCompat.getDrawable(context, R.drawable.dialog_round)
@@ -296,18 +316,16 @@ class SettingsAdapter(
dialog.setTitle(R.string.input_binding)
dialog.setMessage(
String.format(
- context.getString(R.string.input_binding_description),
- item.name
+ context.getString(R.string.input_binding_description), item.name
)
)
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this)
dialog.setButton(
- AlertDialog.BUTTON_NEUTRAL,
- context.getString(R.string.clear)
+ AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.clear)
) { _: DialogInterface?, _: Int -> item.clearValue() }
dialog.setOnDismissListener {
notifyItemChanged(position)
- fragmentView.onSettingChanged()
+ fragmentView.onSettingChanged(item)
}
dialog.setCanceledOnTouchOutside(false)
dialog.show()
@@ -317,10 +335,7 @@ class SettingsAdapter(
val inflater = LayoutInflater.from(context)
val binding = DialogAdvancedMappingBinding.inflate(inflater)
val dialog = AdvancedMappingDialog(
- context,
- binding,
- item.controlReference,
- item.controller
+ context, binding, item.controlReference, item.controller
)
val background = ContextCompat.getDrawable(context, R.drawable.dialog_round)
@@ -338,12 +353,11 @@ class SettingsAdapter(
) { _: DialogInterface?, _: Int ->
item.value = dialog.expression
notifyItemChanged(position)
- fragmentView.onSettingChanged()
+ fragmentView.onSettingChanged(item)
}
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this)
dialog.setButton(
- AlertDialog.BUTTON_NEUTRAL,
- context.getString(R.string.clear)
+ AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.clear)
) { _: DialogInterface?, _: Int -> }
dialog.setCanceledOnTouchOutside(false)
dialog.show()
@@ -361,14 +375,12 @@ class SettingsAdapter(
val directoryPicker = item as DirectoryPicker
if (!PermissionsHandler.isExternalStorageLegacy()) {
- MaterialAlertDialogBuilder(context)
- .setMessage(R.string.path_not_changeable_scoped_storage)
+ MaterialAlertDialogBuilder(context).setMessage(R.string.path_not_changeable_scoped_storage)
.setPositiveButton(R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
.show()
} else {
val intent = FileBrowserHelper.createDirectoryPickerIntent(
- fragmentView.fragmentActivity,
- FileBrowserHelper.GAME_EXTENSIONS
+ fragmentView.fragmentActivity, FileBrowserHelper.GAME_EXTENSIONS
)
directoryPicker.launcher.launch(intent)
}
@@ -400,32 +412,24 @@ class SettingsAdapter(
calendar.timeZone = TimeZone.getTimeZone("UTC")
// Start and end epoch times available for the Wii's date picker
- val calendarConstraints = CalendarConstraints.Builder()
- .setStart(946684800000L)
- .setEnd(2082672000000L)
- .build()
+ val calendarConstraints =
+ CalendarConstraints.Builder().setStart(946684800000L).setEnd(2082672000000L).build()
var timeFormat = TimeFormat.CLOCK_12H
if (DateFormat.is24HourFormat(fragmentView.fragmentActivity)) {
timeFormat = TimeFormat.CLOCK_24H
}
- val datePicker = MaterialDatePicker.Builder.datePicker()
- .setSelection(storedTime)
- .setTitleText(R.string.select_rtc_date)
- .setCalendarConstraints(calendarConstraints)
- .build()
- val timePicker = MaterialTimePicker.Builder()
- .setTimeFormat(timeFormat)
- .setHour(calendar[Calendar.HOUR_OF_DAY])
- .setMinute(calendar[Calendar.MINUTE])
- .setTitleText(R.string.select_rtc_time)
+ val datePicker = MaterialDatePicker.Builder.datePicker().setSelection(storedTime)
+ .setTitleText(R.string.select_rtc_date).setCalendarConstraints(calendarConstraints)
.build()
+ val timePicker = MaterialTimePicker.Builder().setTimeFormat(timeFormat)
+ .setHour(calendar[Calendar.HOUR_OF_DAY]).setMinute(calendar[Calendar.MINUTE])
+ .setTitleText(R.string.select_rtc_time).build()
datePicker.addOnPositiveButtonClickListener {
timePicker.show(
- fragmentView.fragmentActivity.supportFragmentManager,
- "TimePicker"
+ fragmentView.fragmentActivity.supportFragmentManager, "TimePicker"
)
}
timePicker.addOnPositiveButtonClickListener {
@@ -435,7 +439,7 @@ class SettingsAdapter(
val rtcString = "0x" + java.lang.Long.toHexString(epochTime)
if (item.getSelectedValue() != rtcString) {
notifyItemChanged(clickedPosition)
- fragmentView.onSettingChanged()
+ fragmentView.onSettingChanged(item)
}
item.setSelectedValue(fragmentView.settings!!, rtcString)
clickedItem = null
@@ -448,7 +452,7 @@ class SettingsAdapter(
if (filePicker.getSelectedValue() != selectedFile) {
notifyItemChanged(clickedPosition)
- fragmentView.onSettingChanged()
+ fragmentView.onSettingChanged(filePicker)
}
filePicker.setSelectedValue(fragmentView.settings!!, selectedFile)
@@ -470,44 +474,50 @@ class SettingsAdapter(
val scSetting = clickedItem as SingleChoiceSetting
val value = getValueForSingleChoiceSelection(scSetting, which)
- if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
+ if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
scSetting.setSelectedValue(settings!!, value)
closeDialog()
}
+
is SingleChoiceSettingDynamicDescriptions -> {
val scSetting = clickedItem as SingleChoiceSettingDynamicDescriptions
val value = getValueForSingleChoiceDynamicDescriptionsSelection(scSetting, which)
- if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
+ if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
scSetting.setSelectedValue(settings!!, value)
closeDialog()
}
+
is StringSingleChoiceSetting -> {
val scSetting = clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which)
- if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
+ if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
scSetting.setSelectedValue(settings!!, value)
closeDialog()
}
+
is IntSliderSetting -> {
val sliderSetting = clickedItem as IntSliderSetting
if (sliderSetting.selectedValue != seekbarProgress.toInt()) {
- fragmentView.onSettingChanged()
+ fragmentView.onSettingChanged(sliderSetting)
}
sliderSetting.setSelectedValue(settings!!, seekbarProgress.toInt())
closeDialog()
}
+
is FloatSliderSetting -> {
val sliderSetting = clickedItem as FloatSliderSetting
- if (sliderSetting.selectedValue != seekbarProgress) fragmentView.onSettingChanged()
+ if (sliderSetting.selectedValue != seekbarProgress) {
+ fragmentView.onSettingChanged(sliderSetting)
+ }
sliderSetting.setSelectedValue(settings!!, seekbarProgress)
@@ -540,6 +550,7 @@ class SettingsAdapter(
override fun onViewRecycled(holder: SettingViewHolder) {
super.onViewRecycled(holder)
+ holder.clearSearchResultHighlight()
holder.onViewRecycled()
}
@@ -587,8 +598,7 @@ class SettingsAdapter(
}
private fun getValueForSingleChoiceDynamicDescriptionsSelection(
- item: SingleChoiceSettingDynamicDescriptions,
- which: Int
+ item: SingleChoiceSettingDynamicDescriptions, which: Int
): Int {
val valuesId = item.valuesId
return if (valuesId > 0) {
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt
index 1289d30f61..e0f96e77f1 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt
@@ -24,17 +24,22 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.FragmentSettingsBinding
import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
+import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder
import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable
-import java.util.*
-import kotlin.collections.ArrayList
+import java.util.EnumMap
class SettingsFragment : Fragment(), SettingsFragmentView {
private lateinit var presenter: SettingsFragmentPresenter
@@ -51,6 +56,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
SettingsActivityResultLaunchers(this) { adapter }
private var oldControllerSettingsWarningHeight = 0
+ private var hasScrolledToSearchResult = false
+ private var highlightedSearchResult: SettingsItem? = null
+ private var highlightedSearchResultPosition = RecyclerView.NO_POSITION
+ private var searchIndexWarmupJob: Job? = null
+ private var searchJob: Job? = null
private var binding: FragmentSettingsBinding? = null
@@ -82,9 +92,7 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding!!.root
@@ -92,7 +100,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (titles.containsKey(menuTag)) {
- activityView!!.setToolbarTitle(getString(titles[menuTag]!!))
+ activityView!!.setToolbarState(
+ getString(titles[menuTag]!!),
+ menuTag != MenuTag.SETTINGS,
+ menuTag == MenuTag.SETTINGS
+ )
}
val manager = LinearLayoutManager(activity)
@@ -107,10 +119,13 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
setInsets()
val activity = requireActivity() as SettingsActivityView
+ presenter.invalidateSearchIndex()
presenter.onViewCreated(menuTag, activity.settings)
}
override fun onDestroyView() {
+ clearSearchResultHighlight()
+ searchJob?.cancel()
super.onDestroyView()
binding = null
}
@@ -129,7 +144,81 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
override fun showSettingsList(settingsList: ArrayList<SettingsItem>) {
- adapter!!.setSettings(settingsList)
+ val query = activityView?.settingsSearchQuery.orEmpty()
+ val isShowingSearch =
+ menuTag == MenuTag.SETTINGS && activityView?.isSettingsSearchActive == true
+ if (!isShowingSearch) {
+ adapter!!.setSettings(settingsList)
+ }
+ if (menuTag == MenuTag.SETTINGS) {
+ warmUpSearchIndex()
+ if (isShowingSearch) {
+ applySettingsFilter(query)
+ }
+ }
+
+ val position = arguments?.getInt(
+ ARGUMENT_SCROLL_TO_SETTING_POSITION, RecyclerView.NO_POSITION
+ ) ?: RecyclerView.NO_POSITION
+ if (!hasScrolledToSearchResult && position in settingsList.indices) {
+ hasScrolledToSearchResult = true
+ binding?.listSettings?.post {
+ val recyclerView = binding?.listSettings ?: return@post
+ (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(
+ position, 0
+ )
+ highlightSearchResult(position, settingsList[position])
+ }
+ }
+ }
+
+ fun filterSettings(query: String) {
+ if (!this::presenter.isInitialized || presenter.settings == null) {
+ return
+ }
+
+ applySettingsFilter(query)
+ }
+
+ private fun applySettingsFilter(query: String) {
+ searchJob?.cancel()
+ if (query.isBlank()) {
+ val results = if (activityView?.isSettingsSearchActive == true) {
+ arrayListOf()
+ } else {
+ presenter.getSettingsList()
+ }
+ showSearchResults(query, results)
+ return
+ }
+
+ searchJob = viewLifecycleOwner.lifecycleScope.launch {
+ delay(SEARCH_QUERY_DEBOUNCE_MS)
+ val results = presenter.searchSettings(query)
+ if (activityView?.settingsSearchQuery == query) {
+ showSearchResults(query, results)
+ }
+ }
+ }
+
+ private fun warmUpSearchIndex() {
+ if (searchIndexWarmupJob?.isActive == true) {
+ return
+ }
+
+ searchIndexWarmupJob = viewLifecycleOwner.lifecycleScope.launch {
+ presenter.prepareSearchIndex()
+ }
+ }
+
+ private fun showSearchResults(query: String, results: ArrayList<SettingsItem>) {
+ adapter!!.setSettings(results)
+ binding?.textNoSearchResults?.text =
+ getString(R.string.search_settings_no_results, query.trim())
+ binding?.textNoSearchResults?.visibility =
+ if (query.isNotBlank() && results.isEmpty()) View.VISIBLE else View.GONE
+ binding?.listSettings?.visibility =
+ if (query.isNotBlank() && results.isEmpty()) View.GONE else View.VISIBLE
}
override fun loadSubMenu(menuKey: MenuTag) {
@@ -139,13 +228,35 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
activityView!!.showSettingsFragment(
- menuKey,
- null,
- true,
- requireArguments().getString(ARGUMENT_GAME_ID)!!
+ menuKey, null, true, requireArguments().getString(ARGUMENT_GAME_ID)!!
+ )
+ }
+
+ override fun loadSearchResult(menuKey: MenuTag, settingPosition: Int, extras: Bundle?) {
+ activityView!!.showSearchResult(
+ menuKey, settingPosition, requireArguments().getString(ARGUMENT_GAME_ID)!!, extras
)
}
+ private fun highlightSearchResult(position: Int, setting: SettingsItem) {
+ val recyclerView = binding?.listSettings ?: return
+ highlightedSearchResult = setting
+ highlightedSearchResultPosition = position
+ recyclerView.post {
+ (recyclerView.findViewHolderForAdapterPosition(position) as? SettingViewHolder)
+ ?.highlightSearchResult()
+ }
+ }
+
+ private fun clearSearchResultHighlight() {
+ val recyclerView = binding?.listSettings
+ (recyclerView?.findViewHolderForAdapterPosition(
+ highlightedSearchResultPosition
+ ) as? SettingViewHolder)?.clearSearchResultHighlight()
+ highlightedSearchResult = null
+ highlightedSearchResultPosition = RecyclerView.NO_POSITION
+ }
+
override fun showDialogFragment(fragment: DialogFragment) {
activityView!!.showDialogFragment(fragment)
}
@@ -157,7 +268,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
override val settings: Settings?
get() = presenter.settings
- override fun onSettingChanged() {
+ override fun onSettingChanged(setting: SettingsItem?) {
+ if (setting == null || setting === highlightedSearchResult) {
+ clearSearchResultHighlight()
+ }
+ presenter.invalidateSearchIndex()
activityView!!.onSettingChanged()
}
@@ -174,6 +289,10 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
return activityView!!.hasMenuTagActionForValue(menuTag, value)
}
+ override fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle? {
+ return activityView!!.getMenuTagActionExtras(menuTag, value)
+ }
+
override var isMappingAllDevices: Boolean
get() = activityView!!.isMappingAllDevices
set(allDevices) {
@@ -203,17 +322,13 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
val msg = "${presenter.gpuDriver!!.name} ${presenter.gpuDriver!!.driverVersion}"
- MaterialAlertDialogBuilder(requireContext())
- .setTitle(getString(R.string.gpu_driver_dialog_title))
- .setMessage(msg)
- .setNegativeButton(android.R.string.cancel, null)
+ MaterialAlertDialogBuilder(requireContext()).setTitle(getString(R.string.gpu_driver_dialog_title))
+ .setMessage(msg).setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.gpu_driver_dialog_system) { _: DialogInterface?, _: Int ->
presenter.useSystemDriver()
- }
- .setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int ->
+ }.setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int ->
askForDriverFile()
- }
- .show()
+ }.show()
}
override fun getFragmentLifecycle(): Lifecycle {
@@ -230,16 +345,12 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
override fun onDriverInstallDone(result: GpuDriverInstallResult) {
val view = binding?.root ?: return
- Snackbar
- .make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG)
- .show()
+ Snackbar.make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG).show()
}
override fun onDriverUninstallDone() {
Toast.makeText(
- requireContext(),
- R.string.gpu_driver_dialog_uninstall_done,
- Toast.LENGTH_SHORT
+ requireContext(), R.string.gpu_driver_dialog_uninstall_done, Toast.LENGTH_SHORT
).show()
}
@@ -256,6 +367,8 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
companion object {
private const val ARGUMENT_MENU_TAG = "menu_tag"
private const val ARGUMENT_GAME_ID = "game_id"
+ const val ARGUMENT_SCROLL_TO_SETTING_POSITION = "scroll_to_setting_position"
+ private const val SEARCH_QUERY_DEBOUNCE_MS = 120L
private val titles: MutableMap<MenuTag, Int> = EnumMap(MenuTag::class.java)
init {
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
index a9ca9c3c3d..210a34b23f 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -12,7 +12,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.collection.ArraySet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.dolphinemu.dolphinemu.NativeLibrary
import org.dolphinemu.dolphinemu.R
@@ -29,28 +33,63 @@ import org.dolphinemu.dolphinemu.features.input.model.view.InputDeviceSetting
import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting
import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialog
import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialogPresenter
-import org.dolphinemu.dolphinemu.features.settings.model.*
-import org.dolphinemu.dolphinemu.features.settings.model.view.*
+import org.dolphinemu.dolphinemu.features.settings.model.AbstractBooleanSetting
+import org.dolphinemu.dolphinemu.features.settings.model.AbstractIntSetting
+import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel
import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel.logout
+import org.dolphinemu.dolphinemu.features.settings.model.AdHocBooleanSetting
+import org.dolphinemu.dolphinemu.features.settings.model.AdHocStringSetting
+import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
+import org.dolphinemu.dolphinemu.features.settings.model.FloatSetting
+import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
+import org.dolphinemu.dolphinemu.features.settings.model.PostProcessing
+import org.dolphinemu.dolphinemu.features.settings.model.ScaledIntSetting
+import org.dolphinemu.dolphinemu.features.settings.model.Settings
+import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.DateTimeChoiceSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.DirectoryPicker
+import org.dolphinemu.dolphinemu.features.settings.model.view.FilePicker
+import org.dolphinemu.dolphinemu.features.settings.model.view.FloatSliderSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.HeaderSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.HyperLinkHeaderSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.InputStringSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.IntSliderSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.InvertedSwitchSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.LogSwitchSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.PercentSliderSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.RunRunnable
+import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
+import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsSearchResult
+import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSettingDynamicDescriptions
+import org.dolphinemu.dolphinemu.features.settings.model.view.StringSingleChoiceSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.SubmenuSetting
+import org.dolphinemu.dolphinemu.features.settings.model.view.SwitchSetting
import org.dolphinemu.dolphinemu.model.GpuDriverMetadata
-import org.dolphinemu.dolphinemu.utils.*
-import kotlin.collections.ArrayList
+import org.dolphinemu.dolphinemu.utils.BooleanSupplier
+import org.dolphinemu.dolphinemu.utils.EGLHelper
+import org.dolphinemu.dolphinemu.utils.GpuDriverHelper
+import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
+import org.dolphinemu.dolphinemu.utils.ThemeHelper
+import org.dolphinemu.dolphinemu.utils.ThreadUtil
+import org.dolphinemu.dolphinemu.utils.WiiUtils
+import java.util.Locale
import kotlin.math.ceil
import kotlin.math.floor
class SettingsFragmentPresenter(
- private val fragmentView: SettingsFragmentView,
- private val context: Context
+ private val fragmentView: SettingsFragmentView, private val context: Context
) {
private lateinit var menuTag: MenuTag
private var gameId: String? = null
private var settingsList: ArrayList<SettingsItem>? = null
+ private var searchableSettings: List<SearchableSetting>? = null
+ private val searchIndexMutex = Mutex()
+ private var searchIndexGeneration = 0
private var hasOldControllerSettings = false
-
- private var serialPort1Type = 0
- private var controllerNumber = 0
- private var controllerType = 0
+ private var menuExtras = Bundle()
+ private var shouldUpdateWarnings = true
var gpuDriver: GpuDriverMetadata? = null
private val libNameSetting: StringSetting = StringSetting.GFX_DRIVER_LIB_NAME
@@ -58,23 +97,11 @@ class SettingsFragmentPresenter(
fun onCreate(menuTag: MenuTag, gameId: String?, extras: Bundle) {
this.gameId = gameId
this.menuTag = menuTag
+ menuExtras = Bundle(extras)
- if (menuTag.isGCPadMenu || menuTag.isWiimoteExtensionMenu) {
- controllerNumber = menuTag.subType
- controllerType = extras.getInt(ARG_CONTROLLER_TYPE)
- } else if (menuTag.isWiimoteMenu || menuTag.isWiimoteSubmenu) {
- controllerNumber = menuTag.subType
- } else if (menuTag.isSerialPort1Menu) {
- serialPort1Type = extras.getInt(ARG_SERIALPORT1_TYPE)
- } else if (
- menuTag == MenuTag.GRAPHICS
- && this.gameId.isNullOrEmpty()
- && NativeLibrary.IsUninitialized()
- && GpuDriverHelper.supportsCustomDriverLoading()
- ) {
- this.gpuDriver =
- GpuDriverHelper.getInstalledDriverMetadata()
- ?: GpuDriverHelper.getSystemDriverMetadata(context.applicationContext)
+ if (menuTag == MenuTag.GRAPHICS && this.gameId.isNullOrEmpty() && NativeLibrary.IsUninitialized() && GpuDriverHelper.supportsCustomDriverLoading()) {
+ this.gpuDriver = GpuDriverHelper.getInstalledDriverMetadata()
+ ?: GpuDriverHelper.getSystemDriverMetadata(context.applicationContext)
}
}
@@ -100,85 +127,222 @@ class SettingsFragmentPresenter(
}
fun loadSettingsList() {
- val sl = ArrayList<SettingsItem>()
- when (menuTag) {
- MenuTag.SETTINGS -> addTopLevelSettings(sl)
- MenuTag.CONFIG -> addConfigSettings(sl)
- MenuTag.CONFIG_GENERAL -> addGeneralSettings(sl)
- MenuTag.CONFIG_INTERFACE -> addInterfaceSettings(sl)
- MenuTag.CONFIG_AUDIO -> addAudioSettings(sl)
- MenuTag.CONFIG_PATHS -> addPathsSettings(sl)
- MenuTag.CONFIG_GAME_CUBE -> addGameCubeSettings(sl)
- MenuTag.CONFIG_WII -> addWiiSettings(sl)
- MenuTag.CONFIG_ACHIEVEMENTS -> addAchievementSettings(sl);
- MenuTag.CONFIG_ADVANCED -> addAdvancedSettings(sl)
- MenuTag.GRAPHICS -> addGraphicsSettings(sl)
- MenuTag.CONFIG_SERIALPORT1 -> addSerialPortSubSettings(sl, serialPort1Type)
- MenuTag.GCPAD_TYPE -> addGcPadSettings(sl)
- MenuTag.WIIMOTE -> addWiimoteSettings(sl)
- MenuTag.ENHANCEMENTS -> addEnhanceSettings(sl)
- MenuTag.COLOR_CORRECTION -> addColorCorrectionSettings(sl)
- MenuTag.STEREOSCOPY -> addStereoSettings(sl)
- MenuTag.HACKS -> addHackSettings(sl)
- MenuTag.STATISTICS -> addStatisticsSettings(sl)
- MenuTag.ADVANCED_GRAPHICS -> addAdvancedGraphicsSettings(sl)
- MenuTag.CONFIG_LOG -> addLogConfigurationSettings(sl)
- MenuTag.DEBUG -> addDebugSettings(sl)
- MenuTag.GCPAD_1,
- MenuTag.GCPAD_2,
- MenuTag.GCPAD_3,
- MenuTag.GCPAD_4 -> addGcPadSubSettings(
- sl,
- controllerNumber,
- controllerType
- )
+ invalidateSearchIndex()
+ settingsList = tryBuildSettingsList(menuTag, menuExtras, true)
+ ?: throw UnsupportedOperationException("Unimplemented menu")
+ fragmentView.showSettingsList(settingsList!!)
+ }
- MenuTag.WIIMOTE_1,
- MenuTag.WIIMOTE_2,
- MenuTag.WIIMOTE_3,
- MenuTag.WIIMOTE_4 -> addWiimoteSubSettings(
- sl,
- controllerNumber
- )
+ fun getSettingsList(): ArrayList<SettingsItem> = settingsList ?: arrayListOf()
- MenuTag.WIIMOTE_EXTENSION_1,
- MenuTag.WIIMOTE_EXTENSION_2,
- MenuTag.WIIMOTE_EXTENSION_3,
- MenuTag.WIIMOTE_EXTENSION_4 -> addExtensionTypeSettings(
- sl,
- controllerNumber,
- controllerType
- )
+ suspend fun prepareSearchIndex() {
+ getSearchIndex()
+ }
- MenuTag.WIIMOTE_GENERAL_1,
- MenuTag.WIIMOTE_GENERAL_2,
- MenuTag.WIIMOTE_GENERAL_3,
- MenuTag.WIIMOTE_GENERAL_4 -> addWiimoteGeneralSubSettings(
- sl,
- controllerNumber
- )
+ suspend fun searchSettings(query: String): ArrayList<SettingsItem> {
+ val normalizedQuery = query.trim().lowercase(Locale.ROOT)
+ if (normalizedQuery.isEmpty()) {
+ return getSettingsList()
+ }
- MenuTag.WIIMOTE_MOTION_SIMULATION_1,
- MenuTag.WIIMOTE_MOTION_SIMULATION_2,
- MenuTag.WIIMOTE_MOTION_SIMULATION_3,
- MenuTag.WIIMOTE_MOTION_SIMULATION_4 -> addWiimoteMotionSimulationSubSettings(
- sl,
- controllerNumber
- )
+ val terms = normalizedQuery.split(Regex("\\s+"))
+ val index = getSearchIndex()
+ return withContext(Dispatchers.Default) {
+ index.asSequence()
+ .filter { setting -> terms.all(setting.normalizedSearchText::contains) }
+ .map { setting ->
+ val score = when {
+ setting.normalizedName == normalizedQuery -> 0
+ setting.normalizedName.startsWith(normalizedQuery) -> 1
+ terms.all(setting.normalizedName::contains) -> 2
+ setting.normalizedCategory.contains(normalizedQuery) -> 3
+ else -> 4
+ }
+ score to SettingsSearchResult(
+ setting.name,
+ setting.description,
+ setting.menuTag,
+ setting.position,
+ setting.navigationExtras
+ )
+ }
+ .sortedWith(compareBy<Pair<Int, SettingsSearchResult>> { it.first }.thenBy(String.CASE_INSENSITIVE_ORDER) { it.second.name.toString() })
+ .mapTo(ArrayList()) { it.second }
+ }
+ }
- MenuTag.WIIMOTE_MOTION_INPUT_1,
- MenuTag.WIIMOTE_MOTION_INPUT_2,
- MenuTag.WIIMOTE_MOTION_INPUT_3,
- MenuTag.WIIMOTE_MOTION_INPUT_4 -> addWiimoteMotionInputSubSettings(
- sl,
- controllerNumber
- )
+ private suspend fun getSearchIndex(): List<SearchableSetting> {
+ return searchIndexMutex.withLock {
+ searchableSettings?.let { return@withLock it }
- else -> throw UnsupportedOperationException("Unimplemented menu")
+ val generation = searchIndexGeneration
+ val index = withContext(Dispatchers.IO) {
+ buildSearchIndex()
+ }
+ if (generation == searchIndexGeneration) {
+ searchableSettings = index
+ }
+ index
}
+ }
- settingsList = sl
- fragmentView.showSettingsList(settingsList!!)
+ private suspend fun buildSearchIndex(): List<SearchableSetting> {
+ return getSearchableMenus().flatMap { searchableMenu ->
+ currentCoroutineContext().ensureActive()
+ searchableMenu.settings.withIndex().asSequence()
+ .filterNot { it.value is HeaderSetting || it.value is SubmenuSetting }
+ .map { indexedItem ->
+ val item = indexedItem.value
+ val name = item.name.toString()
+ val description = item.description.toString()
+ SearchableSetting(
+ name = name,
+ description = searchableMenu.category,
+ menuTag = searchableMenu.menuTag,
+ navigationExtras = searchableMenu.navigationExtras,
+ position = indexedItem.index,
+ normalizedName = name.lowercase(Locale.ROOT),
+ normalizedCategory = searchableMenu.category.lowercase(Locale.ROOT),
+ normalizedSearchText = "$name $description ${searchableMenu.category}".lowercase(
+ Locale.ROOT
+ )
+ )
+ }.toList()
+ }
+ }
+
+ fun invalidateSearchIndex() {
+ searchableSettings = null
+ searchIndexGeneration++
+ }
+
+ private suspend fun getSearchableMenus(): List<SearchableMenu> {
+ val menus = mutableListOf<SearchableMenu>()
+ val visitedMenus = mutableSetOf<String>()
+
+ suspend fun visitMenu(
+ menuTag: MenuTag,
+ category: String,
+ navigationExtras: Bundle? = null,
+ actionValue: Int? = null
+ ) {
+ currentCoroutineContext().ensureActive()
+ if (!visitedMenus.add("$menuTag:$actionValue")) {
+ return
+ }
+
+ val items = tryBuildSettingsList(menuTag, navigationExtras ?: Bundle(), false) ?: return
+ if (menuTag != MenuTag.SETTINGS) {
+ menus += SearchableMenu(menuTag, category, navigationExtras, items)
+ }
+
+ suspend fun visitMenuAction(
+ childMenuTag: MenuTag?, selectedValue: Int, childCategory: String
+ ) {
+ if (childMenuTag != null && fragmentView.hasMenuTagActionForValue(
+ childMenuTag, selectedValue
+ )
+ ) {
+ visitMenu(
+ childMenuTag,
+ childCategory,
+ fragmentView.getMenuTagActionExtras(childMenuTag, selectedValue),
+ selectedValue
+ )
+ }
+ }
+
+ for (item in items) {
+ currentCoroutineContext().ensureActive()
+ val childCategory = if (category.isEmpty()) item.name.toString()
+ else context.getString(
+ R.string.search_settings_category_path, category, item.name
+ )
+
+ when (item) {
+ is SubmenuSetting -> visitMenu(item.menuKey, childCategory)
+ is SingleChoiceSetting -> {
+ visitMenuAction(item.menuTag, item.selectedValue, childCategory)
+ }
+
+ is StringSingleChoiceSetting -> {
+ visitMenuAction(item.menuTag, item.selectedValueIndex, childCategory)
+ }
+ }
+ }
+ }
+
+ visitMenu(MenuTag.SETTINGS, "")
+ return menus
+ }
+
+ private fun tryBuildSettingsList(
+ targetMenuTag: MenuTag, extras: Bundle, updateWarnings: Boolean
+ ): ArrayList<SettingsItem>? {
+ val sl = ArrayList<SettingsItem>()
+ var isSupportedMenu = true
+ val previousMenuTag = menuTag
+ val previousShouldUpdateWarnings = shouldUpdateWarnings
+ menuTag = targetMenuTag
+ shouldUpdateWarnings = updateWarnings
+ try {
+ when (targetMenuTag) {
+ MenuTag.SETTINGS -> addTopLevelSettings(sl)
+ MenuTag.CONFIG -> addConfigSettings(sl)
+ MenuTag.CONFIG_GENERAL -> addGeneralSettings(sl)
+ MenuTag.CONFIG_INTERFACE -> addInterfaceSettings(sl)
+ MenuTag.CONFIG_AUDIO -> addAudioSettings(sl)
+ MenuTag.CONFIG_PATHS -> addPathsSettings(sl)
+ MenuTag.CONFIG_GAME_CUBE -> addGameCubeSettings(sl)
+ MenuTag.CONFIG_WII -> addWiiSettings(sl)
+ MenuTag.CONFIG_ACHIEVEMENTS -> addAchievementSettings(sl)
+ MenuTag.CONFIG_ADVANCED -> addAdvancedSettings(sl)
+ MenuTag.GRAPHICS -> addGraphicsSettings(sl)
+ MenuTag.CONFIG_SERIALPORT1 -> addSerialPortSubSettings(
+ sl, extras.getInt(ARG_SERIALPORT1_TYPE)
+ )
+
+ MenuTag.GCPAD_TYPE -> addGcPadSettings(sl)
+ MenuTag.WIIMOTE -> addWiimoteSettings(sl)
+ MenuTag.ENHANCEMENTS -> addEnhanceSettings(sl)
+ MenuTag.COLOR_CORRECTION -> addColorCorrectionSettings(sl)
+ MenuTag.STEREOSCOPY -> addStereoSettings(sl)
+ MenuTag.HACKS -> addHackSettings(sl)
+ MenuTag.STATISTICS -> addStatisticsSettings(sl)
+ MenuTag.ADVANCED_GRAPHICS -> addAdvancedGraphicsSettings(sl)
+ MenuTag.CONFIG_LOG -> addLogConfigurationSettings(sl)
+ MenuTag.DEBUG -> addDebugSettings(sl)
+ MenuTag.GCPAD_1, MenuTag.GCPAD_2, MenuTag.GCPAD_3, MenuTag.GCPAD_4 -> addGcPadSubSettings(
+ sl, targetMenuTag.subType, extras.getInt(ARG_CONTROLLER_TYPE)
+ )
+
+ MenuTag.WIIMOTE_1, MenuTag.WIIMOTE_2, MenuTag.WIIMOTE_3, MenuTag.WIIMOTE_4 -> addWiimoteSubSettings(
+ sl, targetMenuTag.subType
+ )
+
+ MenuTag.WIIMOTE_EXTENSION_1, MenuTag.WIIMOTE_EXTENSION_2, MenuTag.WIIMOTE_EXTENSION_3, MenuTag.WIIMOTE_EXTENSION_4 -> addExtensionTypeSettings(
+ sl, targetMenuTag.subType, extras.getInt(ARG_CONTROLLER_TYPE)
+ )
+
+ MenuTag.WIIMOTE_GENERAL_1, MenuTag.WIIMOTE_GENERAL_2, MenuTag.WIIMOTE_GENERAL_3, MenuTag.WIIMOTE_GENERAL_4 -> addWiimoteGeneralSubSettings(
+ sl, targetMenuTag.subType
+ )
+
+ MenuTag.WIIMOTE_MOTION_SIMULATION_1, MenuTag.WIIMOTE_MOTION_SIMULATION_2, MenuTag.WIIMOTE_MOTION_SIMULATION_3, MenuTag.WIIMOTE_MOTION_SIMULATION_4 -> addWiimoteMotionSimulationSubSettings(
+ sl, targetMenuTag.subType
+ )
+
+ MenuTag.WIIMOTE_MOTION_INPUT_1, MenuTag.WIIMOTE_MOTION_INPUT_2, MenuTag.WIIMOTE_MOTION_INPUT_3, MenuTag.WIIMOTE_MOTION_INPUT_4 -> addWiimoteMotionInputSubSettings(
+ sl, targetMenuTag.subType
+ )
+
+ else -> isSupportedMenu = false
+ }
+ } finally {
+ menuTag = previousMenuTag
+ shouldUpdateWarnings = previousShouldUpdateWarnings
+ }
+ return if (isSupportedMenu) sl else null
}
private fun addTopLevelSettings(sl: ArrayList<SettingsItem>) {
@@ -205,9 +369,9 @@ class SettingsFragmentPresenter(
sl.add(SubmenuSetting(context, R.string.log_submenu, MenuTag.CONFIG_LOG))
sl.add(SubmenuSetting(context, R.string.debug_submenu, MenuTag.DEBUG))
sl.add(
- RunRunnable(context, R.string.user_data_submenu, 0, 0, 0, false)
- { UserDataActivity.launch(context) }
- )
+ RunRunnable(
+ context, R.string.user_data_submenu, 0, 0, 0, false
+ ) { UserDataActivity.launch(context) })
}
private fun addGeneralSettings(sl: ArrayList<SettingsItem>) {
@@ -221,10 +385,7 @@ class SettingsFragmentPresenter(
)
sl.add(
SwitchSetting(
- context,
- BooleanSetting.MAIN_ENABLE_CHEATS,
- R.string.enable_cheats,
- 0
+ context, BooleanSetting.MAIN_ENABLE_CHEATS, R.string.enable_cheats, 0
)
)
sl.add(
@@ -237,10 +398,7 @@ class SettingsFragmentPresenter(
)
sl.add(
SwitchSetting(
- context,
- BooleanSetting.MAIN_AUTO_DISC_CHANGE,
- R.string.auto_disc_change,
- 0
+ context, BooleanSetting.MAIN_AUTO_DISC_CHANGE, R.string.auto_disc_change, 0
)
)
sl.add(
@@ -268,10 +426,7 @@ class SettingsFragmentPresenter(
)
sl.add(
SwitchSetting(
- context,
- BooleanSetting.MAIN_ANALYTICS_ENABLED,
- R.string.analytics,
- 0
+ context, BooleanSetting.MAIN_ANALYTICS_ENABLED, R.string.analytics, 0
)
)
sl.add(
@@ -282,8 +437,7 @@ class SettingsFragmentPresenter(
R.string.analytics_new_id_confirmation,
0,
true
- ) { NativeLibrary.GenerateNewStatisticsId() }
- )
+ ) { NativeLibrary.GenerateNewStatisticsId() })
sl.add(
SwitchSetting(
context,
@@ -298,8 +452,9 @@ class SettingsFragmentPresenter(
// Hide the orientation setting if the device only supports one orientation. Old devices which
// support both portrait and landscape may report support for neither, so we use ==, not &&.
val packageManager = context.packageManager
- if (packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT) ==
- packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE)
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT) == packageManager.hasSystemFeature(
+ PackageManager.FEATURE_SCREEN_LANDSCAPE
+ )
) {
sl.add(
SingleChoiceSetting(
@@ -368,8 +523,7 @@ class SettingsFragmentPresenter(
override fun setInt(settings: Settings, newValue: Int) {
IntSetting.MAIN_INTERFACE_THEME.setInt(settings, newValue)
ThemeHelper.saveTheme(
- (fragmentView.fragmentActivity as AppCompatActivity),
- newValue
+ (fragmentView.fragmentActivity as AppCompatActivity), newValue
)
}
}
@@ -417,8 +571,7 @@ class SettingsFragmentPresenter(
override fun setInt(settings: Settings, newValue: Int) {
IntSetting.MAIN_INTERFACE_THEME_MODE.setInt(settings, newValue)
ThemeHelper.saveThemeMode(
- (fragmentView.fragmentActivity as AppCompatActivity),
- newValue
+ (fragmentView.fragmentActivity as AppCompatActivity), newValue
)
}
}
@@ -451,8 +604,7 @@ class SettingsFragmentPresenter(
override fun setBoolean(settings: Settings, newValue: Boolean) {
BooleanSetting.MAIN_USE_BLACK_BACKGROUNDS.setBoolean(settings, newValue)
ThemeHelper.saveBackgroundSetting(
- (fragmentView.fragmentActivity as AppCompatActivity),
- newValue
+ (fragmentView.fragmentActivity as AppCompatActivity), newValue
)
}
}
@@ -501,17 +653,16 @@ class SettingsFragmentPresenter(
}
override val isOverridden: Boolean
- get() = BooleanSetting.MAIN_DSP_HLE.isOverridden ||
- BooleanSetting.MAIN_DSP_JIT.isOverridden
+ get() = BooleanSetting.MAIN_DSP_HLE.isOverridden || BooleanSetting.MAIN_DSP_JIT.isOverridden
override val isRuntimeEditable: Boolean
- get() = BooleanSetting.MAIN_DSP_HLE.isRuntimeEditable &&
- BooleanSetting.MAIN_DSP_JIT.isRuntimeEditable
+ get() = BooleanSetting.MAIN_DSP_HLE.isRuntimeEditable && BooleanSetting.MAIN_DSP_JIT.isRuntimeEditable
override fun delete(settings: Settings): Boolean {
// Not short circuiting
- return BooleanSetting.MAIN_DSP_HLE.delete(settings) and
- BooleanSetting.MAIN_DSP_JIT.delete(settings)
+ return BooleanSetting.MAIN_DSP_HLE.delete(settings) and BooleanSetting.MAIN_DSP_JIT.delete(
+ settings
+ )
}
}
@@ -566,14 +717,7 @@ class SettingsFragmentPresenter(
)
sl.add(
IntSliderSetting(
- context,
- IntSetting.MAIN_AUDIO_VOLUME,
- R.string.audio_volume,
- 0,
- 0,
- 100,
- "%",
- 1
+ context, IntSetting.MAIN_AUDIO_VOLUME, R.string.audio_volume, 0, 0, 100, "%", 1
)
)
}
@@ -581,10 +725,7 @@ class SettingsFragmentPresenter(
private fun addPathsSettings(sl: ArrayList<SettingsItem>) {
sl.add(
SwitchSetting(
- context,
- BooleanSetting.MAIN_RECURSIVE_ISO_PATHS,
- R.string.search_subfolders,
- 0
+ context, BooleanSetting.MAIN_RECURSIVE_ISO_PATHS, R.string.search_subfolders, 0
)
)
sl.add(
@@ -802,10 +943,7 @@ class SettingsFragmentPresenter(
)
sl.add(
SwitchSetting(
- context,
- BooleanSetting.MAIN_ALLOW_SD_WRITES,
- R.string.wii_sd_card_allow_writes,
- 0
+ context, BooleanSetting.MAIN_ALLOW_SD_WRITES, R.string.wii_sd_card_allow_writes, 0
)
)
sl.add(
@@ -847,8 +985,7 @@ class SettingsFragmentPresenter(
R.string.wii_sd_card_folder_to_file_confirmation,
0,
false
- ) { convertOnThread { WiiUtils.syncSdFolderToSdImage() } }
- )
+ ) { convertOnThread { WiiUtils.syncSdFolderToSdImage() } })
sl.add(
RunRunnable(
context,
@@ -857,16 +994,12 @@ class SettingsFragmentPresenter(
R.string.wii_sd_card_file_to_folder_confirmation,
0,
false
- ) { convertOnThread { WiiUtils.syncSdImageToSdFolder() } }
- )
+ ) { convertOnThread { WiiUtils.syncSdImageToSdFolder() } })
sl.add(HeaderSetting(context, R.string.wii_wiimote_settings, 0))
sl.add(
SwitchSetting(
- context,
- BooleanSetting.SYSCONF_WIIMOTE_MOTOR,
- R.string.wiimote_rumble,
- 0
+ context, BooleanSetting.SYSCONF_WIIMOTE_MOTOR, R.string.wiimote_rumble, 0
)
)
sl.add(
@@ -939,18 +1072,12 @@ class SettingsFragmentPresenter(
)
sl.add(
SwitchSetting(
- context,
- BooleanSetting.MAIN_EMULATE_WII_SPEAK,
- R.string.emulate_wii_speak,
- 0
+ context, BooleanSetting.MAIN_EMULATE_WII_SPEAK, R.string.emulate_wii_speak, 0
)
)
sl.add(
SwitchSetting(
- context,
- BooleanSetting.MAIN_WII_SPEAK_MUTED,
- R.string.mute_wii_speak,
- 0
+ context, BooleanSetting.MAIN_WII_SPEAK_MUTED, R.string.mute_wii_speak, 0
)
)
}
@@ -962,10 +1089,8 @@ class SettingsFragmentPresenter(
override fun setBoolean(settings: Settings, newValue: Boolean) {
BooleanSetting.ACHIEVEMENTS_ENABLED.setBoolean(settings, newValue)
- if (newValue)
- AchievementModel.init()
- else
- AchievementModel.shutdown()
+ if (newValue) AchievementModel.init()
+ else AchievementModel.shutdown()
loadSettingsList()
}
@@ -985,38 +1110,25 @@ class SettingsFragmentPresenter(
sl.add(
SwitchSetting(
- context,
- achievementsEnabledSetting,
- R.string.achievements_enabled,
- 0
+ context, achievementsEnabledSetting, R.string.achievements_enabled, 0
)
)
if (BooleanSetting.ACHIEVEMENTS_ENABLED.boolean) {
if (StringSetting.ACHIEVEMENTS_API_TOKEN.string == "") {
sl.add(
RunRunnable(
- context,
- R.string.achievements_login,
- 0,
- 0,
- 0,
- false
+ context, R.string.achievements_login, 0, 0, 0, false
) {
- fragmentView.showDialogFragment(LoginDialog(this))
- loadSettingsList()
+ fragmentView.showDialogFragment(LoginDialog(this))
+ loadSettingsList()
})
} else {
sl.add(
RunRunnable(
- context,
- R.string.achievements_logout,
- 0,
- 0,
- 0,
- false
+ context, R.string.achievements_logout, 0, 0, 0, false
) {
- logout()
- loadSettingsList()
+ logout()
+ loadSettingsList()
})
}
sl.add(
@@ -1112,17 +1224,16 @@ class SettingsFragmentPresenter(
}
override val isOverridden: Boolean
- get() = BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.isOverridden ||
- BooleanSetting.MAIN_SYNC_GPU.isOverridden
+ get() = BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.isOverridden || BooleanSetting.MAIN_SYNC_GPU.isOverridden
override val isRuntimeEditable: Boolean
- get() = BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.isRuntimeEditable &&
- BooleanSetting.MAIN_SYNC_GPU.isRuntimeEditable
+ get() = BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.isRuntimeEditable && BooleanSetting.MAIN_SYNC_GPU.isRuntimeEditable
override fun delete(settings: Settings): Boolean {
// Not short circuiting
- return BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.delete(settings) and
- BooleanSetting.MAIN_SYNC_GPU.delete(settings)
+ return BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.delete(settings) and BooleanSetting.MAIN_SYNC_GPU.delete(
+ settings
+ )
}
}
@@ -1243,26 +1354,12 @@ class SettingsFragmentPresenter(
)
sl.add(
IntSliderSetting(
- context,
- mem1Size,
- R.string.main_mem1_size,
- 0,
- 24,
- 64,
- "MB",
- 1
+ context, mem1Size, R.string.main_mem1_size, 0, 24, 64, "MB", 1
)
)
sl.add(
IntSliderSetting(
- context,
- mem2Size,
- R.string.main_mem2_size,
- 0,
- 64,
- 128,
- "MB",
- 1
+ context, mem2Size, R.string.main_mem2_size, 0, 64, 128, "MB", 1
)
)
@@ -1289,10 +1386,7 @@ class SettingsFragmentPresenter(
)
sl.add(
DateTimeChoiceSetting(
- context,
- StringSetting.MAIN_CUSTOM_RTC_VALUE,
- R.string.set_custom_rtc,
- 0
+ context, StringSetting.MAIN_CUSTOM_RTC_VALUE, R.string.set_custom_rtc, 0
)
)
@@ -1520,42 +1614,29 @@ class SettingsFragmentPresenter(
sl.add(HeaderSetting(context, R.string.graphics_more_settings, 0))
sl.add(
SubmenuSetting(
- context,
- R.string.enhancements_submenu,
- MenuTag.ENHANCEMENTS
+ context, R.string.enhancements_submenu, MenuTag.ENHANCEMENTS
)
)
sl.add(
SubmenuSetting(
- context,
- R.string.hacks_submenu,
- MenuTag.HACKS
+ context, R.string.hacks_submenu, MenuTag.HACKS
)
)
sl.add(
SubmenuSetting(
- context,
- R.string.statistics_submenu,
- MenuTag.STATISTICS
+ context, R.string.statistics_submenu, MenuTag.STATISTICS
)
)
sl.add(
SubmenuSetting(
- context,
- R.string.advanced_graphics_submenu,
- MenuTag.ADVANCED_GRAPHICS
+ context, R.string.advanced_graphics_submenu, MenuTag.ADVANCED_GRAPHICS
)
)
- if (
- this.gpuDriver != null && this.gameId.isNullOrEmpty()
- && NativeLibrary.IsUninitialized()
- && GpuDriverHelper.supportsCustomDriverLoading()
- ) {
+ if (this.gpuDriver != null && this.gameId.isNullOrEmpty() && NativeLibrary.IsUninitialized() && GpuDriverHelper.supportsCustomDriverLoading()) {
sl.add(
SubmenuSetting(
- context,
- R.string.gpu_driver_submenu, MenuTag.GPU_DRIVERS
+ context, R.string.gpu_driver_submenu, MenuTag.GPU_DRIVERS
)
)
}
@@ -1604,9 +1685,7 @@ class SettingsFragmentPresenter(
)
sl.add(
SubmenuSetting(
- context,
- R.string.color_correction_submenu,
- MenuTag.COLOR_CORRECTION
+ context, R.string.color_correction_submenu, MenuTag.COLOR_CORRECTION
)
)
@@ -1697,9 +1776,7 @@ class SettingsFragmentPresenter(
) {
sl.add(
SubmenuSetting(
- context,
- R.string.stereoscopy_submenu,
- MenuTag.STEREOSCOPY
+ context, R.string.stereoscopy_submenu, MenuTag.STEREOSCOPY
)
)
}
@@ -1743,10 +1820,7 @@ class SettingsFragmentPresenter(
)
add(
SwitchSetting(
- context,
- BooleanSetting.GFX_CC_CORRECT_GAMMA,
- R.string.correct_sdr_gamma,
- 0
+ context, BooleanSetting.GFX_CC_CORRECT_GAMMA, R.string.correct_sdr_gamma, 0
)
)
}
@@ -2053,10 +2127,10 @@ class SettingsFragmentPresenter(
IntSetting.GFX_CROP_CUSTOM_LEFT,
R.string.crop_custom_left,
R.string.crop_custom_left_description,
- min=0,
- max=640,
- units="px",
- stepSize=1,
+ min = 0,
+ max = 640,
+ units = "px",
+ stepSize = 1,
)
)
sl.add(
@@ -2075,18 +2149,12 @@ class SettingsFragmentPresenter(
sl.add(HeaderSetting(context, R.string.misc, 0))
sl.add(
SwitchSetting(
- context,
- BooleanSetting.SYSCONF_PROGRESSIVE_SCAN,
- R.string.progressive_scan,
- 0
+ context, BooleanSetting.SYSCONF_PROGRESSIVE_SCAN, R.string.progressive_scan, 0
)
)
sl.add(
SwitchSetting(
- context,
- BooleanSetting.GFX_VSYNC,
- R.string.vsync,
- R.string.vsync_description
+ context, BooleanSetting.GFX_VSYNC, R.string.vsync, R.string.vsync_description
)
)
sl.add(
@@ -2242,35 +2310,21 @@ class SettingsFragmentPresenter(
IntSetting.LOGGER_VERBOSITY,
R.string.log_verbosity,
0,
- getLogVerbosityEntries(), getLogVerbosityValues()
+ getLogVerbosityEntries(),
+ getLogVerbosityValues()
)
)
sl.add(
RunRunnable(
- context,
- R.string.log_enable_all,
- 0,
- R.string.log_enable_all_confirmation,
- 0,
- true
+ context, R.string.log_enable_all, 0, R.string.log_enable_all_confirmation, 0, true
) { setAllLogTypes(true) })
sl.add(
RunRunnable(
- context,
- R.string.log_disable_all,
- 0,
- R.string.log_disable_all_confirmation,
- 0,
- true
+ context, R.string.log_disable_all, 0, R.string.log_disable_all_confirmation, 0, true
) { setAllLogTypes(false) })
sl.add(
RunRunnable(
- context,
- R.string.log_clear,
- 0,
- R.string.log_clear_confirmation,
- 0,
- true
+ context, R.string.log_clear, 0, R.string.log_clear_confirmation, 0, true
) { SettingsAdapter.clearLog() })
sl.add(HeaderSetting(context, R.string.log_types, 0))
@@ -2283,10 +2337,7 @@ class SettingsFragmentPresenter(
sl.add(HeaderSetting(context, R.string.debug_warning, 0))
sl.add(
InvertedSwitchSetting(
- context,
- BooleanSetting.MAIN_FASTMEM,
- R.string.debug_fastmem,
- 0
+ context, BooleanSetting.MAIN_FASTMEM, R.string.debug_fastmem, 0
)
)
sl.add(
@@ -2299,10 +2350,7 @@ class SettingsFragmentPresenter(
)
sl.add(
InvertedSwitchSetting(
- context,
- BooleanSetting.MAIN_FASTMEM_ARENA,
- R.string.debug_fastmem_arena,
- 0
+ context, BooleanSetting.MAIN_FASTMEM_ARENA, R.string.debug_fastmem_arena, 0
)
)
sl.add(
@@ -2321,7 +2369,7 @@ class SettingsFragmentPresenter(
BooleanSetting.MAIN_DEBUG_JIT_ENABLE_PROFILING,
R.string.debug_jit_enable_block_profiling,
0
- )
+ )
)
sl.add(
RunRunnable(
@@ -2331,26 +2379,16 @@ class SettingsFragmentPresenter(
R.string.debug_jit_wipe_block_profiling_data_alert,
0,
true
- ) { NativeLibrary.WipeJitBlockProfilingData() }
- )
+ ) { NativeLibrary.WipeJitBlockProfilingData() })
sl.add(
RunRunnable(
- context,
- R.string.debug_jit_write_block_log_dump,
- 0,
- 0,
- 0,
- true
- ) { NativeLibrary.WriteJitBlockLogDump() }
- )
+ context, R.string.debug_jit_write_block_log_dump, 0, 0, 0, true
+ ) { NativeLibrary.WriteJitBlockLogDump() })
sl.add(HeaderSetting(context, R.string.debug_jit_header, 0))
sl.add(
SwitchSetting(
- context,
- BooleanSetting.MAIN_DEBUG_JIT_OFF,
- R.string.debug_jitoff,
- 0
+ context, BooleanSetting.MAIN_DEBUG_JIT_OFF, R.string.debug_jitoff, 0
)
)
sl.add(
@@ -2387,18 +2425,12 @@ class SettingsFragmentPresenter(
)
sl.add(
SwitchSetting(
- context,
- BooleanSetting.MAIN_DEBUG_JIT_INTEGER_OFF,
- R.string.debug_jitintegeroff,
- 0
+ context, BooleanSetting.MAIN_DEBUG_JIT_INTEGER_OFF, R.string.debug_jitintegeroff, 0
)
)
sl.add(
SwitchSetting(
- context,
- BooleanSetting.MAIN_DEBUG_JIT_PAIRED_OFF,
- R.string.debug_jitpairedoff,
- 0
+ context, BooleanSetting.MAIN_DEBUG_JIT_PAIRED_OFF, R.string.debug_jitpairedoff, 0
)
)
sl.add(
@@ -2411,10 +2443,7 @@ class SettingsFragmentPresenter(
)
sl.add(
SwitchSetting(
- context,
- BooleanSetting.MAIN_DEBUG_JIT_BRANCH_OFF,
- R.string.debug_jitbranchoff,
- 0
+ context, BooleanSetting.MAIN_DEBUG_JIT_BRANCH_OFF, R.string.debug_jitbranchoff, 0
)
)
sl.add(
@@ -2474,7 +2503,9 @@ class SettingsFragmentPresenter(
)
}
- private fun addGcPadSubSettings(sl: ArrayList<SettingsItem>, gcPadNumber: Int, gcPadType: Int) {
+ private fun addGcPadSubSettings(
+ sl: ArrayList<SettingsItem>, gcPadNumber: Int, gcPadType: Int
+ ) {
when (gcPadType) {
6, 8, 9, 10, 11 -> {
// Emulated
@@ -2484,9 +2515,10 @@ class SettingsFragmentPresenter(
addControllerPerGameSettings(sl, gcPad, gcPadNumber)
} else {
addControllerMetaSettings(sl, gcPad)
- addControllerMappingSettings(sl, gcPad, null)
+ addControllerMappingSettings(sl, gcPad, gcPadNumber, null)
}
}
+
7 -> {
// Emulated keyboard controller
val gcKeyboard = EmulatedController.getGcKeyboard(gcPadNumber)
@@ -2496,9 +2528,12 @@ class SettingsFragmentPresenter(
} else {
sl.add(HeaderSetting(context, R.string.keyboard_controller_warning, 0))
addControllerMetaSettings(sl, gcKeyboard)
- addControllerMappingSettings(sl, gcKeyboard, null)
+ addControllerMappingSettings(
+ sl, gcKeyboard, gcPadNumber, null
+ )
}
}
+
12 -> {
// Adapter
sl.add(
@@ -2521,7 +2556,9 @@ class SettingsFragmentPresenter(
}
}
- private fun addWiimoteSubSettings(sl: ArrayList<SettingsItem>, wiimoteNumber: Int) {
+ private fun addWiimoteSubSettings(
+ sl: ArrayList<SettingsItem>, wiimoteNumber: Int
+ ) {
val wiimote = EmulatedController.getWiimote(wiimoteNumber)
if (!TextUtils.isEmpty(gameId)) {
@@ -2557,39 +2594,40 @@ class SettingsFragmentPresenter(
addControllerMappingSettings(
sl,
wiimote,
+ wiimoteNumber,
ArraySet(listOf(ControlGroup.TYPE_ATTACHMENTS, ControlGroup.TYPE_OTHER))
)
}
}
private fun addExtensionTypeSettings(
- sl: ArrayList<SettingsItem>,
- wiimoteNumber: Int,
- extensionType: Int
+ sl: ArrayList<SettingsItem>, wiimoteNumber: Int, extensionType: Int
) {
addContainerMappingSettings(
sl,
EmulatedController.getWiimote(wiimoteNumber),
EmulatedController.getWiimoteAttachment(wiimoteNumber, extensionType),
+ wiimoteNumber,
null
)
}
- private fun addWiimoteGeneralSubSettings(sl: ArrayList<SettingsItem>, wiimoteNumber: Int) {
+ private fun addWiimoteGeneralSubSettings(
+ sl: ArrayList<SettingsItem>, wiimoteNumber: Int
+ ) {
addControllerMappingSettings(
sl,
EmulatedController.getWiimote(wiimoteNumber),
+ wiimoteNumber,
setOf(ControlGroup.TYPE_BUTTONS)
)
}
private fun addWiimoteMotionSimulationSubSettings(
- sl: ArrayList<SettingsItem>,
- wiimoteNumber: Int
+ sl: ArrayList<SettingsItem>, wiimoteNumber: Int
) {
addControllerMappingSettings(
- sl, EmulatedController.getWiimote(wiimoteNumber),
- ArraySet(
+ sl, EmulatedController.getWiimote(wiimoteNumber), wiimoteNumber, ArraySet(
listOf(
ControlGroup.TYPE_FORCE,
ControlGroup.TYPE_TILT,
@@ -2600,10 +2638,11 @@ class SettingsFragmentPresenter(
)
}
- private fun addWiimoteMotionInputSubSettings(sl: ArrayList<SettingsItem>, wiimoteNumber: Int) {
+ private fun addWiimoteMotionInputSubSettings(
+ sl: ArrayList<SettingsItem>, wiimoteNumber: Int
+ ) {
addControllerMappingSettings(
- sl, EmulatedController.getWiimote(wiimoteNumber),
- ArraySet(
+ sl, EmulatedController.getWiimote(wiimoteNumber), wiimoteNumber, ArraySet(
listOf(
ControlGroup.TYPE_IMU_ACCELEROMETER,
ControlGroup.TYPE_IMU_GYROSCOPE,
@@ -2621,9 +2660,7 @@ class SettingsFragmentPresenter(
* @param controllerNumber The index of the controller, 0-3.
*/
private fun addControllerPerGameSettings(
- sl: ArrayList<SettingsItem>,
- controller: EmulatedController,
- controllerNumber: Int
+ sl: ArrayList<SettingsItem>, controller: EmulatedController, controllerNumber: Int
) {
val profiles = ProfileDialogPresenter(menuTag).getProfileNames(false)
val profileKey = controller.getProfileKey() + "Profile" + (controllerNumber + 1)
@@ -2648,15 +2685,11 @@ class SettingsFragmentPresenter(
* @param controller The controller to add settings for.
*/
private fun addControllerMetaSettings(
- sl: ArrayList<SettingsItem>,
- controller: EmulatedController
+ sl: ArrayList<SettingsItem>, controller: EmulatedController
) {
sl.add(
InputDeviceSetting(
- context,
- R.string.input_device,
- 0,
- controller
+ context, R.string.input_device, 0, controller
)
)
@@ -2698,12 +2731,7 @@ class SettingsFragmentPresenter(
) { clearControllerSettings(controller) })
sl.add(
RunRunnable(
- context,
- R.string.input_profiles,
- 0,
- 0,
- 0,
- true
+ context, R.string.input_profiles, 0, 0, 0, true
) { fragmentView.showDialogFragment(ProfileDialog.create(menuTag)) })
updateOldControllerSettingsWarningVisibility(controller)
@@ -2714,14 +2742,18 @@ class SettingsFragmentPresenter(
*
* @param sl The list to place controller settings into.
* @param controller The controller to add settings for.
+ * @param controllerNumber The zero-based controller number.
* @param groupTypeFilter If this is non-null, only groups whose types match this are considered.
*/
private fun addControllerMappingSettings(
- sl: ArrayList<SettingsItem>,
- controller: EmulatedController,
- groupTypeFilter: Set<Int>?
+ sl: ArrayList<SettingsItem>,
+ controller: EmulatedController,
+ controllerNumber: Int,
+ groupTypeFilter: Set<Int>?
) {
- addContainerMappingSettings(sl, controller, controller, groupTypeFilter)
+ addContainerMappingSettings(
+ sl, controller, controller, controllerNumber, groupTypeFilter
+ )
}
/**
@@ -2730,12 +2762,14 @@ class SettingsFragmentPresenter(
* @param sl The list to place controller settings into.
* @param controller The encompassing controller.
* @param container The container of control groups to add settings for.
+ * @param controllerNumber The zero-based controller number.
* @param groupTypeFilter If this is non-null, only groups whose types match this are considered.
*/
private fun addContainerMappingSettings(
sl: ArrayList<SettingsItem>,
controller: EmulatedController,
container: ControlGroupContainer,
+ controllerNumber: Int,
groupTypeFilter: Set<Int>?
) {
updateOldControllerSettingsWarningVisibility(controller)
@@ -2751,10 +2785,7 @@ class SettingsFragmentPresenter(
if (group.getDefaultEnabledValue() != ControlGroup.DEFAULT_ENABLED_ALWAYS) {
sl.add(
SwitchSetting(
- context,
- ControlGroupEnabledSetting(group),
- R.string.enabled,
- 0
+ context, ControlGroupEnabledSetting(group), R.string.enabled, 0
)
)
}
@@ -2768,8 +2799,11 @@ class SettingsFragmentPresenter(
val attachmentSetting = group.getAttachmentSetting()
sl.add(
SingleChoiceSetting(
- context, InputMappingIntSetting(attachmentSetting),
- R.string.wiimote_extensions, 0, R.array.wiimoteExtensionsEntries,
+ context,
+ InputMappingIntSetting(attachmentSetting),
+ R.string.wiimote_extensions,
+ 0,
+ R.array.wiimoteExtensionsEntries,
R.array.wiimoteExtensionsValues,
MenuTag.getWiimoteExtensionMenuTag(controllerNumber)
)
@@ -2813,10 +2847,14 @@ class SettingsFragmentPresenter(
}
private fun updateOldControllerSettingsWarningVisibility(controller: EmulatedController) {
+ if (!shouldUpdateWarnings) {
+ return
+ }
+
val defaultDevice = controller.getDefaultDevice()
- hasOldControllerSettings = defaultDevice.startsWith("Android/") &&
- defaultDevice.endsWith("/Touchscreen")
+ hasOldControllerSettings =
+ defaultDevice.startsWith("Android/") && defaultDevice.endsWith("/Touchscreen")
fragmentView.setOldControllerSettingsWarningVisibility(hasOldControllerSettings)
}
@@ -2836,10 +2874,7 @@ class SettingsFragmentPresenter(
for (logType in NativeLibrary.GetLogTypeNames()) {
AdHocBooleanSetting(
- Settings.FILE_LOGGER,
- Settings.SECTION_LOGGER_LOGS,
- logType.first,
- false
+ Settings.FILE_LOGGER, Settings.SECTION_LOGGER_LOGS, logType.first, false
).setBoolean(settings!!, value)
}
@@ -2851,8 +2886,7 @@ class SettingsFragmentPresenter(
fragmentView.fragmentActivity,
R.string.wii_converting,
0,
- { context.resources.getString(if (f.get()) R.string.wii_convert_success else R.string.wii_convert_failure) }
- )
+ { context.resources.getString(if (f.get()) R.string.wii_convert_success else R.string.wii_convert_failure) })
}
fun installDriver(uri: Uri) {
@@ -2884,9 +2918,8 @@ class SettingsFragmentPresenter(
GpuDriverHelper.uninstallDriver()
withContext(Dispatchers.Main) {
with(this@SettingsFragmentPresenter) {
- this.gpuDriver =
- GpuDriverHelper.getInstalledDriverMetadata()
- ?: GpuDriverHelper.getSystemDriverMetadata(context.applicationContext)
+ this.gpuDriver = GpuDriverHelper.getInstalledDriverMetadata()
+ ?: GpuDriverHelper.getSystemDriverMetadata(context.applicationContext)
this.libNameSetting.setString(this.settings!!, "")
}
fragmentView.onDriverUninstallDone()
@@ -2898,6 +2931,24 @@ class SettingsFragmentPresenter(
const val ARG_CONTROLLER_TYPE = "controller_type"
const val ARG_SERIALPORT1_TYPE = "serialport1_type"
+ private data class SearchableSetting(
+ val name: String,
+ val description: String,
+ val menuTag: MenuTag,
+ val navigationExtras: Bundle?,
+ val position: Int,
+ val normalizedName: String,
+ val normalizedCategory: String,
+ val normalizedSearchText: String
+ )
+
+ private data class SearchableMenu(
+ val menuTag: MenuTag,
+ val category: String,
+ val navigationExtras: Bundle?,
+ val settings: List<SettingsItem>
+ )
+
// Value obtained from LogLevel in Common/Logging/Log.h
private fun getLogVerbosityEntries(): Int {
// GetMaxLogLevel is effectively a constant, but we can't call it before loading
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt
index c0b39400a7..4530792bee 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt
@@ -2,6 +2,7 @@
package org.dolphinemu.dolphinemu.features.settings.ui
+import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
@@ -50,6 +51,12 @@ interface SettingsFragmentView {
* @param menuKey Identifier for the settings group that should be shown.
*/
fun loadSubMenu(menuKey: MenuTag)
+
+ /**
+ * Opens the settings screen containing a search result and scrolls to the result.
+ */
+ fun loadSearchResult(menuKey: MenuTag, settingPosition: Int, extras: Bundle?)
+
fun showDialogFragment(fragment: DialogFragment)
/**
@@ -67,7 +74,7 @@ interface SettingsFragmentView {
/**
* Have the fragment tell the containing Activity that a Setting was modified.
*/
- fun onSettingChanged()
+ fun onSettingChanged(setting: SettingsItem? = null)
/**
* Refetches the values of all controller settings.
@@ -96,6 +103,11 @@ interface SettingsFragmentView {
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean
/**
+ * Returns the arguments used when opening a navigable setting's associated screen.
+ */
+ fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle?
+
+ /**
* Controls whether the input mapping dialog should detect inputs from all devices,
* not just the device configured for the controller.
*/
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt
index dd5d4c97f0..77414fedce 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt
@@ -2,14 +2,20 @@
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder
+import android.animation.ValueAnimator
import android.content.DialogInterface
import android.graphics.Paint
import android.graphics.Typeface
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
import android.view.View
import android.view.View.OnLongClickListener
+import android.view.animation.DecelerateInterpolator
import android.widget.TextView
import android.widget.Toast
import androidx.lifecycle.LifecycleOwner
+import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.DolphinApplication
import org.dolphinemu.dolphinemu.R
@@ -21,6 +27,9 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
LifecycleViewHolder(itemView, adapter.getFragmentLifecycle()),
LifecycleOwner, View.OnClickListener, OnLongClickListener {
+ private val defaultBackground: Drawable? = itemView.background
+ private var searchResultHighlightAnimator: ValueAnimator? = null
+
init {
itemView.setOnClickListener(this)
itemView.setOnLongClickListener(this)
@@ -39,6 +48,35 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
}
}
+ fun highlightSearchResult() {
+ clearSearchResultHighlight()
+
+ val highlight = ColorDrawable(
+ MaterialColors.getColor(
+ itemView, com.google.android.material.R.attr.colorSecondaryContainer
+ )
+ ).apply { alpha = 0 }
+ itemView.background = if (defaultBackground == null) {
+ highlight
+ } else {
+ LayerDrawable(arrayOf(highlight, defaultBackground))
+ }
+ searchResultHighlightAnimator = ValueAnimator.ofInt(
+ 0, SEARCH_RESULT_HIGHLIGHT_MAX_ALPHA
+ ).apply {
+ duration = SEARCH_RESULT_HIGHLIGHT_FADE_IN_DURATION_MS
+ interpolator = DecelerateInterpolator()
+ addUpdateListener { highlight.alpha = it.animatedValue as Int }
+ start()
+ }
+ }
+
+ fun clearSearchResultHighlight() {
+ searchResultHighlightAnimator?.cancel()
+ searchResultHighlightAnimator = null
+ itemView.background = defaultBackground
+ }
+
/**
* Called by the adapter to set this ViewHolder's child views to display the list item
* it must now represent.
@@ -102,4 +140,9 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
Toast.LENGTH_SHORT
).show()
}
+
+ companion object {
+ private const val SEARCH_RESULT_HIGHLIGHT_FADE_IN_DURATION_MS = 180L
+ private const val SEARCH_RESULT_HIGHLIGHT_MAX_ALPHA = 255
+ }
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt
new file mode 100644
index 0000000000..9471d6bf43
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.settings.ui.viewholder
+
+import android.view.View
+import org.dolphinemu.dolphinemu.databinding.ListItemSearchResultBinding
+import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
+import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsSearchResult
+import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter
+
+class SettingsSearchResultViewHolder(
+ private val binding: ListItemSearchResultBinding, adapter: SettingsAdapter
+) : SettingViewHolder(binding.root, adapter) {
+ private lateinit var result: SettingsSearchResult
+
+ override val item: SettingsItem
+ get() = result
+
+ override fun bind(item: SettingsItem) {
+ result = item as SettingsSearchResult
+ binding.textSettingName.text = item.name
+ binding.textSettingDescription.text = item.description
+ }
+
+ override fun onClick(clicked: View) {
+ adapter.onSearchResultClick(result)
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/skylanders/ui/SkylanderSlotAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/skylanders/ui/SkylanderSlotAdapter.kt
index ed3dde3e40..4c23eb4a3c 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/skylanders/ui/SkylanderSlotAdapter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/skylanders/ui/SkylanderSlotAdapter.kt
@@ -47,7 +47,7 @@ class SkylanderSlotAdapter(
holder.binding.buttonLoadFigure.setOnClickListener {
activity.setSkylanderData(0, 0, "", position)
- activity.requestSkylanderFile.launch("*/*")
+ activity.requestSkylanderFile.launch(arrayOf("*/*"))
}
val inflater = LayoutInflater.from(activity)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt
index 4aa7e2055a..e3ab05f3ab 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt
@@ -89,10 +89,6 @@ fun PreviewTheme(
private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme {
fun attr(@AttrRes attr: Int) = Color(MaterialColors.getColor(this, attr, 0))
- val background = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground)).use {
- Color(it.getColor(0, 0))
- }
-
return if (isDark) {
darkColorScheme(
primary = attr(AppCompatR.attr.colorPrimary),
@@ -111,7 +107,7 @@ private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme {
onError = attr(MaterialR.attr.colorOnError),
errorContainer = attr(MaterialR.attr.colorErrorContainer),
onErrorContainer = attr(MaterialR.attr.colorOnErrorContainer),
- background = background,
+ background = attr(android.R.attr.colorBackground),
onBackground = attr(MaterialR.attr.colorOnBackground),
surface = attr(MaterialR.attr.colorSurface),
onSurface = attr(MaterialR.attr.colorOnSurface),
@@ -140,7 +136,7 @@ private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme {
onError = attr(MaterialR.attr.colorOnError),
errorContainer = attr(MaterialR.attr.colorErrorContainer),
onErrorContainer = attr(MaterialR.attr.colorOnErrorContainer),
- background = background,
+ background = attr(android.R.attr.colorBackground),
onBackground = attr(MaterialR.attr.colorOnBackground),
surface = attr(MaterialR.attr.colorSurface),
onSurface = attr(MaterialR.attr.colorOnSurface),
@@ -257,7 +253,7 @@ fun OutlinedBox(
if (onClick != null) {
Box(
modifier = Modifier
- .fillMaxSize()
+ .matchParentSize()
.clip(MaterialTheme.shapes.extraSmall)
.clickable(
interactionSource = interactionSource,
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt
index 2aa2122ad5..8b41fc6c55 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt
@@ -1,20 +1,20 @@
package org.dolphinemu.dolphinemu.utils
-import androidx.appcompat.app.AppCompatActivity
-import org.dolphinemu.dolphinemu.R
+import android.content.res.Configuration
import android.os.Build
-import androidx.core.content.ContextCompat
+import androidx.annotation.ColorInt
+import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
-import androidx.core.view.WindowInsetsControllerCompat
+import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
-import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
-import android.content.res.Configuration
-import com.google.android.material.appbar.MaterialToolbar
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.preference.PreferenceManager
import com.google.android.material.appbar.AppBarLayout
-import com.google.android.material.elevation.ElevationOverlayProvider
+import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.color.MaterialColors
-import androidx.annotation.ColorInt
-import androidx.preference.PreferenceManager
+import com.google.android.material.elevation.ElevationOverlayProvider
+import org.dolphinemu.dolphinemu.R
+import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
object ThemeHelper {
@@ -52,8 +52,7 @@ object ThemeHelper {
.getInt(CURRENT_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
activity.delegate.localNightMode = themeMode
val windowController = WindowCompat.getInsetsController(
- activity.window,
- activity.window.decorView
+ activity.window, activity.window.decorView
)
val systemReportedThemeMode =
activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
@@ -62,6 +61,7 @@ object ThemeHelper {
Configuration.UI_MODE_NIGHT_NO -> setLightModeSystemBars(windowController)
Configuration.UI_MODE_NIGHT_YES -> setDarkModeSystemBars(windowController)
}
+
AppCompatDelegate.MODE_NIGHT_NO -> setLightModeSystemBars(windowController)
AppCompatDelegate.MODE_NIGHT_YES -> setDarkModeSystemBars(windowController)
}
@@ -83,66 +83,50 @@ object ThemeHelper {
@JvmStatic
fun saveTheme(activity: AppCompatActivity, themeValue: Int) {
- PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
- .edit()
- .putInt(CURRENT_THEME, themeValue)
- .apply()
+ PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
+ .putInt(CURRENT_THEME, themeValue).apply()
activity.recreate()
}
@JvmStatic
fun deleteThemeKey(activity: AppCompatActivity) {
- PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
- .edit()
- .remove(CURRENT_THEME)
- .apply()
+ PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
+ .remove(CURRENT_THEME).apply()
activity.recreate()
}
@JvmStatic
fun saveThemeMode(activity: AppCompatActivity, themeModeValue: Int) {
- PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
- .edit()
- .putInt(CURRENT_THEME_MODE, themeModeValue)
- .apply()
+ PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
+ .putInt(CURRENT_THEME_MODE, themeModeValue).apply()
setThemeMode(activity)
}
@JvmStatic
fun deleteThemeModeKey(activity: AppCompatActivity) {
- PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
- .edit()
- .remove(CURRENT_THEME_MODE)
- .apply()
+ PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
+ .remove(CURRENT_THEME_MODE).apply()
setThemeMode(activity)
}
@JvmStatic
fun saveBackgroundSetting(activity: AppCompatActivity, backgroundValue: Boolean) {
- PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
- .edit()
- .putBoolean(USE_BLACK_BACKGROUNDS, backgroundValue)
- .apply()
+ PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
+ .putBoolean(USE_BLACK_BACKGROUNDS, backgroundValue).apply()
activity.recreate()
}
@JvmStatic
fun deleteBackgroundSetting(activity: AppCompatActivity) {
- PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
- .edit()
- .remove(USE_BLACK_BACKGROUNDS)
- .apply()
+ PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
+ .remove(USE_BLACK_BACKGROUNDS).apply()
activity.recreate()
}
@JvmStatic
fun resetThemePreferences(activity: AppCompatActivity, applyImmediately: Boolean = false) {
- PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
- .edit()
- .remove(CURRENT_THEME)
- .remove(CURRENT_THEME_MODE)
- .remove(USE_BLACK_BACKGROUNDS)
- .apply()
+ PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
+ .remove(CURRENT_THEME).remove(CURRENT_THEME_MODE).remove(USE_BLACK_BACKGROUNDS).apply()
activity.delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
activity.delegate.applyDayNight()
if (applyImmediately) {
@@ -170,7 +154,7 @@ object ThemeHelper {
activity: AppCompatActivity, toolbar: MaterialToolbar, appBarLayout: AppBarLayout
) {
appBarLayout.addOnOffsetChangedListener { layout: AppBarLayout, verticalOffset: Int ->
- if (-verticalOffset >= layout.totalScrollRange / 2) {
+ if (layout.totalScrollRange > 0 && -verticalOffset >= layout.totalScrollRange / 2) {
@ColorInt val color =
ElevationOverlayProvider(appBarLayout.context).compositeOverlay(
MaterialColors.getColor(appBarLayout, R.attr.colorSurface),
@@ -180,8 +164,7 @@ object ThemeHelper {
setStatusBarColor(activity, color)
} else {
@ColorInt val statusBarColor = ContextCompat.getColor(
- activity.applicationContext,
- android.R.color.transparent
+ activity.applicationContext, android.R.color.transparent
)
@ColorInt val appBarColor = MaterialColors.getColor(toolbar, R.attr.colorSurface)
toolbar.setBackgroundColor(appBarColor)
@@ -198,8 +181,7 @@ object ThemeHelper {
setStatusBarColor(activity, color)
} else {
@ColorInt val statusBarColor = ContextCompat.getColor(
- activity.applicationContext,
- android.R.color.transparent
+ activity.applicationContext, android.R.color.transparent
)
setStatusBarColor(activity, statusBarColor)
}
diff --git a/Source/Android/app/src/main/res/anim/anim_settings_search_pop_in.xml b/Source/Android/app/src/main/res/anim/anim_settings_search_pop_in.xml
new file mode 100644
index 0000000000..338676f504
--- /dev/null
+++ b/Source/Android/app/src/main/res/anim/anim_settings_search_pop_in.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="180"
+ android:fromAlpha="0"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:startOffset="60"
+ android:toAlpha="1" />
diff --git a/Source/Android/app/src/main/res/anim/anim_settings_search_pop_out.xml b/Source/Android/app/src/main/res/anim/anim_settings_search_pop_out.xml
new file mode 100644
index 0000000000..f131732c71
--- /dev/null
+++ b/Source/Android/app/src/main/res/anim/anim_settings_search_pop_out.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="90"
+ android:fromAlpha="1"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:toAlpha="0" />
diff --git a/Source/Android/app/src/main/res/color/settings_search_outline.xml b/Source/Android/app/src/main/res/color/settings_search_outline.xml
new file mode 100644
index 0000000000..5a4a93e941
--- /dev/null
+++ b/Source/Android/app/src/main/res/color/settings_search_outline.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:alpha="0.45" android:color="?attr/colorOutline" />
+</selector>
diff --git a/Source/Android/app/src/main/res/drawable/ic_search.xml b/Source/Android/app/src/main/res/drawable/ic_search.xml
new file mode 100644
index 0000000000..c85c3c5e61
--- /dev/null
+++ b/Source/Android/app/src/main/res/drawable/ic_search.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M9.5,3a6.5,6.5 0,1 0,0 13a6.5,6.5 0,0 0,0 -13zM9.5,5a4.5,4.5 0,1 1,0 9a4.5,4.5 0,0 1,0 -9zM14.65,13.24l5.56,5.56l-1.41,1.41l-5.56,-5.56z" />
+</vector>
diff --git a/Source/Android/app/src/main/res/layout/activity_settings.xml b/Source/Android/app/src/main/res/layout/activity_settings.xml
index 533893df12..fbecfd3886 100644
--- a/Source/Android/app/src/main/res/layout/activity_settings.xml
+++ b/Source/Android/app/src/main/res/layout/activity_settings.xml
@@ -1,22 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
-<androidx.coordinatorlayout.widget.CoordinatorLayout
+<androidx.coordinatorlayout.widget.CoordinatorLayout android:id="@+id/coordinator_main"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/coordinator_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
+ <FrameLayout
+ android:id="@+id/frame_content_settings"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+ <TextView
+ android:id="@+id/old_controller_settings_warning"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:background="?attr/colorErrorContainer"
+ android:clickable="true"
+ android:focusable="false"
+ android:text="@string/old_controller_settings"
+ android:textColor="?attr/colorOnErrorContainer"
+ android:visibility="invisible" />
+
+ <View
+ android:id="@+id/workaround_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_gravity="bottom"
+ android:background="@android:color/transparent"
+ android:clickable="true"
+ android:focusable="false" />
+
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
+ android:background="@android:color/transparent"
+ app:backgroundTint="@android:color/transparent"
app:elevation="0dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
- style="?attr/collapsingToolbarLayoutMediumStyle"
android:id="@+id/toolbar_settings_layout"
+ style="?attr/collapsingToolbarLayoutMediumStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:contentScrim="@android:color/transparent"
@@ -31,33 +59,94 @@
</com.google.android.material.appbar.CollapsingToolbarLayout>
- </com.google.android.material.appbar.AppBarLayout>
+ <FrameLayout
+ android:id="@+id/settings_search_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/colorSurface"
+ android:paddingBottom="@dimen/spacing_medlarge">
- <FrameLayout
- android:id="@+id/frame_content_settings"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/settings_search_preview"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:clickable="true"
+ android:focusable="true"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:layout_marginEnd="@dimen/spacing_large"
+ android:layout_marginStart="@dimen/spacing_large"
+ app:cardBackgroundColor="?attr/colorSurfaceVariant"
+ app:cardCornerRadius="28dp"
+ app:cardElevation="0dp"
+ app:strokeColor="@color/settings_search_outline"
+ app:strokeWidth="1dp">
- <TextView
- android:id="@+id/old_controller_settings_warning"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom"
- android:background="?attr/colorErrorContainer"
- android:text="@string/old_controller_settings"
- android:textColor="?attr/colorOnErrorContainer"
- android:visibility="invisible"
- android:clickable="true"
- android:focusable="false" />
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:paddingEnd="@dimen/spacing_large"
+ android:paddingStart="20dp">
- <View
- android:id="@+id/workaround_view"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_gravity="bottom"
- android:clickable="true"
- android:focusable="false"
- android:background="@android:color/transparent" />
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:contentDescription="@null"
+ app:srcCompat="@drawable/ic_search"
+ app:tint="?android:attr/textColorSecondary" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/spacing_large"
+ android:text="@string/search_settings"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="18sp" />
+
+ </LinearLayout>
+
+ </com.google.android.material.card.MaterialCardView>
+
+ </FrameLayout>
+
+ <LinearLayout
+ android:id="@+id/settings_search_mode_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/colorSurface"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/settings_search_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorSurface"
+ app:navigationContentDescription="@string/search_settings_back"
+ app:navigationIcon="?attr/homeAsUpIndicator">
+
+ <androidx.appcompat.widget.SearchView
+ android:id="@+id/settings_search"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent"
+ android:imeOptions="actionSearch"
+ android:inputType="text"
+ app:iconifiedByDefault="false"
+ app:queryBackground="@android:color/transparent"
+ app:queryHint="@string/search_settings"
+ app:searchIcon="@null" />
+
+ </com.google.android.material.appbar.MaterialToolbar>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/settings_search_outline" />
+
+ </LinearLayout>
+
+ </com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/Source/Android/app/src/main/res/layout/fragment_settings.xml b/Source/Android/app/src/main/res/layout/fragment_settings.xml
index 77b673b53c..5b8896d9f7 100644
--- a/Source/Android/app/src/main/res/layout/fragment_settings.xml
+++ b/Source/Android/app/src/main/res/layout/fragment_settings.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -11,4 +10,16 @@
android:layout_height="match_parent"
android:clipToPadding="false" />
+ <TextView
+ android:id="@+id/text_no_search_results"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:padding="@dimen/spacing_xtralarge"
+ android:text="@string/search_settings_no_results"
+ android:textAlignment="center"
+ android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
+ android:textColor="?android:attr/textColorSecondary"
+ android:visibility="gone" />
+
</FrameLayout>
diff --git a/Source/Android/app/src/main/res/layout/list_item_search_result.xml b/Source/Android/app/src/main/res/layout/list_item_search_result.xml
new file mode 100644
index 0000000000..237fc46c04
--- /dev/null
+++ b/Source/Android/app/src/main/res/layout/list_item_search_result.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:minHeight="64dp"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/spacing_large"
+ android:paddingEnd="@dimen/spacing_large"
+ android:paddingStart="@dimen/spacing_large"
+ android:paddingTop="@dimen/spacing_large">
+
+ <TextView
+ android:id="@+id/text_setting_name"
+ style="@style/TextAppearance.MaterialComponents.Headline5"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ android:textSize="16sp"
+ tools:text="Internal Resolution" />
+
+ <TextView
+ android:id="@+id/text_setting_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/spacing_small"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textAlignment="viewStart"
+ android:textColor="?android:attr/textColorSecondary"
+ tools:text="Graphics Settings › Enhancements" />
+
+</LinearLayout>
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index 9d97169d95..2204f4380f 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -62,6 +62,10 @@
<!-- Main Preference Fragment -->
<string name="settings">Settings</string>
+ <string name="search_settings">Search settings</string>
+ <string name="search_settings_back">Back to settings</string>
+ <string name="search_settings_no_results">No settings found for “%1$s”</string>
+ <string name="search_settings_category_path">%1$s › %2$s</string>
<string name="game_settings">Game Settings: %1$s</string>
<string name="config">Config</string>
<string name="graphics_settings">Graphics Settings</string>
@@ -952,6 +956,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="skylander_id">ID</string>
<string name="skylander_variant">Variant</string>
<string name="invalid_skylander">Invalid Skylander Selection</string>
+ <string name="skylander_load_failed">Failed to load Skylander figure</string>
<string name="emulate_infinity_base">Infinity Base</string>
<string name="infinity_manager">Infinity Manager</string>
@@ -1012,6 +1017,9 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="netplay_start">Start</string>
<string name="netplay_start_warning_title">Warning</string>
<string name="netplay_start_warning_not_all_players_have_game">Not all players have the game. Do you really want to start?</string>
+ <string name="netplay_dual_core_warning_title">Dual Core Enabled</string>
+ <string name="netplay_dual_core_warning_message">Dual core during netplay is not recommended. It can cause performance issues and prevent certain games from booting.</string>
+ <string name="netplay_dont_warn_again">Don\'t warn again</string>
<string name="netplay_chat_label">Chat</string>
<string name="netplay_chat_send">Send</string>
<string name="netplay_message_game_changed">Game changed to %1$s</string>
@@ -1023,6 +1031,11 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="netplay_players_name">Name</string>
<string name="netplay_players_ping">Ping</string>
<string name="netplay_players_mapping">Mapping</string>
+ <string name="netplay_controllers_gamecube">GameCube</string>
+ <string name="netplay_controllers_wii_remotes">Wii Remotes</string>
+ <string name="netplay_controllers_port">Port %1$d</string>
+ <string name="netplay_controllers_remote">Remote %1$d</string>
+ <string name="netplay_controllers_none">None</string>
<string name="netplay_network_mode_label">Input mode</string>
<string name="netplay_network_mode_fair_input_delay">Fair Input Delay</string>
<string name="netplay_network_mode_host_input_authority">Host Input Authority</string>
diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp
index d6e24d250b..16804b808b 100644
--- a/Source/Android/jni/NetPlay/Netplay.cpp
+++ b/Source/Android/jni/NetPlay/Netplay.cpp
@@ -1,8 +1,11 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <array>
#include <memory>
#include <string>
+#include <tuple>
+#include <type_traits>
#include <vector>
#include <jni.h>
@@ -38,6 +41,30 @@ static NetPlay::NetPlayServer* GetServerPointer(JNIEnv* env, jobject obj)
env->GetLongField(obj, IDCache::GetNetPlayServerPointer()));
}
+static jintArray PadMappingArrayToJIntArray(JNIEnv* env, const NetPlay::PadMappingArray& mapping)
+{
+ const jsize size = static_cast<jsize>(mapping.size());
+ jintArray jmapping = env->NewIntArray(size);
+ std::array<jint, std::tuple_size_v<std::decay_t<decltype(mapping)>>> values;
+ for (size_t i = 0; i < mapping.size(); i++)
+ values[i] = static_cast<jint>(mapping[i]);
+ env->SetIntArrayRegion(jmapping, 0, size, values.data());
+ return jmapping;
+}
+
+static bool JIntArrayToPadMappingArray(JNIEnv* env, jintArray jmapping,
+ NetPlay::PadMappingArray* mapping)
+{
+ if (env->GetArrayLength(jmapping) != static_cast<jsize>(mapping->size()))
+ return false;
+
+ std::array<jint, std::tuple_size_v<std::decay_t<decltype(*mapping)>>> values;
+ env->GetIntArrayRegion(jmapping, 0, static_cast<jsize>(values.size()), values.data());
+ for (size_t i = 0; i < mapping->size(); i++)
+ (*mapping)[i] = static_cast<NetPlay::PlayerId>(values[i]);
+ return true;
+}
+
extern "C" {
JNIEXPORT void JNICALL
@@ -224,6 +251,54 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReconnectTr
Common::g_TraversalClient->ReconnectToServer();
}
+JNIEXPORT jintArray JNICALL
+Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetPadMapping(JNIEnv* env,
+ jobject obj)
+{
+ auto* server = GetServerPointer(env, obj);
+ if (!server)
+ return env->NewIntArray(0);
+
+ return PadMappingArrayToJIntArray(env, server->GetPadMapping());
+}
+
+JNIEXPORT jintArray JNICALL
+Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetWiimoteMapping(JNIEnv* env,
+ jobject obj)
+{
+ auto* server = GetServerPointer(env, obj);
+ if (!server)
+ return env->NewIntArray(0);
+
+ return PadMappingArrayToJIntArray(env, server->GetWiimoteMapping());
+}
+
+JNIEXPORT void JNICALL
+Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSetPadMapping(
+ JNIEnv* env, jobject obj, jintArray jmapping)
+{
+ auto* server = GetServerPointer(env, obj);
+ if (!server)
+ return;
+
+ NetPlay::PadMappingArray mapping = server->GetPadMapping();
+ if (JIntArrayToPadMappingArray(env, jmapping, &mapping))
+ server->SetPadMapping(mapping);
+}
+
+JNIEXPORT void JNICALL
+Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSetWiimoteMapping(
+ JNIEnv* env, jobject obj, jintArray jmapping)
+{
+ auto* server = GetServerPointer(env, obj);
+ if (!server)
+ return;
+
+ NetPlay::PadMappingArray mapping = server->GetWiimoteMapping();
+ if (JIntArrayToPadMappingArray(env, jmapping, &mapping))
+ server->SetWiimoteMapping(mapping);
+}
+
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks(
JNIEnv*, jobject, jlong pointer)