diff --git a/app/src/main/java/rs/ruffle/Keyboard.kt b/app/src/main/java/rs/ruffle/Keyboard.kt new file mode 100644 index 00000000..2ff43500 --- /dev/null +++ b/app/src/main/java/rs/ruffle/Keyboard.kt @@ -0,0 +1,262 @@ +package rs.ruffle + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import rs.ruffle.ui.theme.RuffleTheme + +val BUTTON_ROWS = arrayOf( + arrayOf( + KeyboardButton(keyCode = 49, keyChar = '1'), + KeyboardButton(keyCode = 50, keyChar = '2'), + KeyboardButton(keyCode = 51, keyChar = '3'), + KeyboardButton(keyCode = 52, keyChar = '4'), + KeyboardButton(keyCode = 53, keyChar = '5'), + KeyboardButton(keyCode = 54, keyChar = '6'), + KeyboardButton(keyCode = 55, keyChar = '7'), + KeyboardButton(keyCode = 56, keyChar = '8'), + KeyboardButton(keyCode = 57, keyChar = '9'), + KeyboardButton(keyCode = 48, keyChar = '0') + ), + arrayOf( + KeyboardButton(keyCode = 81, keyChar = 'q', text = "Q"), + KeyboardButton(keyCode = 87, keyChar = 'w', text = "W"), + KeyboardButton(keyCode = 69, keyChar = 'e', text = "E"), + KeyboardButton(keyCode = 82, keyChar = 'r', text = "R"), + KeyboardButton(keyCode = 84, keyChar = 't', text = "T"), + KeyboardButton(keyCode = 89, keyChar = 'y', text = "Y"), + KeyboardButton(keyCode = 85, keyChar = 'u', text = "U"), + KeyboardButton(keyCode = 73, keyChar = 'i', text = "I"), + KeyboardButton(keyCode = 79, keyChar = 'o', text = "O"), + KeyboardButton(keyCode = 80, keyChar = 'p', text = "P") + ), + arrayOf( + KeyboardButton(keyCode = 65, keyChar = 'a', text = "A"), + KeyboardButton(keyCode = 83, keyChar = 's', text = "S"), + KeyboardButton(keyCode = 68, keyChar = 'd', text = "D"), + KeyboardButton(keyCode = 70, keyChar = 'f', text = "F"), + KeyboardButton(keyCode = 71, keyChar = 'g', text = "G"), + KeyboardButton(keyCode = 72, keyChar = 'h', text = "H"), + KeyboardButton(keyCode = 74, keyChar = 'j', text = "J"), + KeyboardButton(keyCode = 75, keyChar = 'k', text = "K"), + KeyboardButton(keyCode = 76, keyChar = 'l', text = "L") + ), + arrayOf( + KeyboardButton(keyCode = 90, keyChar = 'z', text = "Z"), + KeyboardButton(keyCode = 88, keyChar = 'x', text = "X"), + KeyboardButton(keyCode = 67, keyChar = 'c', text = "C"), + KeyboardButton(keyCode = 86, keyChar = 'v', text = "V"), + KeyboardButton(keyCode = 66, keyChar = 'b', text = "B"), + KeyboardButton(keyCode = 78, keyChar = 'n', text = "N"), + KeyboardButton(keyCode = 77, keyChar = 'm', text = "M"), + KeyboardButton(keyCode = 13, keyChar = '\u000D', text = "↵", size = 5.em) + ), + arrayOf( + KeyboardButton(keyCode = 17, text = "CTRL"), + KeyboardButton(keyCode = 18, text = "ALT"), + KeyboardButton(keyCode = 32, keyChar = ' ', text = "␣"), + KeyboardButton( + keyCode = 37, + icon = Icons.AutoMirrored.Filled.KeyboardArrowLeft, + text = "Left" + ), + KeyboardButton(keyCode = 38, icon = Icons.Filled.KeyboardArrowUp, text = "Up"), + KeyboardButton(keyCode = 40, icon = Icons.Filled.KeyboardArrowDown, text = "Down"), + KeyboardButton( + keyCode = 39, + icon = Icons.AutoMirrored.Filled.KeyboardArrowRight, + text = "Right" + ) + ) +) + +data class KeyboardButton( + val keyCode: Byte, + val keyChar: Char = '\u0000', + val text: String = keyChar.toString(), + val icon: ImageVector? = null, + val size: TextUnit = TextUnit.Unspecified +) + +data class ContextMenuItem( + val text: String, + val separatorBefore: Boolean, + val enabled: Boolean, + val checked: Boolean, + val onClick: () -> Unit +) + +@Composable +fun OnScreenControls( + onKeyClick: (code: Byte, char: Char) -> Unit, + onShowContextMenu: () -> Unit, + onHideContextMenu: () -> Unit, + contextMenuItems: List +) { + var showKeyboard by rememberSaveable { mutableStateOf(true) } + val menuHasAnyCheckmark = contextMenuItems.any { it.checked } + + Surface { + Column(modifier = Modifier.padding(horizontal = 1.dp)) { + if (showKeyboard) { + VirtualKeyboard(onKeyClick) + } + + BottomMenu( + toggleKeyboard = { showKeyboard = !showKeyboard }, + onShowContextMenu, + contextMenuItems, + onHideContextMenu, + menuHasAnyCheckmark + ) + } + } +} + +@Composable +private fun BottomMenu( + toggleKeyboard: () -> Unit, + onShowContextMenu: () -> Unit, + contextMenuItems: List, + onHideContextMenu: () -> Unit, + menuHasAnyCheckmark: Boolean +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Button( + onClick = { toggleKeyboard() }, + contentPadding = PaddingValues(1.dp), + shape = RectangleShape + ) { + Text("⌨") + } + + Button( + onClick = { onShowContextMenu() }, + contentPadding = PaddingValues(1.dp), + shape = RectangleShape + ) { + DropdownMenu( + expanded = contextMenuItems.isNotEmpty(), + onDismissRequest = { onHideContextMenu() } + ) { + contextMenuItems.forEach { + if (it.separatorBefore) { + HorizontalDivider() + } + DropdownMenuItem( + enabled = it.enabled, + text = { + Row { + if (menuHasAnyCheckmark) { + if (it.checked) { + Icon(Icons.Filled.Check, "") + } else { + Spacer( + modifier = Modifier.width( + Icons.Filled.Check.defaultWidth + ) + ) + } + } + Text(it.text) + } + }, + onClick = { it.onClick() } + ) + } + } + Text("▤") + } + } +} + +@Composable +private fun VirtualKeyboard(onKeyClick: (code: Byte, char: Char) -> Unit) { + Column { + BUTTON_ROWS.forEach { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(3.dp) + ) { + it.forEach { + TextButton( + modifier = Modifier.weight(1f), + button = it, + onKeyClick + ) + } + } + } + } +} + +@Composable +fun TextButton( + modifier: Modifier, + button: KeyboardButton, + onKeyClick: (code: Byte, char: Char) -> Unit +) { + Button( + modifier = modifier, + onClick = { + onKeyClick(button.keyCode, button.keyChar) + }, + contentPadding = PaddingValues(1.dp), + shape = RectangleShape + ) { + if (button.icon != null) { + Icon(button.icon, contentDescription = button.text) + } else { + Text( + text = button.text, + fontSize = button.size + ) + } + } +} + +@Preview +@Composable +fun OnScreenControlsPreview() { + RuffleTheme { + OnScreenControls( + onKeyClick = { _: Byte, _: Char -> }, + onShowContextMenu = {}, + onHideContextMenu = {}, + contextMenuItems = listOf() + ) + } +} diff --git a/app/src/main/java/rs/ruffle/PlayerActivity.kt b/app/src/main/java/rs/ruffle/PlayerActivity.kt index 5f223f5a..cb1b33e1 100644 --- a/app/src/main/java/rs/ruffle/PlayerActivity.kt +++ b/app/src/main/java/rs/ruffle/PlayerActivity.kt @@ -1,23 +1,20 @@ package rs.ruffle -import android.annotation.SuppressLint import android.content.Intent -import android.content.res.Configuration import android.net.Uri import android.os.Build import android.os.Build.VERSION_CODES import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.MotionEvent -import android.view.View import android.view.ViewGroup import android.view.Window import android.view.WindowManager -import android.widget.Button -import android.widget.PopupMenu -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.ViewCompat +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowColumn +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat @@ -25,6 +22,7 @@ import com.google.androidgamesdk.GameActivity import java.io.DataInputStream import java.io.File import java.io.IOException +import rs.ruffle.ui.theme.RuffleTheme class PlayerActivity : GameActivity() { @Suppress("unused") @@ -93,6 +91,8 @@ class PlayerActivity : GameActivity() { private val surfaceHeight: Int get() = mSurfaceView.height + private var contextMenuItems = mutableStateListOf() + private external fun keydown(keyCode: Byte, keyChar: Char) private external fun keyup(keyCode: Byte, keyChar: Char) private external fun requestContextMenu() @@ -101,40 +101,40 @@ class PlayerActivity : GameActivity() { @Suppress("unused") // Used by Rust - private fun showContextMenu(items: Array) { + private fun setContextMenu(items: Array) { runOnUiThread { - val popup = PopupMenu(this, findViewById(R.id.button_cm)) - val menu = popup.menu - if (Build.VERSION.SDK_INT >= VERSION_CODES.P) { - menu.setGroupDividerEnabled(true) - } - var group = 1 + contextMenuItems.clear() for (i in items.indices) { val elements = items[i].split(" ".toRegex(), limit = 4).toTypedArray() val enabled = elements[0].toBoolean() val separatorBefore = elements[1].toBoolean() val checked = elements[2].toBoolean() val caption = elements[3] - if (separatorBefore) group += 1 - val item = menu.add(group, i, Menu.NONE, caption) - item.setEnabled(enabled) - if (checked) { - item.setCheckable(true) - item.setChecked(true) - } - } - val exitItemId: Int = items.size - menu.add(group, exitItemId, Menu.NONE, "Exit") - popup.setOnMenuItemClickListener { item: MenuItem -> - if (item.itemId == exitItemId) { - finish() - } else { - runContextMenuCallback(item.itemId) - } - true + contextMenuItems.add( + ContextMenuItem( + text = caption, + separatorBefore = separatorBefore, + enabled = enabled, + checked = checked, + onClick = { + runContextMenuCallback(i) + clearContextMenu() + contextMenuItems.clear() + } + ) + ) } - popup.setOnDismissListener { clearContextMenu() } - popup.show() + contextMenuItems.add( + ContextMenuItem( + text = "Exit", + separatorBefore = true, + enabled = true, + checked = false, + onClick = { + finish() + } + ) + ) } } @@ -150,65 +150,6 @@ class PlayerActivity : GameActivity() { return storageDirPath } - override fun onCreateSurfaceView() { - val inflater = layoutInflater - - @SuppressLint("InflateParams") - val layout = inflater.inflate(R.layout.keyboard, null) as ConstraintLayout - - contentViewId = ViewCompat.generateViewId() - layout.id = contentViewId - setContentView(layout) - mSurfaceView = InputEnabledSurfaceView(this) - - mSurfaceView.contentDescription = "Ruffle Player" - - val placeholder = findViewById(R.id.placeholder) - val pars = placeholder.layoutParams as ConstraintLayout.LayoutParams - val parent = placeholder.parent as ViewGroup - val index = parent.indexOfChild(placeholder) - parent.removeView(placeholder) - parent.addView(mSurfaceView, index) - mSurfaceView.setLayoutParams(pars) - val keys = gatherAllDescendantsOfType