diff --git a/app/src/main/java/org/openmw/EngineActivity.kt b/app/src/main/java/org/openmw/EngineActivity.kt index 78af38c1..ab840255 100644 --- a/app/src/main/java/org/openmw/EngineActivity.kt +++ b/app/src/main/java/org/openmw/EngineActivity.kt @@ -1,8 +1,6 @@ package org.openmw - import android.annotation.SuppressLint -import android.content.Context import android.os.Bundle import android.os.Process import android.system.ErrnoException @@ -15,17 +13,13 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Star import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -38,17 +32,21 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import org.libsdl.app.SDLActivity +import org.openmw.ui.controls.ButtonState import org.openmw.ui.controls.CustomCursorView -import org.openmw.ui.overlay.GameControllerButtons -import org.openmw.ui.overlay.HiddenMenu +import org.openmw.ui.controls.DynamicButtonManager +import org.openmw.ui.controls.ResizableDraggableButton +import org.openmw.ui.controls.ResizableDraggableThumbstick +import org.openmw.ui.controls.UIStateManager +import org.openmw.ui.controls.loadButtonState +import org.openmw.ui.controls.saveButtonState import org.openmw.ui.overlay.OverlayUI -import org.openmw.ui.overlay.Thumbstick -import org.openmw.ui.overlay.sendKeyEvent -import org.openmw.utils.LogCat class EngineActivity : SDLActivity() { - private var customCursorView: CustomCursorView? = null + private lateinit var customCursorView: CustomCursorView private lateinit var sdlView: View + private val createdButtons = mutableStateListOf() + private var editMode = mutableStateOf(false) init { setEnvironmentVariables() @@ -73,10 +71,20 @@ class EngineActivity : SDLActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.engine_activity) sdlView = getContentView() - customCursorView = findViewById(R.id.customCursorView).apply { - sdlView = this@EngineActivity.sdlView // Set SDL view reference + customCursorView = findViewById(R.id.customCursorView) + + customCursorView.apply { + sdlView = this@EngineActivity.sdlView } + // Ensure the correct initial state of the cursor + setupInitialCursorState() + + // Load saved buttons + val allButtons = loadButtonState(this) + val thumbstick = allButtons.find { it.id == 99 } + createdButtons.addAll(allButtons.filter { it.id != 99 }) + // Add SDL view programmatically val sdlContainer = findViewById(R.id.sdl_container) sdlContainer.addView(sdlView) // Add SDL view to the sdl_container @@ -93,28 +101,50 @@ class EngineActivity : SDLActivity() { //logCat.enableLogcat() getPathToJni(filesDir.parent!!, Constants.USER_FILE_STORAGE) - // Setup Compose overlay for thumbstick - val composeView = findViewById(R.id.compose_overlay) - composeView.setContent { - Thumbstick( - onWClick = { onNativeKeyDown(KeyEvent.KEYCODE_W) }, - onAClick = { onNativeKeyDown(KeyEvent.KEYCODE_A) }, - onSClick = { onNativeKeyDown(KeyEvent.KEYCODE_S) }, - onDClick = { onNativeKeyDown(KeyEvent.KEYCODE_D) }, - onRelease = { - onNativeKeyUp(KeyEvent.KEYCODE_W) - onNativeKeyUp(KeyEvent.KEYCODE_A) - onNativeKeyUp(KeyEvent.KEYCODE_S) - onNativeKeyUp(KeyEvent.KEYCODE_D) - } - ) - } - // Setup Compose overlay for buttons val composeViewMenu = findViewById(R.id.compose_overlayMenu) composeViewMenu.setContent { - OverlayUI() - HiddenMenu() + OverlayUI(engineActivityContext = this) + + createdButtons.forEach { button -> + ResizableDraggableButton( + context = this@EngineActivity, + id = button.id, + keyCode = button.keyCode, + editMode = editMode.value, + onDelete = { deleteButton(button.id) } + ) + } + + thumbstick?.let { + ResizableDraggableThumbstick( + context = this, + id = 99, + keyCode = it.keyCode, + editMode = editMode.value, + onWClick = { onNativeKeyDown(KeyEvent.KEYCODE_W) }, + onAClick = { onNativeKeyDown(KeyEvent.KEYCODE_A) }, + onSClick = { onNativeKeyDown(KeyEvent.KEYCODE_S) }, + onDClick = { onNativeKeyDown(KeyEvent.KEYCODE_D) }, + onRelease = { + onNativeKeyUp(KeyEvent.KEYCODE_W) + onNativeKeyUp(KeyEvent.KEYCODE_A) + onNativeKeyUp(KeyEvent.KEYCODE_S) + onNativeKeyUp(KeyEvent.KEYCODE_D) + } + ) + } + + DynamicButtonManager( + context = this@EngineActivity, + onNewButtonAdded = { newButtonState -> + createdButtons.add(newButtonState) + }, + editMode = editMode, + createdButtons = createdButtons // Pass created buttons to DynamicButtonManager + ) + + //HiddenMenu() // RadialMenu.kt } // Setup Compose overlay for buttons @@ -143,52 +173,27 @@ class EngineActivity : SDLActivity() { fontWeight = FontWeight.Bold ) } - Button( - onClick = { - onNativeKeyDown(KeyEvent.KEYCODE_F) - sendKeyEvent(KeyEvent.KEYCODE_F) - onNativeKeyUp(KeyEvent.KEYCODE_F) - }, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent - ), - modifier = Modifier.size(50.dp).border(3.dp, Color.Black, shape = CircleShape) - ) { - Text( - text = "W", - color = Color.White, - fontSize = 15.sp, - fontWeight = FontWeight.Bold - ) - } - // Button to perform mouse click - Button( - onClick = { - customCursorView!!.performMouseClick() - }, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent - ), - modifier = Modifier.size(50.dp).border(3.dp, Color.Black, shape = CircleShape) - ) { - Text( - text = "C", - color = Color.White, - fontSize = 15.sp, - fontWeight = FontWeight.Bold - ) - } - // Game Controller Buttons at the Bottom - GameControllerButtons() } } } - private var isCustomCursorEnabled = false + private fun setupInitialCursorState() { + if (UIStateManager.isCustomCursorEnabled) { + customCursorView.visibility = View.VISIBLE + } else { + customCursorView.visibility = View.GONE + } + } + + private fun deleteButton(buttonId: Int) { + createdButtons.removeIf { it.id == buttonId } + saveButtonState(this, createdButtons + loadButtonState(this).filter { it.id == 99 }) // Ensure thumbstick is not removed + } + fun toggleCustomCursor() { runOnUiThread { - isCustomCursorEnabled = !isCustomCursorEnabled - customCursorView?.visibility = if (isCustomCursorEnabled) View.VISIBLE else View.GONE + UIStateManager.isCustomCursorEnabled = !UIStateManager.isCustomCursorEnabled + customCursorView.visibility = if (UIStateManager.isCustomCursorEnabled) View.VISIBLE else View.GONE } } diff --git a/app/src/main/java/org/openmw/MainActivity.kt b/app/src/main/java/org/openmw/MainActivity.kt index 74bd5edb..3024495e 100755 --- a/app/src/main/java/org/openmw/MainActivity.kt +++ b/app/src/main/java/org/openmw/MainActivity.kt @@ -82,6 +82,13 @@ class MainActivity : ComponentActivity() { controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } + // a little extra protection from people deleting the thumbstick by mistake + val file = File("${Constants.USER_CONFIG}/UI.cfg") + if (!file.exists()) { + file.createNewFile() + file.appendText("ButtonID_99(200.0;200.56776;281.6349;false;29)\n") + } + setContent { OpenMWTheme { Surface(modifier = Modifier.fillMaxSize()) { @@ -99,8 +106,8 @@ class MainActivity : ComponentActivity() { val file = File(Constants.SETTINGS_FILE) val lines = file.readLines().map { line -> when { - line.startsWith("resolution y =") -> "resolution y = $height" // $height - line.startsWith("resolution x =") -> "resolution x = $width" // $width + line.startsWith("resolution y =") -> "resolution y = $width" // mix these up to convert to landscape + line.startsWith("resolution x =") -> "resolution x = $height" else -> line } } diff --git a/app/src/main/java/org/openmw/fragments/ModsFragment.kt b/app/src/main/java/org/openmw/fragments/ModsFragment.kt index 68a6d0cf..665937e3 100644 --- a/app/src/main/java/org/openmw/fragments/ModsFragment.kt +++ b/app/src/main/java/org/openmw/fragments/ModsFragment.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.openmw.Constants import org.openmw.getAbsolutePathFromUri -import org.openmw.storeGameFilesUri import org.openmw.utils.ModValue import org.openmw.utils.writeModValuesToFile @@ -31,7 +30,6 @@ class ModsFragment { try { context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) CoroutineScope(Dispatchers.IO).launch { - storeGameFilesUri(context, uri) val ignoreList = listOf("Morrowind.bsa", "Tribunal.bsa", "Bloodmoon.bsa") val extensions = arrayOf("bsa", "esm", "esp", "omwaddon", "omwgame", "omwscripts") val selectedDirectory = DocumentFile.fromTreeUri(context, uri) diff --git a/app/src/main/java/org/openmw/fragments/SettingsFragment.kt b/app/src/main/java/org/openmw/fragments/SettingsFragment.kt index cba23423..ac96e7ae 100644 --- a/app/src/main/java/org/openmw/fragments/SettingsFragment.kt +++ b/app/src/main/java/org/openmw/fragments/SettingsFragment.kt @@ -58,7 +58,6 @@ class SettingsFragment : ComponentActivity() { val extensions = arrayOf("esm", "bsa") val modDirectory = dataFilesFolder ?: selectedDirectory val files = findFilesWithExtensions(modDirectory, extensions) - val modPath = getAbsolutePathFromUri(context, uri) if (iniFile != null && dataFilesFolder != null && dataFilesFolder.isDirectory) { // Update savedPath after setting it with the document tree @@ -93,7 +92,7 @@ class SettingsFragment : ComponentActivity() { // Define the regex pattern to match any user-data value val regexData = Regex("""^data\s*=\s*".*?"""") - val replacementStringData = """data="${modPath}Data Files"""" + val replacementStringData = """data="${savedPath}Data Files"""" val file = File(fileName) // Read and replace lines in the file diff --git a/app/src/main/java/org/openmw/ui/controls/DynamicButtons.kt b/app/src/main/java/org/openmw/ui/controls/DynamicButtons.kt new file mode 100644 index 00000000..c9b2baa5 --- /dev/null +++ b/app/src/main/java/org/openmw/ui/controls/DynamicButtons.kt @@ -0,0 +1,224 @@ +package org.openmw.ui.controls + +import android.content.Context +import android.view.KeyEvent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import org.libsdl.app.SDLActivity.onNativeKeyDown +import org.libsdl.app.SDLActivity.onNativeKeyUp +import org.openmw.ui.overlay.sendKeyEvent +import kotlin.math.roundToInt + +@Composable +fun ResizableDraggableButton( + context: Context, + id: Int, + keyCode: Int, + editMode: Boolean, + onDelete: () -> Unit // Add onDelete parameter +) { + var buttonState by remember { + mutableStateOf( + loadButtonState(context).find { it.id == id } + ?: ButtonState(id, 100f, 0f, 0f, false, keyCode) + ) + } + var buttonSize by remember { mutableStateOf(buttonState.size.dp) } + var offsetX by remember { mutableStateOf(buttonState.offsetX) } + var offsetY by remember { mutableStateOf(buttonState.offsetY) } + var isDragging by remember { mutableStateOf(false) } + var isPressed by remember { mutableStateOf(false) } + val context = LocalContext.current + val density = LocalDensity.current + val visible = UIStateManager.visible + val saveState = { + val updatedState = ButtonState(id, buttonSize.value, offsetX, offsetY, buttonState.isLocked, keyCode) + val allStates = loadButtonState(context).filter { it.id != id } + updatedState + saveButtonState(context, allStates) + } + + AnimatedVisibility( + visible = visible, + enter = slideInVertically( + initialOffsetY = { with(density) { -20.dp.roundToPx() } }, + animationSpec = tween(durationMillis = 1000) + ) + expandVertically( + expandFrom = Alignment.Bottom, + animationSpec = tween(durationMillis = 1000) + ) + fadeIn( + initialAlpha = 0.3f, + animationSpec = tween(durationMillis = 1000) + ), + exit = slideOutVertically( + targetOffsetY = { with(density) { -20.dp.roundToPx() } }, + animationSpec = tween(durationMillis = 1000) + ) + shrinkVertically( + animationSpec = tween(durationMillis = 1000) + ) + fadeOut( + animationSpec = tween(durationMillis = 1000) + ) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + ) { + Box( + modifier = Modifier + .size(buttonSize) + .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } + .background(Color.Transparent) + .then( + if (editMode) { + Modifier.pointerInput(Unit) { + detectDragGestures( + onDragStart = { isDragging = true }, + onDrag = { change, dragAmount -> + offsetX += dragAmount.x + offsetY += dragAmount.y + }, + onDragEnd = { + isDragging = false + saveState() + } + ) + } + } else Modifier + ) + .border(2.dp, if (isDragging) Color.Red else Color.Black, shape = CircleShape) + ) { + // Main button + Box( + modifier = Modifier + .fillMaxSize() + .background( + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { + if (isPressed) Color.Green else Color(alpha = 0.6f, red = 0f, green = 0f, blue = 0f) + } else { + Color(alpha = 0.6f, red = 0f, green = 0f, blue = 0f) + }, shape = CircleShape + ) + .clickable { + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { + isPressed = !isPressed + if (isPressed) { + onNativeKeyDown(keyCode) + sendKeyEvent(keyCode) + } else { + onNativeKeyUp(keyCode) + } + } else { + onNativeKeyDown(keyCode) + sendKeyEvent(keyCode) + onNativeKeyUp(keyCode) + } + }, + contentAlignment = Alignment.Center + ) { + if (editMode) { + Text(text = "ID: $id, Key: ${keyCodeToChar(keyCode)}", color = Color.White) + } else { + Text(text = keyCodeToChar(keyCode), color = Color.White) + } + } + + + if (editMode) { + // + button + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(30.dp) + .background(Color.Black, shape = CircleShape) + .clickable { + buttonSize += 20.dp + saveState() + } + .border(2.dp, Color.White, shape = CircleShape), + contentAlignment = Alignment.Center + ) { + Text(text = "+", color = Color.White, fontWeight = FontWeight.Bold) + } + + // - button + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .size(30.dp) + .background(Color.Black, shape = CircleShape) + .clickable { + buttonSize -= 20.dp + if (buttonSize < 50.dp) buttonSize = 50.dp // Minimum size + saveState() + } + .border(2.dp, Color.White, shape = CircleShape), + contentAlignment = Alignment.Center + ) { + Text(text = "-", color = Color.White, fontWeight = FontWeight.Bold) + } + + // Delete button + Box( + modifier = Modifier + .align(Alignment.BottomStart) + .size(30.dp) + .background(Color.Red, shape = CircleShape) + .clickable { + onDelete() + } + .border(2.dp, Color.White, shape = CircleShape), + contentAlignment = Alignment.Center + ) { + Text(text = "X", color = Color.White, fontWeight = FontWeight.Bold) + } + } + } + } + } +} + + +fun keyCodeToChar(keyCode: Int): String { + return when (keyCode) { + in KeyEvent.KEYCODE_F1..KeyEvent.KEYCODE_F12 -> "F${keyCode - KeyEvent.KEYCODE_F1 + 1}" + KeyEvent.KEYCODE_SHIFT_LEFT -> "Shift-L" + KeyEvent.KEYCODE_SHIFT_RIGHT -> "Shift-R" + KeyEvent.KEYCODE_CTRL_LEFT -> "Ctrl-L" + KeyEvent.KEYCODE_CTRL_RIGHT -> "Ctrl-R" + KeyEvent.KEYCODE_SPACE -> "Space" + KeyEvent.KEYCODE_ESCAPE -> "Escape" + KeyEvent.KEYCODE_ENTER -> "Enter" + KeyEvent.KEYCODE_GRAVE -> "Grave" + else -> (keyCode - KeyEvent.KEYCODE_A + 'A'.code).toChar().toString() + } +} diff --git a/app/src/main/java/org/openmw/ui/controls/DynamicLeftThumbstick.kt b/app/src/main/java/org/openmw/ui/controls/DynamicLeftThumbstick.kt new file mode 100644 index 00000000..65c68621 --- /dev/null +++ b/app/src/main/java/org/openmw/ui/controls/DynamicLeftThumbstick.kt @@ -0,0 +1,241 @@ +package org.openmw.ui.controls + +import android.content.Context +import android.view.KeyEvent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import org.libsdl.app.SDLActivity.onNativeKeyUp +import kotlin.math.abs +import kotlin.math.hypot +import kotlin.math.roundToInt + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun ResizableDraggableThumbstick( + context: Context, + id: Int, + keyCode: Int, + editMode: Boolean, + onWClick: () -> Unit, + onAClick: () -> Unit, + onSClick: () -> Unit, + onDClick: () -> Unit, + onRelease: () -> Unit +) { + var thumbstickState by remember { + mutableStateOf(loadButtonState(context).find { it.id == id } ?: ButtonState( + id, + 200f, + 0f, + 0f, + false, + keyCode + )) + } + var buttonSize by remember { mutableStateOf(thumbstickState.size.dp) } + var offsetX by remember { mutableStateOf(thumbstickState.offsetX) } + var offsetY by remember { mutableStateOf(thumbstickState.offsetY) } + var isDragging by remember { mutableStateOf(false) } + val visible = UIStateManager.visible + val density = LocalDensity.current + val radiusPx = with(density) { (buttonSize / 2).toPx() } + val deadZone = 0.2f * radiusPx + var touchState by remember { mutableStateOf(Offset(0f, 0f)) } + val saveState = { + val updatedState = ButtonState(id, buttonSize.value, offsetX, offsetY, false, keyCode) + val allStates = loadButtonState(context).filter { it.id != id } + updatedState + saveButtonState(context, allStates) + } + AnimatedVisibility( + visible = visible, + enter = slideInVertically( + initialOffsetY = { with(density) { -20.dp.roundToPx() } }, + animationSpec = tween(durationMillis = 1000) + ) + expandVertically( + expandFrom = Alignment.Bottom, + animationSpec = tween(durationMillis = 1000) + ) + fadeIn( + initialAlpha = 0.3f, + animationSpec = tween(durationMillis = 1000) + ), + exit = slideOutVertically( + targetOffsetY = { with(density) { -20.dp.roundToPx() } }, + animationSpec = tween(durationMillis = 1000) + ) + shrinkVertically( + animationSpec = tween(durationMillis = 1000) + ) + fadeOut( + animationSpec = tween(durationMillis = 1000) + ) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(buttonSize) + .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } + .background(Color.Transparent) + .then( + if (editMode) { + Modifier.pointerInput(Unit) { + detectDragGestures( + onDragStart = { isDragging = true }, + onDrag = { change, dragAmount -> + offsetX += dragAmount.x + offsetY += dragAmount.y + }, + onDragEnd = { + isDragging = false + saveState() + } + ) + } + } else Modifier + ) + .border(2.dp, if (isDragging) Color.Red else Color.Black, shape = CircleShape) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + .border(2.dp, Color.Black, CircleShape) + .pointerInput(Unit) { + detectDragGestures( + onDragStart = { offset -> + val location = Offset(offset.x, offset.y) + val isInBounds = hypot( + location.x - radiusPx, + location.y - radiusPx + ) <= radiusPx + if (isInBounds) { + touchState = location - Offset(radiusPx, radiusPx) + } + }, + onDrag = { change, dragAmount -> + val newOffset = touchState + Offset(dragAmount.x, dragAmount.y) + val isInBounds = hypot(newOffset.x, newOffset.y) <= radiusPx + if (isInBounds) { + touchState = newOffset + val xRatio = touchState.x / radiusPx + val yRatio = touchState.y / radiusPx + onNativeKeyUp(KeyEvent.KEYCODE_W) + onNativeKeyUp(KeyEvent.KEYCODE_A) + onNativeKeyUp(KeyEvent.KEYCODE_S) + onNativeKeyUp(KeyEvent.KEYCODE_D) + when { + abs(yRatio) > abs(xRatio) -> { + if (touchState.y < -deadZone) onWClick() + if (touchState.y > deadZone) onSClick() + } + + abs(xRatio) > abs(yRatio) -> { + if (touchState.x < -deadZone) onAClick() + if (touchState.x > deadZone) onDClick() + } + } + } + }, + onDragEnd = { + touchState = Offset.Zero + onNativeKeyUp(KeyEvent.KEYCODE_W) + onNativeKeyUp(KeyEvent.KEYCODE_A) + onNativeKeyUp(KeyEvent.KEYCODE_S) + onNativeKeyUp(KeyEvent.KEYCODE_D) + onRelease() + }, + onDragCancel = { + touchState = Offset.Zero + onNativeKeyUp(KeyEvent.KEYCODE_W) + onNativeKeyUp(KeyEvent.KEYCODE_A) + onNativeKeyUp(KeyEvent.KEYCODE_S) + onNativeKeyUp(KeyEvent.KEYCODE_D) + onRelease() + } + ) + } + ) { + Box( + modifier = Modifier + .size(25.dp) + .offset( + x = (touchState.x / density.density).dp, + y = (touchState.y / density.density).dp + ) + .background( + Color(alpha = 0.6f, red = 0f, green = 0f, blue = 0f), + shape = CircleShape + ) + ) + } + if (editMode) { + // + button + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(30.dp) + .background(Color.Black, shape = CircleShape) + .clickable { + buttonSize += 20.dp + saveState() + } + .border(2.dp, Color.White, shape = CircleShape), + contentAlignment = Alignment.Center + ) { + Text(text = "+", color = Color.White, fontWeight = FontWeight.Bold) + } + + // - button + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .size(30.dp) + .background(Color.Black, shape = CircleShape) + .clickable { + buttonSize -= 20.dp + if (buttonSize < 50.dp) buttonSize = 50.dp + saveState() + } + .border(2.dp, Color.White, shape = CircleShape), + contentAlignment = Alignment.Center + ) { + Text(text = "-", color = Color.White, fontWeight = FontWeight.Bold) + } + } + } + } + } +} diff --git a/app/src/main/java/org/openmw/ui/controls/StateManager.kt b/app/src/main/java/org/openmw/ui/controls/StateManager.kt new file mode 100644 index 00000000..1bdf5266 --- /dev/null +++ b/app/src/main/java/org/openmw/ui/controls/StateManager.kt @@ -0,0 +1,313 @@ +package org.openmw.ui.controls + +import android.content.Context +import android.view.KeyEvent +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Build +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +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.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import org.openmw.Constants +import java.io.File + +data class ButtonState( + val id: Int, + val size: Float, + val offsetX: Float, + val offsetY: Float, + val isLocked: Boolean, + val keyCode: Int +) + +object UIStateManager { + var isUIHidden by mutableStateOf(false) + var visible by mutableStateOf(true) + var isVibrationEnabled by mutableStateOf(true) + var isRunEnabled by mutableStateOf(false) + var isCustomCursorEnabled by mutableStateOf(false) +} + +fun saveButtonState(context: Context, state: List) { + val file = File("${Constants.USER_CONFIG}/UI.cfg") + if (!file.exists()) { + file.createNewFile() + } + + val thumbstick = loadButtonState(context).find { it.id == 99 } + val existingStates = state.filter { it.id != 99 }.toMutableList() + + thumbstick?.let { existingStates.add(it) } + + file.printWriter().use { out -> + existingStates.forEach { button -> + out.println("ButtonID_${button.id}(${button.size};${button.offsetX};${button.offsetY};${button.isLocked};${button.keyCode})") + } + } +} + + +fun loadButtonState(context: Context): List { + val file = File("${Constants.USER_CONFIG}/UI.cfg") + return if (file.exists()) { + file.readLines().mapNotNull { line -> + val regex = """ButtonID_(\d+)\(([\d.]+);([\d.]+);([\d.]+);(true|false);(\d+)\)""".toRegex() + val matchResult = regex.find(line) + matchResult?.let { + ButtonState( + id = it.groupValues[1].toInt(), + size = it.groupValues[2].toFloat(), + offsetX = it.groupValues[3].toFloat(), + offsetY = it.groupValues[4].toFloat(), + isLocked = it.groupValues[5].toBoolean(), + keyCode = it.groupValues[6].toInt() + ) + } + } + } else { + emptyList() + } +} + +@Composable +fun KeySelectionMenu(onKeySelected: (Int) -> Unit, usedKeys: List) { + val letterKeys = ('A'..'Z').toList().filter { key -> + val keyCode = KeyEvent.KEYCODE_A + key.minus('A') + keyCode !in usedKeys + } + val fKeys = (KeyEvent.KEYCODE_F1..KeyEvent.KEYCODE_F12).filter { keyCode -> + keyCode !in usedKeys + } + val additionalKeys = listOf( + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_SHIFT_RIGHT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_CTRL_RIGHT, + KeyEvent.KEYCODE_SPACE, + KeyEvent.KEYCODE_ESCAPE, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_GRAVE + ).filter { keyCode -> keyCode !in usedKeys } + + var showDialog by remember { mutableStateOf(false) } + IconButton(onClick = { showDialog = true }) { + Icon(Icons.Default.Add, contentDescription = "Add Button") + } + + if (showDialog) { + Dialog(onDismissRequest = { showDialog = false }) { + Surface( + shape = MaterialTheme.shapes.medium, + color = Color.Black.copy(alpha = 0.7f) + ) { + Column( + modifier = Modifier + .padding(16.dp) + .widthIn(min = 300.dp, max = 400.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + text = "Select a Key", + style = MaterialTheme.typography.titleMedium, + color = Color.White, + modifier = Modifier.padding(bottom = 16.dp) + ) + + letterKeys.chunked(6).forEach { rowKeys -> + Row( + modifier = Modifier.fillMaxWidth().padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + rowKeys.forEach { key -> + val keyCode = KeyEvent.KEYCODE_A + key.minus('A') + Box( + modifier = Modifier + .size(48.dp) + .background(Color.LightGray, shape = CircleShape) + .clickable { + onKeySelected(keyCode) + showDialog = false + }, + contentAlignment = Alignment.Center + ) { + Text( + text = key.toString(), + style = MaterialTheme.typography.titleMedium, + color = Color.Black + ) + } + } + } + } + + Divider(color = Color.White, thickness = 1.dp, modifier = Modifier.padding(vertical = 16.dp)) + Text( + text = "Select a Function Key", + style = MaterialTheme.typography.titleMedium, + color = Color.White, + modifier = Modifier.padding(bottom = 16.dp) + ) + fKeys.chunked(6).forEach { rowKeys -> + Row( + modifier = Modifier.fillMaxWidth().padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + rowKeys.forEach { keyCode -> + val key = "F${keyCode - KeyEvent.KEYCODE_F1 + 1}" + Box( + modifier = Modifier + .size(48.dp) + .background(Color.LightGray, shape = CircleShape) + .clickable { + onKeySelected(keyCode) + showDialog = false + }, + contentAlignment = Alignment.Center + ) { + Text( + text = key, + style = MaterialTheme.typography.titleMedium, + color = Color.Black + ) + } + } + } + } + + Divider(color = Color.White, thickness = 1.dp, modifier = Modifier.padding(vertical = 16.dp)) + Text( + text = "Select a Unique Key, The shift keys toggle.", + style = MaterialTheme.typography.titleMedium, + color = Color.White, + modifier = Modifier.padding(bottom = 16.dp) + ) + additionalKeys.chunked(4).forEach { rowKeys -> + Row( + modifier = Modifier.fillMaxWidth().padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + rowKeys.forEach { keyCode -> + val key = when (keyCode) { + KeyEvent.KEYCODE_SHIFT_LEFT -> "Shift-L" + KeyEvent.KEYCODE_SHIFT_RIGHT -> "Shift-R" + KeyEvent.KEYCODE_CTRL_LEFT -> "Ctrl-L" + KeyEvent.KEYCODE_CTRL_RIGHT -> "Ctrl-R" + KeyEvent.KEYCODE_SPACE -> "Space" + KeyEvent.KEYCODE_ESCAPE -> "Escape" + KeyEvent.KEYCODE_ENTER -> "Enter" + KeyEvent.KEYCODE_GRAVE -> "`" + else -> keyCode.toString() + } + Box( + modifier = Modifier + .size(48.dp) + .background(Color.LightGray, shape = CircleShape) + .clickable { + onKeySelected(keyCode) + showDialog = false + }, + contentAlignment = Alignment.Center + ) { + Text( + text = key, + style = MaterialTheme.typography.titleMedium, + color = Color.Black + ) + } + } + } + } + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { showDialog = false }, + modifier = Modifier.align(Alignment.End) + ) { + Text("Cancel") + } + } + } + } + } +} + +@Composable +fun DynamicButtonManager( + context: Context, + onNewButtonAdded: (ButtonState) -> Unit, + editMode: MutableState, + createdButtons: List +) { + var showDialog by remember { mutableStateOf(false) } + + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = { + showDialog = !showDialog + editMode.value = showDialog + }) { + Icon(Icons.Default.Build, contentDescription = "Button Menu") + } + } + + if (showDialog) { + KeySelectionMenu( + onKeySelected = { keyCode -> + val allButtons = loadButtonState(context) + val thumbstick = allButtons.find { it.id == 99 } + val otherButtons = allButtons.filter { it.id != 99 } + val maxExistingId = otherButtons.maxOfOrNull { it.id } ?: 0 + val newId = maxExistingId + 1 + val newButtonState = ButtonState( + id = newId, + size = 100f, + offsetX = 0f, + offsetY = 0f, + isLocked = false, + keyCode = keyCode + ) + val updatedButtons = otherButtons + newButtonState + thumbstick?.let { updatedButtons + it } + saveButtonState(context, updatedButtons) + onNewButtonAdded(newButtonState) + showDialog = false + }, + usedKeys = createdButtons.map { it.keyCode } + ) + } + } +} + + diff --git a/app/src/main/java/org/openmw/ui/controls/mouse.kt b/app/src/main/java/org/openmw/ui/controls/mouse.kt index 5e8286d3..8a7484e8 100644 --- a/app/src/main/java/org/openmw/ui/controls/mouse.kt +++ b/app/src/main/java/org/openmw/ui/controls/mouse.kt @@ -3,31 +3,57 @@ package org.openmw.ui.controls import android.annotation.SuppressLint import android.content.Context import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint import android.os.SystemClock import android.util.AttributeSet import android.util.Log import android.view.MotionEvent import android.view.View +import androidx.core.content.ContextCompat +import org.openmw.Constants +import org.openmw.R +import java.io.File class CustomCursorView(context: Context, attrs: AttributeSet?) : View(context, attrs) { - private val paint = Paint().apply { color = Color.WHITE; style = Paint.Style.FILL } private var cursorX = 0f private var cursorY = 0f - private var offset = -150f + private var offsetX = 0f + private var offsetY = 0f var sdlView: View? = null private var isCursorEnabled = true + private val cursorIcon = ContextCompat.getDrawable(context, R.drawable.pointer_icon)!! + + private fun readSettingsFile(): Triple { + val settingsFile = File(Constants.SETTINGS_FILE) + var resolutionX = 0 + var resolutionY = 0 + var scalingFactor = 1.0f + + settingsFile.forEachLine { line -> + when { + line.startsWith("resolution x =") -> resolutionX = line.split("=").last().trim().toInt() + line.startsWith("resolution y =") -> resolutionY = line.split("=").last().trim().toInt() + line.startsWith("scaling factor =") -> scalingFactor = line.split("=").last().trim().toFloat() + } + } + + return Triple(resolutionX, resolutionY, scalingFactor) + } + + private val settings = readSettingsFile() + private val resolutionX = settings.first + private val resolutionY = settings.second + fun setCursorPosition(x: Float, y: Float) { - cursorX = x + offset - cursorY = y + offset + cursorX = x.coerceIn(0f, width.toFloat() - cursorIcon.intrinsicWidth) + cursorY = y.coerceIn(0f, height.toFloat() - cursorIcon.intrinsicHeight) + Log.d("CustomCursorView", "Cursor Position: X=$cursorX, Y=$cursorY") invalidate() } fun performMouseClick() { - val adjustedX = cursorX - val adjustedY = cursorY + val adjustedX = cursorX * (resolutionX.toFloat() / width.toFloat()) + val adjustedY = cursorY * (resolutionY.toFloat() / height.toFloat()) val eventDown = MotionEvent.obtain( SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), @@ -54,15 +80,30 @@ class CustomCursorView(context: Context, attrs: AttributeSet?) : View(context, a override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (isCursorEnabled) { - canvas.drawCircle(cursorX, cursorY, 20f, paint) + val iconSize = 72 + cursorIcon.setBounds(cursorX.toInt(), cursorY.toInt(), cursorX.toInt() + iconSize, cursorY.toInt() + iconSize) + cursorIcon.draw(canvas) } } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { - MotionEvent.ACTION_MOVE, MotionEvent.ACTION_DOWN -> { - setCursorPosition(event.x, event.y) + MotionEvent.ACTION_DOWN -> { + offsetX = event.x - cursorX + offsetY = event.y - cursorY + Log.d("CustomCursorView", "Touch Down at X: ${event.x}, Y: ${event.y}") + return true + } + MotionEvent.ACTION_MOVE -> { + setCursorPosition(event.x - offsetX, event.y - offsetY) + Log.d("CustomCursorView", "Touch Move at X: ${event.x}, Y: ${event.y}") + return true + } + MotionEvent.ACTION_UP -> { + offsetX = event.x - cursorX + offsetY = event.y - cursorY + Log.d("CustomCursorView", "Touch Released at X: ${event.x}, Y: ${event.y}") return true } } diff --git a/app/src/main/java/org/openmw/ui/overlay/Overlay.kt b/app/src/main/java/org/openmw/ui/overlay/Overlay.kt index 8bcfc703..7f4c754d 100644 --- a/app/src/main/java/org/openmw/ui/overlay/Overlay.kt +++ b/app/src/main/java/org/openmw/ui/overlay/Overlay.kt @@ -1,55 +1,44 @@ package org.openmw.ui.overlay import android.content.Context -import android.os.Build -import android.os.VibrationEffect -import android.os.Vibrator import android.view.KeyEvent import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.tween -import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.rounded.Settings import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.* -import org.libsdl.app.SDLActivity import org.libsdl.app.SDLActivity.onNativeKeyDown import org.libsdl.app.SDLActivity.onNativeKeyUp +import org.openmw.ui.controls.UIStateManager import org.openmw.utils.* -import kotlin.math.abs -import kotlin.math.hypot + +fun sendKeyEvent(keyCode: Int) { + onNativeKeyDown(keyCode) + onNativeKeyUp(keyCode) +} data class MemoryInfo( val totalMemory: String, @@ -57,16 +46,10 @@ data class MemoryInfo( val usedMemory: String ) -object UIStateManager { - var isUIHidden by mutableStateOf(false) - var visible by mutableStateOf(true) - var isVibrationEnabled by mutableStateOf(true) - var isRunEnabled by mutableStateOf(false) -} - @Composable -fun OverlayUI() { +fun OverlayUI(engineActivityContext: Context) { val context = LocalContext.current + var expanded by remember { mutableStateOf(false) } var memoryInfoText by remember { mutableStateOf("") } var batteryStatus by remember { mutableStateOf("") } var logMessages by remember { mutableStateOf("") } @@ -110,10 +93,7 @@ fun OverlayUI() { delay(1000) } } - Box(modifier = Modifier.fillMaxSize()) { - var expanded by remember { mutableStateOf(false) } - Surface( color = Color.Transparent, onClick = { expanded = !expanded } @@ -193,7 +173,7 @@ fun OverlayUI() { Text(text = "Run / Walk", color = Color.White, fontSize = 10.sp) Switch(checked = isRunEnabled, onCheckedChange = { UIStateManager.isRunEnabled = it - if (it) SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_SHIFT_LEFT) + if (it) onNativeKeyDown(KeyEvent.KEYCODE_SHIFT_LEFT) else onNativeKeyUp(KeyEvent.KEYCODE_SHIFT_LEFT) }) // F10 Toggle Switch @@ -201,10 +181,10 @@ fun OverlayUI() { Switch(checked = isF10Enabled, onCheckedChange = { isF10Enabled = it if (it) { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_F10) + onNativeKeyDown(KeyEvent.KEYCODE_F10) onNativeKeyUp(KeyEvent.KEYCODE_F10) } else { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_F10) + onNativeKeyDown(KeyEvent.KEYCODE_F10) onNativeKeyUp(KeyEvent.KEYCODE_F10) } }) @@ -213,10 +193,10 @@ fun OverlayUI() { Switch(checked = isBacktickEnabled, onCheckedChange = { isBacktickEnabled = it if (it) { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_GRAVE) + onNativeKeyDown(KeyEvent.KEYCODE_GRAVE) onNativeKeyUp(KeyEvent.KEYCODE_GRAVE) } else { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_GRAVE) + onNativeKeyDown(KeyEvent.KEYCODE_GRAVE) onNativeKeyUp(KeyEvent.KEYCODE_GRAVE) } }) @@ -230,14 +210,14 @@ fun OverlayUI() { item { // Button for J (Journal) IconButton(onClick = { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_J) + onNativeKeyDown(KeyEvent.KEYCODE_J) onNativeKeyUp(KeyEvent.KEYCODE_J) }) { Text(text = "Journal", color = Color.White, fontSize = 10.sp) } // Button for F5 (Quicksave) IconButton(onClick = { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_F5) + onNativeKeyDown(KeyEvent.KEYCODE_F5) onNativeKeyUp(KeyEvent.KEYCODE_F5) }) { Text(text = "Quicksave", color = Color.White, fontSize = 10.sp) @@ -245,7 +225,7 @@ fun OverlayUI() { // Button for F6 (Quickload) IconButton(onClick = { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_F6) + onNativeKeyDown(KeyEvent.KEYCODE_F6) onNativeKeyUp(KeyEvent.KEYCODE_F6) }) { Text(text = "Quickload", color = Color.White, fontSize = 10.sp) @@ -253,7 +233,7 @@ fun OverlayUI() { // Button for F12 (Screenshot) IconButton(onClick = { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_F12) + onNativeKeyDown(KeyEvent.KEYCODE_F12) onNativeKeyUp(KeyEvent.KEYCODE_F12) }) { Text(text = "Screenshot", color = Color.White, fontSize = 10.sp) @@ -263,10 +243,10 @@ fun OverlayUI() { IconButton(onClick = { isF2Enabled = !isF2Enabled if (isF2Enabled) { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_F2) + onNativeKeyDown(KeyEvent.KEYCODE_F2) onNativeKeyUp(KeyEvent.KEYCODE_F2) } else { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_F2) + onNativeKeyDown(KeyEvent.KEYCODE_F2) onNativeKeyUp(KeyEvent.KEYCODE_F2) } }) { @@ -277,10 +257,10 @@ fun OverlayUI() { IconButton(onClick = { isF3Enabled = !isF3Enabled if (isF3Enabled) { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_F3) + onNativeKeyDown(KeyEvent.KEYCODE_F3) onNativeKeyUp(KeyEvent.KEYCODE_F3) } else { - SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_F3) + onNativeKeyDown(KeyEvent.KEYCODE_F3) onNativeKeyUp(KeyEvent.KEYCODE_F3) } }) { @@ -291,6 +271,32 @@ fun OverlayUI() { } } else { Icon(Icons.Rounded.Settings, contentDescription = "Settings") + Button( + onClick = { + onNativeKeyDown(KeyEvent.KEYCODE_J) + onNativeKeyUp(KeyEvent.KEYCODE_J) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent + ), + modifier = Modifier + .padding(start = 60.dp) + ) { + Icon(Icons.Default.Person, contentDescription = "Sneak", tint = Color.Black) + } + Button( + onClick = { + onNativeKeyDown(KeyEvent.KEYCODE_T) + onNativeKeyUp(KeyEvent.KEYCODE_T) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent + ), + modifier = Modifier + .padding(start = 20.dp) + ) { + Icon(Icons.Default.Refresh, contentDescription = "Rest", tint = Color.Black) + } } } } @@ -343,272 +349,4 @@ fun OverlayUI() { } } } - -} - -@Suppress("DEPRECATION") -fun vibrate(context: Context) { - val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator - if (vibrator.hasVibrator()) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - vibrator.vibrate(VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE)) - } - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun Thumbstick( - onWClick: () -> Unit, - onAClick: () -> Unit, - onSClick: () -> Unit, - onDClick: () -> Unit, - onRelease: () -> Unit -) { - val density = LocalDensity.current - val radiusPx = with(density) { 75.dp.toPx() } - val deadZone = 0.2f * radiusPx - var touchState by remember { mutableStateOf(Offset(0f, 0f)) } - - Box(modifier = Modifier.fillMaxSize()) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(200.dp) - .border(2.dp, Color.Black, shape = CircleShape) - .pointerInput(Unit) { - detectDragGestures( - onDragStart = { offset -> - val location = Offset(offset.x, offset.y) - val isInBounds = hypot(location.x - radiusPx, location.y - radiusPx) <= radiusPx - if (isInBounds) { - touchState = location - Offset(radiusPx, radiusPx) - } - }, - onDrag = { change, dragAmount -> - val newOffset = touchState + Offset(dragAmount.x, dragAmount.y) - val isInBounds = hypot(newOffset.x, newOffset.y) <= radiusPx - if (isInBounds) { - touchState = newOffset - val xRatio = touchState.x / radiusPx - val yRatio = touchState.y / radiusPx - - onNativeKeyUp(KeyEvent.KEYCODE_W) - onNativeKeyUp(KeyEvent.KEYCODE_A) - onNativeKeyUp(KeyEvent.KEYCODE_S) - onNativeKeyUp(KeyEvent.KEYCODE_D) - - when { - abs(yRatio) > abs(xRatio) -> { - if (touchState.y < -deadZone) onWClick() - if (touchState.y > deadZone) onSClick() - } - abs(xRatio) > abs(yRatio) -> { - if (touchState.x < -deadZone) onAClick() - if (touchState.x > deadZone) onDClick() - } - } - } - }, - onDragEnd = { - touchState = Offset.Zero - onNativeKeyUp(KeyEvent.KEYCODE_W) - onNativeKeyUp(KeyEvent.KEYCODE_A) - onNativeKeyUp(KeyEvent.KEYCODE_S) - onNativeKeyUp(KeyEvent.KEYCODE_D) - onRelease() - }, - onDragCancel = { - touchState = Offset.Zero - onNativeKeyUp(KeyEvent.KEYCODE_W) - onNativeKeyUp(KeyEvent.KEYCODE_A) - onNativeKeyUp(KeyEvent.KEYCODE_S) - onNativeKeyUp(KeyEvent.KEYCODE_D) - onRelease() - } - ) - } - ) { - Box( - modifier = Modifier - .size(25.dp) - .offset( - x = (touchState.x / density.density).dp, - y = (touchState.y / density.density).dp - ) - .background( - Color(alpha = 0.6f, red = 0f, green = 0f, blue = 0f), - shape = CircleShape - ) - ) - } - } -} - - - -@Composable -fun GameControllerButtons( - -) { - val context = LocalContext.current - val density = LocalDensity.current - val visible = UIStateManager.visible - var isVibrationEnabled = UIStateManager.isVibrationEnabled - - AnimatedVisibility( - visible = visible, - enter = slideInVertically( - initialOffsetY = { with(density) { -20.dp.roundToPx() } }, - animationSpec = tween(durationMillis = 1000) // Adjust the duration as needed - ) + expandVertically( - expandFrom = Alignment.Bottom, - animationSpec = tween(durationMillis = 1000) - ) + fadeIn( - initialAlpha = 0.3f, - animationSpec = tween(durationMillis = 1000) - ), - exit = slideOutVertically( - targetOffsetY = { with(density) { -20.dp.roundToPx() } }, - animationSpec = tween(durationMillis = 1000) - ) + shrinkVertically( - animationSpec = tween(durationMillis = 1000) - ) + fadeOut( - animationSpec = tween(durationMillis = 1000) - ) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(1.dp), // Adjusted spacing - modifier = Modifier.align(Alignment.BottomEnd) - ) { - Button( - onClick = { - onNativeKeyDown(KeyEvent.KEYCODE_E) - sendKeyEvent(KeyEvent.KEYCODE_E) - onNativeKeyUp(KeyEvent.KEYCODE_E) - if (isVibrationEnabled) { - vibrate(context) - } - }, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent - ), - modifier = Modifier - .size(50.dp) - .border(1.dp, Color.Black, shape = CircleShape), - shape = CircleShape - ) { - Text( - text = "Y", - color = Color.White, - fontSize = 15.sp, - fontWeight = FontWeight.Bold - ) - } - Row( - horizontalArrangement = Arrangement.SpaceEvenly, // Adjusted spacing - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.width(200.dp) // Sets the fixed width for the row - ) { - Button( - onClick = { - onNativeKeyDown(KeyEvent.KEYCODE_SPACE) - sendKeyEvent(KeyEvent.KEYCODE_SPACE) - onNativeKeyUp(KeyEvent.KEYCODE_SPACE) - if (isVibrationEnabled) { - vibrate(context) - } - }, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent - ), - modifier = Modifier - .size(50.dp) - .border(1.dp, Color.Black, shape = CircleShape), - shape = CircleShape - ) { - Text( - text = "X", - color = Color.White, - fontSize = 15.sp, - fontWeight = FontWeight.Bold - ) - } - Button( - onClick = { - onNativeKeyDown(KeyEvent.KEYCODE_NUMPAD_DOT) - sendKeyEvent(KeyEvent.KEYCODE_NUMPAD_DOT) - onNativeKeyUp(KeyEvent.KEYCODE_NUMPAD_DOT) - }, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent - ), - modifier = Modifier - .size(50.dp) - .border(3.dp, Color.Black, shape = CircleShape) - ) { - Icon( - imageVector = Icons.Filled.Star, - contentDescription = null, - tint = Color.White - ) - } - Button( - onClick = { - onNativeKeyDown(KeyEvent.KEYCODE_ESCAPE) - sendKeyEvent(KeyEvent.KEYCODE_ESCAPE) - onNativeKeyUp(KeyEvent.KEYCODE_ESCAPE) - }, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent - ), - modifier = Modifier - .size(50.dp) - .border(1.dp, Color.Black, shape = CircleShape), - shape = CircleShape - ) { - Text( - text = "B", - color = Color.White, - fontSize = 15.sp, - fontWeight = FontWeight.Bold - ) - } - } - Button( - onClick = { - onNativeKeyDown(KeyEvent.KEYCODE_ENTER) - sendKeyEvent(KeyEvent.KEYCODE_ENTER) - onNativeKeyUp(KeyEvent.KEYCODE_ENTER) - }, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent - ), - modifier = Modifier - .size(50.dp) - .border(1.dp, Color.Black, shape = CircleShape), - shape = CircleShape - ) { - Text( - text = "A", - color = Color.White, - fontSize = 15.sp, - fontWeight = FontWeight.Bold - ) - } - } - } - } -} - -fun sendKeyEvent(keyCode: Int) { - onNativeKeyDown(keyCode) - onNativeKeyUp(keyCode) } diff --git a/app/src/main/java/org/openmw/utils/Base64Helper.kt b/app/src/main/java/org/openmw/utils/Base64Helper.kt deleted file mode 100755 index 594e35d1..00000000 --- a/app/src/main/java/org/openmw/utils/Base64Helper.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.openmw.utils - -import android.util.Base64 -import java.io.File -import java.io.FileInputStream -import java.io.IOException - - -object Base64Encoder { - fun encodeFileToBase64(filePath: String): String { - val file = File(filePath) - val fileBytes = ByteArray(file.length().toInt()) - try { - FileInputStream(file).use { fis -> - fis.read(fileBytes) - } - } catch (e: IOException) { - e.printStackTrace() - } - return Base64.encodeToString(fileBytes, Base64.DEFAULT) - } -} diff --git a/app/src/main/java/org/openmw/utils/IniAssistant.kt b/app/src/main/java/org/openmw/utils/IniAssistant.kt index 99026f77..90ed6142 100755 --- a/app/src/main/java/org/openmw/utils/IniAssistant.kt +++ b/app/src/main/java/org/openmw/utils/IniAssistant.kt @@ -617,4 +617,3 @@ fun ExpandableBox(expanded: MutableState) { } } } - diff --git a/app/src/main/java/org/openmw/utils/UITools.kt b/app/src/main/java/org/openmw/utils/UITools.kt index dec00f1f..150dd230 100644 --- a/app/src/main/java/org/openmw/utils/UITools.kt +++ b/app/src/main/java/org/openmw/utils/UITools.kt @@ -1,5 +1,6 @@ package org.openmw.utils +import android.annotation.SuppressLint import android.app.ActivityManager import android.content.Context import android.content.Intent @@ -10,6 +11,8 @@ import android.os.StatFs import org.openmw.ui.overlay.MemoryInfo import java.io.BufferedReader import java.io.InputStreamReader +import kotlin.math.ln +import kotlin.math.pow fun getAvailableStorageSpace(context: Context): String { val storageDirectory = Environment.getExternalStorageDirectory() @@ -29,12 +32,13 @@ fun getMemoryInfo(context: Context): MemoryInfo { return MemoryInfo(totalMemory, availableMemory, usedMemory) } +@SuppressLint("DefaultLocale") fun humanReadableByteCountBin(bytes: Long): String { val unit = 1024 if (bytes < unit) return "$bytes B" - val exp = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt() + val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt() val pre = "KMGTPE"[exp - 1] + "i" - return String.format("%.1f %sB", bytes / Math.pow(unit.toDouble(), exp.toDouble()), pre) + return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre) } fun getBatteryStatus(context: Context): String {