Reorganize settings into sub-screens with per-platform controller layouts
Move controller-related settings (devices, remap, button mapping, touch overlay, layout editor) into a dedicated ControllerSettingsScreen. Add per-platform layout entries for all implemented consoles and make layout editor button labels platform-aware. Auto-focus search field on open.
This commit is contained in:
@@ -16,6 +16,7 @@ import com.lazy.emulate.ui.screens.controller.ControllerLayoutScreen
|
||||
import com.lazy.emulate.ui.screens.controller.ControllerTestScreen
|
||||
import com.lazy.emulate.ui.screens.game.GameScreen
|
||||
import com.lazy.emulate.ui.screens.home.HomeScreen
|
||||
import com.lazy.emulate.ui.screens.settings.ControllerSettingsScreen
|
||||
import com.lazy.emulate.ui.screens.settings.SettingsScreen
|
||||
|
||||
@Composable
|
||||
@@ -40,20 +41,28 @@ fun EmulateNavGraph(
|
||||
},
|
||||
onSettingsClick = {
|
||||
navController.navigate(Screen.Settings.route)
|
||||
},
|
||||
onControllerLayoutClick = { consoleType ->
|
||||
navController.navigate(Screen.ControllerLayout.createRoute(consoleType.name))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Screen.Settings.route) {
|
||||
SettingsScreen(
|
||||
preferencesManager = preferencesManager,
|
||||
onBack = { navController.popBackStack() },
|
||||
onControllerSettings = { navController.navigate(Screen.ControllerSettings.route) }
|
||||
)
|
||||
}
|
||||
|
||||
composable(Screen.ControllerSettings.route) {
|
||||
ControllerSettingsScreen(
|
||||
controllerManager = controllerManager,
|
||||
preferencesManager = preferencesManager,
|
||||
buttonMappingManager = buttonMappingManager,
|
||||
onBack = { navController.popBackStack() },
|
||||
onTestController = { navController.navigate(Screen.ControllerTest.route) }
|
||||
onTestController = { navController.navigate(Screen.ControllerTest.route) },
|
||||
onControllerLayout = { consoleType ->
|
||||
navController.navigate(Screen.ControllerLayout.createRoute(consoleType.name))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.lazy.emulate.ui.navigation
|
||||
sealed class Screen(val route: String) {
|
||||
data object Home : Screen("home")
|
||||
data object Settings : Screen("settings")
|
||||
data object ControllerSettings : Screen("controller_settings")
|
||||
data object ControllerTest : Screen("controller_test")
|
||||
data object ControllerLayout : Screen("controller_layout/{consoleType}") {
|
||||
fun createRoute(consoleType: String) = "controller_layout/$consoleType"
|
||||
|
||||
@@ -97,6 +97,7 @@ fun ControllerLayoutScreen(
|
||||
layout.buttons.forEach { buttonPos ->
|
||||
DraggableButton(
|
||||
buttonPosition = buttonPos,
|
||||
consoleType = consoleType,
|
||||
screenWidth = screenWidth,
|
||||
screenHeight = screenHeight,
|
||||
opacity = opacity,
|
||||
@@ -170,6 +171,7 @@ fun ControllerLayoutScreen(
|
||||
@Composable
|
||||
private fun DraggableButton(
|
||||
buttonPosition: ButtonPosition,
|
||||
consoleType: ConsoleType,
|
||||
screenWidth: Float,
|
||||
screenHeight: Float,
|
||||
opacity: Float,
|
||||
@@ -198,31 +200,70 @@ private fun DraggableButton(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = buttonPosition.button.toLabel(),
|
||||
text = buttonPosition.button.toLabel(consoleType),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.White.copy(alpha = opacity)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun GamepadButton.toLabel(): String = when (this) {
|
||||
GamepadButton.FACE_BOTTOM -> "X"
|
||||
GamepadButton.FACE_RIGHT -> "O"
|
||||
GamepadButton.FACE_LEFT -> "[]"
|
||||
GamepadButton.FACE_TOP -> "/\\"
|
||||
GamepadButton.DPAD_UP -> "Up"
|
||||
GamepadButton.DPAD_DOWN -> "Dn"
|
||||
GamepadButton.DPAD_LEFT -> "Lt"
|
||||
GamepadButton.DPAD_RIGHT -> "Rt"
|
||||
GamepadButton.L1 -> "L1"
|
||||
GamepadButton.R1 -> "R1"
|
||||
GamepadButton.L2 -> "L2"
|
||||
GamepadButton.R2 -> "R2"
|
||||
GamepadButton.L3 -> "L3"
|
||||
GamepadButton.R3 -> "R3"
|
||||
GamepadButton.START -> "ST"
|
||||
GamepadButton.SELECT -> "SL"
|
||||
GamepadButton.MODE -> "MD"
|
||||
GamepadButton.Z_BUTTON -> "Z"
|
||||
GamepadButton.C_BUTTON -> "C"
|
||||
private fun GamepadButton.toLabel(consoleType: ConsoleType): String {
|
||||
// Shared labels for non-face buttons
|
||||
val shared = when (this) {
|
||||
GamepadButton.DPAD_UP -> "Up"
|
||||
GamepadButton.DPAD_DOWN -> "Dn"
|
||||
GamepadButton.DPAD_LEFT -> "Lt"
|
||||
GamepadButton.DPAD_RIGHT -> "Rt"
|
||||
GamepadButton.L1 -> "L1"
|
||||
GamepadButton.R1 -> "R1"
|
||||
GamepadButton.L2 -> "L2"
|
||||
GamepadButton.R2 -> "R2"
|
||||
GamepadButton.L3 -> "L3"
|
||||
GamepadButton.R3 -> "R3"
|
||||
GamepadButton.START -> "ST"
|
||||
GamepadButton.SELECT -> "SL"
|
||||
GamepadButton.MODE -> "MD"
|
||||
GamepadButton.Z_BUTTON -> "Z"
|
||||
GamepadButton.C_BUTTON -> "C"
|
||||
else -> null
|
||||
}
|
||||
if (shared != null) return shared
|
||||
|
||||
return when (consoleType) {
|
||||
ConsoleType.PS1, ConsoleType.PS2 -> when (this) {
|
||||
GamepadButton.FACE_BOTTOM -> "X"
|
||||
GamepadButton.FACE_RIGHT -> "O"
|
||||
GamepadButton.FACE_LEFT -> "[]"
|
||||
GamepadButton.FACE_TOP -> "/\\"
|
||||
else -> ""
|
||||
}
|
||||
ConsoleType.NES -> when (this) {
|
||||
GamepadButton.FACE_BOTTOM -> "A"
|
||||
GamepadButton.FACE_RIGHT -> "B"
|
||||
GamepadButton.FACE_LEFT -> "A"
|
||||
GamepadButton.FACE_TOP -> "B"
|
||||
else -> ""
|
||||
}
|
||||
ConsoleType.SNES -> when (this) {
|
||||
GamepadButton.FACE_BOTTOM -> "B"
|
||||
GamepadButton.FACE_RIGHT -> "A"
|
||||
GamepadButton.FACE_LEFT -> "Y"
|
||||
GamepadButton.FACE_TOP -> "X"
|
||||
else -> ""
|
||||
}
|
||||
ConsoleType.N64 -> when (this) {
|
||||
GamepadButton.FACE_BOTTOM -> "A"
|
||||
GamepadButton.FACE_RIGHT -> "B"
|
||||
GamepadButton.FACE_LEFT -> "A"
|
||||
GamepadButton.FACE_TOP -> "B"
|
||||
else -> ""
|
||||
}
|
||||
ConsoleType.GENESIS -> when (this) {
|
||||
GamepadButton.FACE_BOTTOM -> "A"
|
||||
GamepadButton.FACE_RIGHT -> "B"
|
||||
GamepadButton.FACE_LEFT -> "X"
|
||||
GamepadButton.FACE_TOP -> "Y"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Gamepad
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.SportsEsports
|
||||
@@ -36,6 +35,7 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -44,6 +44,8 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.lazy.emulate.data.model.Game
|
||||
import com.lazy.emulate.data.repository.GameRepository
|
||||
@@ -65,12 +67,12 @@ fun HomeScreen(
|
||||
windowSizeClass: WindowSizeClass,
|
||||
gameRepository: GameRepository,
|
||||
onGameSelected: (Game) -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onControllerLayoutClick: (ConsoleType) -> Unit
|
||||
onSettingsClick: () -> Unit
|
||||
) {
|
||||
val games by gameRepository.games.collectAsState()
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var searchActive by remember { mutableStateOf(false) }
|
||||
val searchFocusRequester = remember { FocusRequester() }
|
||||
val layoutType = windowSizeClass.toLayoutType()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -100,10 +102,14 @@ fun HomeScreen(
|
||||
TopAppBar(
|
||||
title = {
|
||||
if (searchActive) {
|
||||
LaunchedEffect(Unit) {
|
||||
searchFocusRequester.requestFocus()
|
||||
}
|
||||
androidx.compose.foundation.text.BasicTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
singleLine = true,
|
||||
modifier = Modifier.focusRequester(searchFocusRequester),
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
@@ -146,13 +152,6 @@ fun HomeScreen(
|
||||
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||
}
|
||||
}
|
||||
val controllerConsole = when (currentTab) {
|
||||
is HomeTab.Console -> currentTab.consoleType
|
||||
else -> ConsoleType.PS1
|
||||
}
|
||||
IconButton(onClick = { onControllerLayoutClick(controllerConsole) }) {
|
||||
Icon(Icons.Default.Gamepad, contentDescription = "Controller Layout")
|
||||
}
|
||||
IconButton(onClick = onSettingsClick) {
|
||||
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
package com.lazy.emulate.ui.screens.settings
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Bluetooth
|
||||
import androidx.compose.material.icons.filled.BluetoothConnected
|
||||
import androidx.compose.material.icons.filled.Gamepad
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.lazy.emulate.data.PreferencesManager
|
||||
import com.lazy.emulate.emulation.ConsoleType
|
||||
import com.lazy.emulate.input.ButtonMappingManager
|
||||
import com.lazy.emulate.input.ControllerManager
|
||||
import com.lazy.emulate.input.GameController
|
||||
import com.lazy.emulate.input.GamepadButton
|
||||
import com.lazy.emulate.input.OverlayVisibility
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ControllerSettingsScreen(
|
||||
controllerManager: ControllerManager,
|
||||
preferencesManager: PreferencesManager,
|
||||
buttonMappingManager: ButtonMappingManager,
|
||||
onBack: () -> Unit,
|
||||
onTestController: () -> Unit,
|
||||
onControllerLayout: (ConsoleType) -> Unit
|
||||
) {
|
||||
val controllers by controllerManager.connectedControllers.collectAsState()
|
||||
val activeController by controllerManager.activeController.collectAsState()
|
||||
|
||||
var overlayVisibility by remember { mutableStateOf(preferencesManager.overlayVisibility) }
|
||||
|
||||
// Button remapping state
|
||||
var showRemapDialog by remember { mutableStateOf(false) }
|
||||
var remapConsole by remember { mutableStateOf(ConsoleType.PS1) }
|
||||
var remapButton by remember { mutableStateOf<GamepadButton?>(null) }
|
||||
var remapVersion by remember { mutableStateOf(0) }
|
||||
|
||||
// Controller remap state
|
||||
var showControllerRemapDialog by remember { mutableStateOf(false) }
|
||||
var controllerRemapFrom by remember { mutableStateOf<GamepadButton?>(null) }
|
||||
var controllerRemapVersion by remember { mutableStateOf(0) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Controllers") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Connected controllers
|
||||
Text("Devices", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
if (controllers.isEmpty()) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Bluetooth,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
"No controllers connected",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
"Pair a Bluetooth controller in system settings",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
controllers.forEachIndexed { index, controller ->
|
||||
ControllerItem(
|
||||
controller = controller,
|
||||
isActive = controller.deviceId == activeController?.deviceId,
|
||||
onClick = { controllerManager.setActiveController(controller) }
|
||||
)
|
||||
if (index < controllers.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onTestController)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Gamepad,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
Text("Test Controller", style = MaterialTheme.typography.bodyLarge)
|
||||
Text(
|
||||
"Live button tester and raw event log",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Touch Overlay
|
||||
Text("Touch Overlay", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
overlayVisibility = overlayVisibility.next()
|
||||
preferencesManager.overlayVisibility = overlayVisibility
|
||||
}
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text("Visibility", style = MaterialTheme.typography.bodyLarge)
|
||||
Text(
|
||||
overlayVisibility.label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
// Per-platform layout editors
|
||||
val implementedConsoles = ConsoleType.entries.filter { it.isImplemented }
|
||||
implementedConsoles.forEachIndexed { index, console ->
|
||||
ListItem(
|
||||
headlineContent = { Text("${console.displayName} Layout") },
|
||||
trailingContent = {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable { onControllerLayout(console) }
|
||||
)
|
||||
if (index < implementedConsoles.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Controller Button Remap
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Controller Remap", style = MaterialTheme.typography.titleLarge)
|
||||
IconButton(onClick = {
|
||||
buttonMappingManager.resetControllerRemaps()
|
||||
controllerRemapVersion++
|
||||
}) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Reset remaps")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
"Remap physical controller buttons",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
val controllerRemaps = remember(controllerRemapVersion) {
|
||||
buttonMappingManager.getControllerRemaps()
|
||||
}
|
||||
val controllerButtons = listOf(
|
||||
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
||||
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
||||
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
||||
GamepadButton.START, GamepadButton.SELECT
|
||||
)
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
controllerButtons.forEachIndexed { index, button ->
|
||||
val currentTarget = controllerRemaps[button]
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
controllerRemapFrom = button
|
||||
showControllerRemapDialog = true
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
buttonDisplayName(button),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
if (currentTarget != null) buttonDisplayName(currentTarget) else "Default",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (currentTarget != null)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (index < controllerButtons.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Button Mapping (per-console libretro mapping)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Button Mapping", style = MaterialTheme.typography.titleLarge)
|
||||
IconButton(onClick = {
|
||||
buttonMappingManager.resetToDefaults(remapConsole)
|
||||
remapVersion++
|
||||
}) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Reset to defaults")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
"Map buttons to libretro IDs for ${remapConsole.displayName}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
val currentMapping = remember(remapVersion, remapConsole) {
|
||||
buttonMappingManager.getMapping(remapConsole)
|
||||
}
|
||||
val mappableButtons = listOf(
|
||||
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
||||
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
||||
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
||||
GamepadButton.START, GamepadButton.SELECT
|
||||
)
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
mappableButtons.forEachIndexed { index, button ->
|
||||
val retroId = currentMapping[button] ?: -1
|
||||
val defaultId = buttonMappingManager.getDefaultMapping(remapConsole)[button] ?: -1
|
||||
val isCustom = retroId != defaultId
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
remapButton = button
|
||||
showRemapDialog = true
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
buttonDisplayName(button),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
retroIdDisplayName(retroId),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (isCustom) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = if (isCustom) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
if (index < mappableButtons.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Button mapping dialog (libretro ID picker)
|
||||
if (showRemapDialog && remapButton != null) {
|
||||
val retroIds = listOf(
|
||||
0 to "B (Cross)", 1 to "Y (Square)", 2 to "Select", 3 to "Start",
|
||||
4 to "D-Up", 5 to "D-Down", 6 to "D-Left", 7 to "D-Right",
|
||||
8 to "A (Circle)", 9 to "X (Triangle)", 10 to "L1", 11 to "R1",
|
||||
12 to "L2", 13 to "R2", 14 to "L3", 15 to "R3"
|
||||
)
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRemapDialog = false },
|
||||
title = { Text("Map ${buttonDisplayName(remapButton!!)}") },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
"Select the libretro button to map to:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
retroIds.forEach { (id, name) ->
|
||||
Text(
|
||||
text = name,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
buttonMappingManager.setButtonMapping(remapConsole, remapButton!!, id)
|
||||
remapVersion++
|
||||
showRemapDialog = false
|
||||
}
|
||||
.padding(vertical = 10.dp, horizontal = 4.dp),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showRemapDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Controller remap dialog (button-to-button picker)
|
||||
if (showControllerRemapDialog && controllerRemapFrom != null) {
|
||||
val targetButtons = listOf(
|
||||
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
||||
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
||||
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
||||
GamepadButton.L3, GamepadButton.R3,
|
||||
GamepadButton.DPAD_UP, GamepadButton.DPAD_DOWN,
|
||||
GamepadButton.DPAD_LEFT, GamepadButton.DPAD_RIGHT,
|
||||
GamepadButton.START, GamepadButton.SELECT
|
||||
)
|
||||
AlertDialog(
|
||||
onDismissRequest = { showControllerRemapDialog = false },
|
||||
title = { Text("Remap ${buttonDisplayName(controllerRemapFrom!!)}") },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
"Select what this button should act as:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
targetButtons.forEach { target ->
|
||||
Text(
|
||||
text = buttonDisplayName(target),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
buttonMappingManager.setControllerRemap(controllerRemapFrom!!, target)
|
||||
controllerRemapVersion++
|
||||
showControllerRemapDialog = false
|
||||
}
|
||||
.padding(vertical = 10.dp, horizontal = 4.dp),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showControllerRemapDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buttonDisplayName(button: GamepadButton): String {
|
||||
return when (button) {
|
||||
GamepadButton.FACE_BOTTOM -> "Cross / A"
|
||||
GamepadButton.FACE_RIGHT -> "Circle / B"
|
||||
GamepadButton.FACE_LEFT -> "Square / X"
|
||||
GamepadButton.FACE_TOP -> "Triangle / Y"
|
||||
GamepadButton.DPAD_UP -> "D-Pad Up"
|
||||
GamepadButton.DPAD_DOWN -> "D-Pad Down"
|
||||
GamepadButton.DPAD_LEFT -> "D-Pad Left"
|
||||
GamepadButton.DPAD_RIGHT -> "D-Pad Right"
|
||||
GamepadButton.L1 -> "L1"
|
||||
GamepadButton.R1 -> "R1"
|
||||
GamepadButton.L2 -> "L2"
|
||||
GamepadButton.R2 -> "R2"
|
||||
GamepadButton.L3 -> "L3"
|
||||
GamepadButton.R3 -> "R3"
|
||||
GamepadButton.START -> "Start"
|
||||
GamepadButton.SELECT -> "Select"
|
||||
GamepadButton.MODE -> "Mode"
|
||||
GamepadButton.Z_BUTTON -> "Z"
|
||||
GamepadButton.C_BUTTON -> "C"
|
||||
}
|
||||
}
|
||||
|
||||
private fun retroIdDisplayName(id: Int): String {
|
||||
return when (id) {
|
||||
0 -> "B (Cross)"
|
||||
1 -> "Y (Square)"
|
||||
2 -> "Select"
|
||||
3 -> "Start"
|
||||
4 -> "D-Up"
|
||||
5 -> "D-Down"
|
||||
6 -> "D-Left"
|
||||
7 -> "D-Right"
|
||||
8 -> "A (Circle)"
|
||||
9 -> "X (Triangle)"
|
||||
10 -> "L1"
|
||||
11 -> "R1"
|
||||
12 -> "L2"
|
||||
13 -> "R2"
|
||||
14 -> "L3"
|
||||
15 -> "R3"
|
||||
else -> "Unknown ($id)"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ControllerItem(
|
||||
controller: GameController,
|
||||
isActive: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isActive) Icons.Default.BluetoothConnected else Icons.Default.Gamepad,
|
||||
contentDescription = null,
|
||||
tint = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = controller.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = if (isActive) "Active" else if (controller.isExternal) "Bluetooth" else "Built-in",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,45 @@
|
||||
package com.lazy.emulate.ui.screens.settings
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Bluetooth
|
||||
import androidx.compose.material.icons.filled.BluetoothConnected
|
||||
import androidx.compose.material.icons.filled.Gamepad
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.lazy.emulate.data.PreferencesManager
|
||||
import com.lazy.emulate.emulation.ConsoleType
|
||||
import com.lazy.emulate.emulation.DisplayMode
|
||||
import com.lazy.emulate.input.ButtonMappingManager
|
||||
import com.lazy.emulate.input.ControllerManager
|
||||
import com.lazy.emulate.input.GameController
|
||||
import com.lazy.emulate.input.GamepadButton
|
||||
import com.lazy.emulate.input.OverlayVisibility
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
controllerManager: ControllerManager,
|
||||
preferencesManager: PreferencesManager,
|
||||
buttonMappingManager: ButtonMappingManager,
|
||||
onBack: () -> Unit,
|
||||
onTestController: () -> Unit = {}
|
||||
onControllerSettings: () -> Unit
|
||||
) {
|
||||
val controllers by controllerManager.connectedControllers.collectAsState()
|
||||
val activeController by controllerManager.activeController.collectAsState()
|
||||
|
||||
var displayMode by remember { mutableStateOf(preferencesManager.displayMode) }
|
||||
var overlayVisibility by remember { mutableStateOf(preferencesManager.overlayVisibility) }
|
||||
|
||||
// Button remapping state
|
||||
var showRemapDialog by remember { mutableStateOf(false) }
|
||||
var remapConsole by remember { mutableStateOf(ConsoleType.PS1) }
|
||||
var remapButton by remember { mutableStateOf<GamepadButton?>(null) }
|
||||
var remapTarget by remember { mutableStateOf<String?>(null) }
|
||||
// Force recomposition after remap changes
|
||||
var remapVersion by remember { mutableStateOf(0) }
|
||||
|
||||
// Controller remap state
|
||||
var showControllerRemapDialog by remember { mutableStateOf(false) }
|
||||
var controllerRemapFrom by remember { mutableStateOf<GamepadButton?>(null) }
|
||||
var controllerRemapVersion by remember { mutableStateOf(0) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -95,266 +60,28 @@ fun SettingsScreen(
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Controllers section
|
||||
Text(
|
||||
text = "Controllers",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
// Controllers
|
||||
Text("Controllers", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
if (controllers.isEmpty()) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
ListItem(
|
||||
headlineContent = { Text("Controller & Touch Overlay") },
|
||||
supportingContent = { Text("Devices, remapping, touch layout") },
|
||||
trailingContent = {
|
||||
Icon(
|
||||
Icons.Default.Bluetooth,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = "No controllers connected",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
text = "Pair a Bluetooth controller in system settings",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
controllers.forEachIndexed { index, controller ->
|
||||
ControllerItem(
|
||||
controller = controller,
|
||||
isActive = controller.deviceId == activeController?.deviceId,
|
||||
onClick = { controllerManager.setActiveController(controller) }
|
||||
)
|
||||
if (index < controllers.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Test Controller — live tester with raw event log
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onTestController)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Gamepad,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
"Test Controller",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
"Live button tester and raw event log",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Touch overlay section
|
||||
Text(
|
||||
text = "Touch Overlay",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
overlayVisibility = overlayVisibility.next()
|
||||
preferencesManager.overlayVisibility = overlayVisibility
|
||||
}
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text("Visibility", style = MaterialTheme.typography.bodyLarge)
|
||||
Text(
|
||||
overlayVisibility.label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Controller Button Remap section
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Controller Remap",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = onControllerSettings)
|
||||
)
|
||||
IconButton(onClick = {
|
||||
buttonMappingManager.resetControllerRemaps()
|
||||
controllerRemapVersion++
|
||||
}) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Reset remaps")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Remap physical controller buttons",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
val controllerRemaps = remember(controllerRemapVersion) {
|
||||
buttonMappingManager.getControllerRemaps()
|
||||
}
|
||||
val controllerButtons = listOf(
|
||||
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
||||
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
||||
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
||||
GamepadButton.START, GamepadButton.SELECT
|
||||
)
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
controllerButtons.forEachIndexed { index, button ->
|
||||
val currentTarget = controllerRemaps[button]
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
controllerRemapFrom = button
|
||||
showControllerRemapDialog = true
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
buttonDisplayName(button),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
if (currentTarget != null) buttonDisplayName(currentTarget) else "Default",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (currentTarget != null)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (index < controllerButtons.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Button Mapping section (per-console libretro mapping)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Button Mapping",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
IconButton(onClick = {
|
||||
buttonMappingManager.resetToDefaults(remapConsole)
|
||||
remapVersion++
|
||||
}) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Reset to defaults")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Map buttons to libretro IDs for ${remapConsole.displayName}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
// Emulation
|
||||
Text("Emulation", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
val currentMapping = remember(remapVersion, remapConsole) {
|
||||
buttonMappingManager.getMapping(remapConsole)
|
||||
}
|
||||
val mappableButtons = listOf(
|
||||
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
||||
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
||||
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
||||
GamepadButton.START, GamepadButton.SELECT
|
||||
)
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
mappableButtons.forEachIndexed { index, button ->
|
||||
val retroId = currentMapping[button] ?: -1
|
||||
val defaultId = buttonMappingManager.getDefaultMapping(remapConsole)[button] ?: -1
|
||||
val isCustom = retroId != defaultId
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
remapButton = button
|
||||
remapTarget = null
|
||||
showRemapDialog = true
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
buttonDisplayName(button),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
retroIdDisplayName(retroId),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (isCustom) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = if (isCustom) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
if (index < mappableButtons.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Emulation section
|
||||
Text(
|
||||
text = "Emulation",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text("Audio", style = MaterialTheme.typography.bodyLarge)
|
||||
@@ -377,13 +104,9 @@ fun SettingsScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Display section
|
||||
Text(
|
||||
text = "Display",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
// Display
|
||||
Text("Display", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -406,10 +129,7 @@ fun SettingsScreen(
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// About
|
||||
Text(
|
||||
text = "About",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Text("About", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
@@ -423,172 +143,4 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Button mapping dialog (libretro ID picker)
|
||||
if (showRemapDialog && remapButton != null) {
|
||||
val retroIds = listOf(
|
||||
0 to "B (Cross)", 1 to "Y (Square)", 2 to "Select", 3 to "Start",
|
||||
4 to "D-Up", 5 to "D-Down", 6 to "D-Left", 7 to "D-Right",
|
||||
8 to "A (Circle)", 9 to "X (Triangle)", 10 to "L1", 11 to "R1",
|
||||
12 to "L2", 13 to "R2", 14 to "L3", 15 to "R3"
|
||||
)
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRemapDialog = false },
|
||||
title = { Text("Map ${buttonDisplayName(remapButton!!)}") },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
"Select the libretro button to map to:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
retroIds.forEach { (id, name) ->
|
||||
Text(
|
||||
text = name,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
buttonMappingManager.setButtonMapping(remapConsole, remapButton!!, id)
|
||||
remapVersion++
|
||||
showRemapDialog = false
|
||||
}
|
||||
.padding(vertical = 10.dp, horizontal = 4.dp),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showRemapDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Controller remap dialog (button-to-button picker)
|
||||
if (showControllerRemapDialog && controllerRemapFrom != null) {
|
||||
val targetButtons = listOf(
|
||||
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
||||
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
||||
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
||||
GamepadButton.L3, GamepadButton.R3,
|
||||
GamepadButton.DPAD_UP, GamepadButton.DPAD_DOWN,
|
||||
GamepadButton.DPAD_LEFT, GamepadButton.DPAD_RIGHT,
|
||||
GamepadButton.START, GamepadButton.SELECT
|
||||
)
|
||||
AlertDialog(
|
||||
onDismissRequest = { showControllerRemapDialog = false },
|
||||
title = { Text("Remap ${buttonDisplayName(controllerRemapFrom!!)}") },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
"Select what this button should act as:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
targetButtons.forEach { target ->
|
||||
Text(
|
||||
text = buttonDisplayName(target),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
buttonMappingManager.setControllerRemap(controllerRemapFrom!!, target)
|
||||
controllerRemapVersion++
|
||||
showControllerRemapDialog = false
|
||||
}
|
||||
.padding(vertical = 10.dp, horizontal = 4.dp),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showControllerRemapDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buttonDisplayName(button: GamepadButton): String {
|
||||
return when (button) {
|
||||
GamepadButton.FACE_BOTTOM -> "Cross / A"
|
||||
GamepadButton.FACE_RIGHT -> "Circle / B"
|
||||
GamepadButton.FACE_LEFT -> "Square / X"
|
||||
GamepadButton.FACE_TOP -> "Triangle / Y"
|
||||
GamepadButton.DPAD_UP -> "D-Pad Up"
|
||||
GamepadButton.DPAD_DOWN -> "D-Pad Down"
|
||||
GamepadButton.DPAD_LEFT -> "D-Pad Left"
|
||||
GamepadButton.DPAD_RIGHT -> "D-Pad Right"
|
||||
GamepadButton.L1 -> "L1"
|
||||
GamepadButton.R1 -> "R1"
|
||||
GamepadButton.L2 -> "L2"
|
||||
GamepadButton.R2 -> "R2"
|
||||
GamepadButton.L3 -> "L3"
|
||||
GamepadButton.R3 -> "R3"
|
||||
GamepadButton.START -> "Start"
|
||||
GamepadButton.SELECT -> "Select"
|
||||
GamepadButton.MODE -> "Mode"
|
||||
GamepadButton.Z_BUTTON -> "Z"
|
||||
GamepadButton.C_BUTTON -> "C"
|
||||
}
|
||||
}
|
||||
|
||||
private fun retroIdDisplayName(id: Int): String {
|
||||
return when (id) {
|
||||
0 -> "B (Cross)"
|
||||
1 -> "Y (Square)"
|
||||
2 -> "Select"
|
||||
3 -> "Start"
|
||||
4 -> "D-Up"
|
||||
5 -> "D-Down"
|
||||
6 -> "D-Left"
|
||||
7 -> "D-Right"
|
||||
8 -> "A (Circle)"
|
||||
9 -> "X (Triangle)"
|
||||
10 -> "L1"
|
||||
11 -> "R1"
|
||||
12 -> "L2"
|
||||
13 -> "R2"
|
||||
14 -> "L3"
|
||||
15 -> "R3"
|
||||
else -> "Unknown ($id)"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ControllerItem(
|
||||
controller: GameController,
|
||||
isActive: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isActive) Icons.Default.BluetoothConnected else Icons.Default.Gamepad,
|
||||
contentDescription = null,
|
||||
tint = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = controller.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = if (isActive) "Active" else if (controller.isExternal) "Bluetooth" else "Built-in",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user