diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index af3091ea..52be86d7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,6 +10,7 @@
android:required="true" />
throw IllegalArgumentException()
@@ -163,7 +159,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
else -> null
}
- setController(entry.key, type, partnerIndex ?: -1)
+ setController(controller.id, type, partnerIndex ?: -1)
}
}
@@ -214,8 +210,6 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
or View.SYSTEM_UI_FLAG_FULLSCREEN)
}
- input = InputManager(this)
-
game_view.holder.addCallback(this)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
@@ -236,6 +230,11 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
game_view.setOnTouchListener(this)
+ // Hide on screen controls when first controller is not set
+ on_screen_controller_view.isInvisible = InputManager.controllers[0]!!.type == ControllerType.None
+ on_screen_controller_view.setOnButtonStateChangedListener(::onButtonStateChanged)
+ on_screen_controller_view.setOnStickStateChangedListener(::onStickStateChanged)
+
executeApplication(intent.data!!)
}
@@ -310,7 +309,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
else -> return super.dispatchKeyEvent(event)
}
- return when (val guestEvent = input.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
+ return when (val guestEvent = InputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
is ButtonGuestEvent -> {
if (guestEvent.button != ButtonId.Menu)
setButtonState(guestEvent.id, guestEvent.button.value(), action.state)
@@ -334,14 +333,14 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
/**
* The last value of the HAT axes so it can be ignored in [onGenericMotionEvent] so they are handled by [dispatchKeyEvent] instead
*/
- private var oldHat = Pair(0.0f, 0.0f)
+ private var oldHat = PointF()
/**
* This handles translating any [MotionHostEvent]s to a [GuestEvent] that is passed into libskyline
*/
override fun onGenericMotionEvent(event : MotionEvent) : Boolean {
if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE) {
- val hat = Pair(event.getAxisValue(MotionEvent.AXIS_HAT_X), event.getAxisValue(MotionEvent.AXIS_HAT_Y))
+ val hat = PointF(event.getAxisValue(MotionEvent.AXIS_HAT_X), event.getAxisValue(MotionEvent.AXIS_HAT_Y))
if (hat == oldHat) {
for (axisItem in MotionHostEvent.axes.withIndex()) {
@@ -351,11 +350,13 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
if ((event.historySize != 0 && value != event.getHistoricalAxisValue(axis, 0)) || (axesHistory[axisItem.index]?.let { it == value } == false)) {
var polarity = value >= 0
- val guestEvent = input.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)] ?: if (value == 0f) {
- polarity = false
- input.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)]
- } else {
- null
+ val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
+ InputManager.eventMap[hostEvent] ?: if (value == 0f) {
+ polarity = false
+ InputManager.eventMap[hostEvent.copy(polarity = false)]
+ } else {
+ null
+ }
}
when (guestEvent) {
@@ -388,7 +389,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(view : View, event : MotionEvent) : Boolean {
- val count = if(event.action != MotionEvent.ACTION_UP && event.action != MotionEvent.ACTION_CANCEL) event.pointerCount else 0
+ val count = if (event.action != MotionEvent.ACTION_UP && event.action != MotionEvent.ACTION_CANCEL) event.pointerCount else 0
val points = IntArray(count * 5) // This is an array of skyline::input::TouchScreenPoint in C++ as that allows for efficient transfer of values to it
var offset = 0
for (index in 0 until count) {
@@ -410,12 +411,30 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
return true
}
+ private fun onButtonStateChanged(buttonId : ButtonId, state : ButtonState) {
+ setButtonState(0, buttonId.value(), state.state)
+ }
+
+ private fun onStickStateChanged(buttonId : ButtonId, position : PointF) {
+ val stickId = when (buttonId) {
+ ButtonId.LeftStick -> StickId.Left
+
+ ButtonId.RightStick -> StickId.Right
+
+ else -> error("Invalid button id")
+ }
+ Log.i("blaa", "$position")
+ setAxisValue(0, stickId.xAxis.ordinal, (position.x * Short.MAX_VALUE).toInt())
+ setAxisValue(0, stickId.yAxis.ordinal, (-position.y * Short.MAX_VALUE).toInt()) // Y is inverted
+ }
+
@SuppressLint("WrongConstant")
+ @Suppress("unused")
fun vibrateDevice(index : Int, timing : LongArray, amplitude : IntArray) {
val vibrator = if (vibrators[index] != null) {
vibrators[index]!!
} else {
- input.controllers[index]?.rumbleDeviceDescriptor?.let {
+ InputManager.controllers[index]!!.rumbleDeviceDescriptor?.let {
if (it == "builtin") {
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
vibrators[index] = vibrator
@@ -423,7 +442,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
} else {
for (id in InputDevice.getDeviceIds()) {
val device = InputDevice.getDevice(id)
- if (device.descriptor == input.controllers[index]?.rumbleDeviceDescriptor) {
+ if (device.descriptor == InputManager.controllers[index]!!.rumbleDeviceDescriptor) {
vibrators[index] = device.vibrator
device.vibrator
}
@@ -437,6 +456,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
vibrator.vibrate(effect)
}
+ @Suppress("unused")
fun clearVibrationDevice(index : Int) {
vibrators[index]?.cancel()
}
diff --git a/app/src/main/java/emu/skyline/LogActivity.kt b/app/src/main/java/emu/skyline/LogActivity.kt
index 4be36cdb..b214e5bc 100644
--- a/app/src/main/java/emu/skyline/LogActivity.kt
+++ b/app/src/main/java/emu/skyline/LogActivity.kt
@@ -137,7 +137,7 @@ class LogActivity : AppCompatActivity() {
private fun uploadAndShareLog() {
Snackbar.make(findViewById(android.R.id.content), getString(R.string.upload_logs), Snackbar.LENGTH_SHORT).show()
- val shareThread = Thread(Runnable {
+ val shareThread = Thread {
var urlConnection : HttpsURLConnection? = null
try {
@@ -173,7 +173,7 @@ class LogActivity : AppCompatActivity() {
} finally {
urlConnection!!.disconnect()
}
- })
+ }
shareThread.start()
}
diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt
index 0b5b3085..2d3fa610 100644
--- a/app/src/main/java/emu/skyline/MainActivity.kt
+++ b/app/src/main/java/emu/skyline/MainActivity.kt
@@ -7,7 +7,6 @@ package emu.skyline
import android.animation.ObjectAnimator
import android.content.Intent
-import android.content.SharedPreferences
import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Bundle
@@ -45,15 +44,19 @@ class MainActivity : AppCompatActivity() {
/**
* This is used to get/set shared preferences
*/
- private lateinit var sharedPreferences : SharedPreferences
+ private val sharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
/**
* The adapter used for adding elements to [app_list]
*/
- private lateinit var adapter : AppAdapter
+ private val adapter by lazy {
+ AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
+ }
private var reloading = AtomicBoolean()
+ private val layoutType get() = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
+
/**
* This adds all files in [directory] with [extension] as an entry in [adapter] using [RomFile] to load metadata
*/
@@ -160,7 +163,6 @@ class MainActivity : AppCompatActivity() {
setSupportActionBar(toolbar)
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
- sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
AppCompatDelegate.setDefaultNightMode(when ((sharedPreferences.getString("app_theme", "2")?.toInt())) {
0 -> AppCompatDelegate.MODE_NIGHT_NO
@@ -221,12 +223,8 @@ class MainActivity : AppCompatActivity() {
val metrics = resources.displayMetrics
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
- val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
-
- adapter = AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
app_list.adapter = adapter
-
- app_list.layoutManager = when (layoutType) {
+ app_list.layoutManager = when (adapter.layoutType) {
LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) }
LayoutType.Grid, LayoutType.GridCompact -> GridLayoutManager(this, gridSpan).apply {
@@ -340,7 +338,6 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
- val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
if (layoutType != adapter.layoutType) {
setupAppList()
}
diff --git a/app/src/main/java/emu/skyline/SettingsActivity.kt b/app/src/main/java/emu/skyline/SettingsActivity.kt
index 4b18b555..5092147e 100644
--- a/app/src/main/java/emu/skyline/SettingsActivity.kt
+++ b/app/src/main/java/emu/skyline/SettingsActivity.kt
@@ -11,9 +11,7 @@ import android.view.KeyEvent
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceGroup
-import emu.skyline.input.InputManager
import emu.skyline.preference.ActivityResultDelegate
-import emu.skyline.preference.ControllerPreference
import emu.skyline.preference.DocumentActivity
import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.android.synthetic.main.titlebar.*
@@ -24,19 +22,12 @@ class SettingsActivity : AppCompatActivity() {
*/
private val preferenceFragment = PreferenceFragment()
- /**
- * This is an instance of [InputManager] used by [ControllerPreference]
- */
- lateinit var inputManager : InputManager
-
/**
* This initializes all of the elements in the activity and displays the settings fragment
*/
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState)
- inputManager = InputManager(this)
-
setContentView(R.layout.settings_activity)
setSupportActionBar(toolbar)
diff --git a/app/src/main/java/emu/skyline/SkylineApplication.kt b/app/src/main/java/emu/skyline/SkylineApplication.kt
new file mode 100644
index 00000000..f6768da9
--- /dev/null
+++ b/app/src/main/java/emu/skyline/SkylineApplication.kt
@@ -0,0 +1,16 @@
+/*
+ * SPDX-License-Identifier: MPL-2.0
+ * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
+ */
+
+package emu.skyline
+
+import android.app.Application
+import emu.skyline.input.InputManager
+
+class SkylineApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ InputManager.init(applicationContext)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt b/app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt
index a5a8dd9e..504be4f1 100644
--- a/app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt
+++ b/app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt
@@ -72,7 +72,7 @@ class ControllerGeneralItem(val context : ControllerActivity, val type : General
* This returns the summary for [type] by using data encapsulated within [Controller]
*/
fun getSummary(context : ControllerActivity, type : GeneralType) : String {
- val controller = context.manager.controllers[context.id]!!
+ val controller = InputManager.controllers[context.id]!!
return when (type) {
GeneralType.PartnerJoyCon -> {
@@ -105,7 +105,7 @@ class ControllerButtonItem(val context : ControllerActivity, val button : Button
*/
fun getSummary(context : ControllerActivity, button : ButtonId) : String {
val guestEvent = ButtonGuestEvent(context.id, button)
- return context.manager.eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
+ return InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
}
}
@@ -125,19 +125,19 @@ class ControllerStickItem(val context : ControllerActivity, val stick : StickId)
*/
fun getSummary(context : ControllerActivity, stick : StickId) : String {
val buttonGuestEvent = ButtonGuestEvent(context.id, stick.button)
- val button = context.manager.eventMap.filter { it.value is ButtonGuestEvent && it.value == buttonGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
+ val button = InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == buttonGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
var axisGuestEvent = AxisGuestEvent(context.id, stick.yAxis, true)
- val yAxisPlus = context.manager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
+ val yAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
axisGuestEvent = AxisGuestEvent(context.id, stick.yAxis, false)
- val yAxisMinus = context.manager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
+ val yAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
axisGuestEvent = AxisGuestEvent(context.id, stick.xAxis, true)
- val xAxisPlus = context.manager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
+ val xAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
axisGuestEvent = AxisGuestEvent(context.id, stick.xAxis, false)
- val xAxisMinus = context.manager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
+ val xAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
return "${context.getString(R.string.button)}: $button\n${context.getString(R.string.up)}: $yAxisPlus\n${context.getString(R.string.down)}: $yAxisMinus\n${context.getString(R.string.left)}: $xAxisMinus\n${context.getString(R.string.right)}: $xAxisPlus"
}
@@ -152,7 +152,7 @@ class ControllerStickItem(val context : ControllerActivity, val stick : StickId)
/**
* This adapter is used to create a list which handles having a simple view
*/
-class ControllerAdapter(val context : Context) : HeaderAdapter() {
+class ControllerAdapter(private val onItemClickCallback : (item : ControllerItem) -> Unit) : HeaderAdapter() {
/**
* This adds a header to the view with the contents of [string]
*/
@@ -189,24 +189,14 @@ class ControllerAdapter(val context : Context) : HeaderAdapter LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false).let { view ->
+ HeaderViewHolder(view).apply { header = view.findViewById(R.id.text_title) }
}
- return holder!!
+ ElementType.Item -> LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false).let { view ->
+ ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle), view.findViewById(R.id.controller_item))
+ }
}
/**
@@ -221,7 +211,7 @@ class ControllerAdapter(val context : Context) : HeaderAdapter()
/**
- * This function updates the [adapter] based on information from [manager]
+ * This function updates the [adapter] based on information from [InputManager]
*/
private fun update() {
adapter.clear()
- val controller = manager.controllers[id]!!
+ val controller = InputManager.controllers[id]!!
adapter.addItem(ControllerTypeItem(this, controller.type))
@@ -134,8 +128,6 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(state : Bundle?) {
super.onCreate(state)
- manager = InputManager(this)
-
id = intent.getIntExtra("index", 0)
if (id < 0 || id > 7)
@@ -158,32 +150,29 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
* This causes the input file to be synced when the activity has been paused
*/
override fun onPause() {
- manager.syncFile()
+ InputManager.syncFile()
super.onPause()
}
- /**
- * This handles the onClick events for the items in the activity
- */
- override fun onClick(v : View?) {
- when (val tag = v!!.tag) {
+ private fun onControllerItemClick(item : ControllerItem) {
+ when (item) {
is ControllerTypeItem -> {
- val controller = manager.controllers[id]!!
+ val controller = InputManager.controllers[id]!!
val types = ControllerType.values().filter { !it.firstController || id == 0 }
val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
MaterialAlertDialogBuilder(this)
- .setTitle(tag.content)
+ .setTitle(item.content)
.setSingleChoiceItems(typeNames, types.indexOf(controller.type)) { dialog, typeIndex ->
val selectedType = types[typeIndex]
if (controller.type != selectedType) {
if (controller is JoyConLeftController)
- controller.partnerId?.let { (manager.controllers[it] as JoyConRightController).partnerId = null }
+ controller.partnerId?.let { (InputManager.controllers[it] as JoyConRightController).partnerId = null }
else if (controller is JoyConRightController)
- controller.partnerId?.let { (manager.controllers[it] as JoyConLeftController).partnerId = null }
+ controller.partnerId?.let { (InputManager.controllers[it] as JoyConLeftController).partnerId = null }
- manager.controllers[id] = when (selectedType) {
+ InputManager.controllers[id] = when (selectedType) {
ControllerType.None -> Controller(id, ControllerType.None)
ControllerType.HandheldProController -> HandheldController(id)
ControllerType.ProController -> ProController(id)
@@ -200,26 +189,28 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
}
is ControllerGeneralItem -> {
- when (tag.type) {
+ when (item.type) {
GeneralType.PartnerJoyCon -> {
- val controller = manager.controllers[id] as JoyConLeftController
+ val controller = InputManager.controllers[id] as JoyConLeftController
- val rJoyCons = manager.controllers.values.filter { it.type == ControllerType.JoyConRight }
+ val rJoyCons = InputManager.controllers.values.filter { it.type == ControllerType.JoyConRight }
val rJoyConNames = (listOf(getString(R.string.none)) + rJoyCons.map { "${getString(R.string.controller)} #${it.id + 1}" }).toTypedArray()
- val partnerNameIndex = if (controller.partnerId == null) 0 else rJoyCons.withIndex().single { it.value.id == controller.partnerId }.index + 1
+ val partnerNameIndex = controller.partnerId?.let { partnerId ->
+ rJoyCons.withIndex().single { it.value.id == partnerId }.index + 1
+ } ?: 0
MaterialAlertDialogBuilder(this)
- .setTitle(tag.content)
+ .setTitle(item.content)
.setSingleChoiceItems(rJoyConNames, partnerNameIndex) { dialog, index ->
- (manager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null
+ (InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null
controller.partnerId = if (index == 0) null else rJoyCons[index - 1].id
if (controller.partnerId != null)
- (manager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id
+ (InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id
- tag.update()
+ item.update()
dialog.dismiss()
}
@@ -227,20 +218,17 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
}
GeneralType.RumbleDevice -> {
- val dialog = RumbleDialog(tag)
- dialog.show(supportFragmentManager, null)
+ RumbleDialog(item).show(supportFragmentManager, null)
}
}
}
is ControllerButtonItem -> {
- val dialog = ButtonDialog(tag)
- dialog.show(supportFragmentManager, null)
+ ButtonDialog(item).show(supportFragmentManager, null)
}
is ControllerStickItem -> {
- val dialog = StickDialog(tag)
- dialog.show(supportFragmentManager, null)
+ StickDialog(item).show(supportFragmentManager, null)
}
}
}
diff --git a/app/src/main/java/emu/skyline/input/HostEvent.kt b/app/src/main/java/emu/skyline/input/HostEvent.kt
index 10e6fe18..c0e85ac7 100644
--- a/app/src/main/java/emu/skyline/input/HostEvent.kt
+++ b/app/src/main/java/emu/skyline/input/HostEvent.kt
@@ -8,75 +8,53 @@ package emu.skyline.input
import android.view.KeyEvent
import android.view.MotionEvent
import java.io.Serializable
-import java.util.*
/**
* This an abstract class for all host events that is inherited by all other event classes
*
* @param descriptor The device descriptor of the device this event originates from
*/
-abstract class HostEvent(val descriptor : String = "") : Serializable {
+sealed class HostEvent(open val descriptor : String = "") : Serializable {
/**
* The [toString] function is abstract so that the derived classes can return a proper string
*/
abstract override fun toString() : String
-
- /**
- * The equality function is abstract so that equality checking will be for the derived classes rather than this abstract class
- */
- abstract override fun equals(other : Any?) : Boolean
-
- /**
- * The hash function is abstract so that hashes will be generated for the derived classes rather than this abstract class
- */
- abstract override fun hashCode() : Int
}
/**
* This class represents all events on the host that arise from a [KeyEvent]
*/
-class KeyHostEvent(descriptor : String = "", val keyCode : Int) : HostEvent(descriptor) {
+data class KeyHostEvent(override val descriptor : String = "", val keyCode : Int) : HostEvent(descriptor) {
/**
* This returns the string representation of [keyCode]
*/
override fun toString() : String = KeyEvent.keyCodeToString(keyCode)
-
- /**
- * This does some basic equality checking for the type of [other] and all members in the class
- */
- override fun equals(other : Any?) : Boolean = if (other is KeyHostEvent) this.descriptor == other.descriptor && this.keyCode == other.keyCode else false
-
- /**
- * This computes the hash for all members of the class
- */
- override fun hashCode() : Int = Objects.hash(descriptor, keyCode)
}
/**
* This class represents all events on the host that arise from a [MotionEvent]
*/
-class MotionHostEvent(descriptor : String = "", val axis : Int, val polarity : Boolean) : HostEvent(descriptor) {
+data class MotionHostEvent(override val descriptor : String = "", val axis : Int, val polarity : Boolean) : HostEvent(descriptor) {
companion object {
/**
* This is an array of all the axes that are checked during a [MotionEvent]
*/
- val axes = arrayOf(MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ, MotionEvent.AXIS_LTRIGGER, MotionEvent.AXIS_RTRIGGER, MotionEvent.AXIS_THROTTLE, MotionEvent.AXIS_RUDDER, MotionEvent.AXIS_WHEEL, MotionEvent.AXIS_GAS, MotionEvent.AXIS_BRAKE).plus(IntRange(MotionEvent.AXIS_GENERIC_1, MotionEvent.AXIS_GENERIC_16).toList())
+ val axes = arrayOf(
+ MotionEvent.AXIS_X,
+ MotionEvent.AXIS_Y,
+ MotionEvent.AXIS_Z,
+ MotionEvent.AXIS_RZ,
+ MotionEvent.AXIS_LTRIGGER,
+ MotionEvent.AXIS_RTRIGGER,
+ MotionEvent.AXIS_THROTTLE,
+ MotionEvent.AXIS_RUDDER,
+ MotionEvent.AXIS_WHEEL,
+ MotionEvent.AXIS_GAS,
+ MotionEvent.AXIS_BRAKE) + (IntRange(MotionEvent.AXIS_GENERIC_1, MotionEvent.AXIS_GENERIC_16).toList())
}
/**
* This returns the string representation of [axis] combined with [polarity]
*/
override fun toString() : String = MotionEvent.axisToString(axis) + if (polarity) "+" else "-"
-
- /**
- * This does some basic equality checking for the type of [other] and all members in the class
- */
- override fun equals(other : Any?) : Boolean {
- return if (other is MotionHostEvent) this.descriptor == other.descriptor && this.axis == other.axis && this.polarity == other.polarity else false
- }
-
- /**
- * This computes the hash for all members of the class
- */
- override fun hashCode() : Int = Objects.hash(descriptor, axis, polarity)
}
diff --git a/app/src/main/java/emu/skyline/input/InputManager.kt b/app/src/main/java/emu/skyline/input/InputManager.kt
index ce3fdbf7..dc173ffc 100644
--- a/app/src/main/java/emu/skyline/input/InputManager.kt
+++ b/app/src/main/java/emu/skyline/input/InputManager.kt
@@ -10,13 +10,13 @@ import android.util.Log
import java.io.*
/**
- * This class is used to manage all transactions with storing/retrieving data in relation to input
+ * This object is used to manage all transactions with storing/retrieving data in relation to input
*/
-class InputManager constructor(val context : Context) {
+object InputManager {
/**
* The underlying [File] object with the input data
*/
- private val file = File("${context.applicationInfo.dataDir}/input.bin")
+ private lateinit var file : File
/**
* A [HashMap] of all the controllers that contains their metadata
@@ -28,33 +28,31 @@ class InputManager constructor(val context : Context) {
*/
lateinit var eventMap : HashMap
- init {
- var readFile = false
+ fun init(context : Context) {
+ file = File("${context.applicationInfo.dataDir}/input.bin")
try {
if (file.exists() && file.length() != 0L) {
syncObjects()
- readFile = true
+ return
}
} catch (e : Exception) {
Log.e(this.toString(), e.localizedMessage ?: "InputManager cannot read \"${file.absolutePath}\"")
}
- if (!readFile) {
- controllers = hashMapOf(
- 0 to Controller(0, ControllerType.None),
- 1 to Controller(1, ControllerType.None),
- 2 to Controller(2, ControllerType.None),
- 3 to Controller(3, ControllerType.None),
- 4 to Controller(4, ControllerType.None),
- 5 to Controller(5, ControllerType.None),
- 6 to Controller(6, ControllerType.None),
- 7 to Controller(7, ControllerType.None))
+ controllers = hashMapOf(
+ 0 to Controller(0, ControllerType.None),
+ 1 to Controller(1, ControllerType.None),
+ 2 to Controller(2, ControllerType.None),
+ 3 to Controller(3, ControllerType.None),
+ 4 to Controller(4, ControllerType.None),
+ 5 to Controller(5, ControllerType.None),
+ 6 to Controller(6, ControllerType.None),
+ 7 to Controller(7, ControllerType.None))
- eventMap = hashMapOf()
+ eventMap = hashMapOf()
- syncFile()
- }
+ syncFile()
}
/**
diff --git a/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt b/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt
index 4bcd6ac5..b0ca1130 100644
--- a/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt
+++ b/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt
@@ -24,7 +24,7 @@ import kotlin.math.abs
*
* @param item This is used to hold the [ControllerButtonItem] between instances
*/
-class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment() {
+class ButtonDialog @JvmOverloads constructor(private val item : ControllerButtonItem? = null) : BottomSheetDialogFragment() {
/**
* This inflates the layout of the dialog after initial view creation
*/
@@ -46,9 +46,9 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
override fun onActivityCreated(savedInstanceState : Bundle?) {
super.onActivityCreated(savedInstanceState)
- if (context is ControllerActivity) {
+ if (item != null && context is ControllerActivity) {
val context = requireContext() as ControllerActivity
- val controller = context.manager.controllers[context.id]!!
+ val controller = InputManager.controllers[context.id]!!
// View focus handling so all input is always directed to this view
view?.requestFocus()
@@ -61,7 +61,7 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
button_reset.setOnClickListener {
val guestEvent = ButtonGuestEvent(context.id, item.button)
- context.manager.eventMap.filterValues { it is ButtonGuestEvent && it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
+ InputManager.eventMap.filterValues { it is ButtonGuestEvent && it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
item.update()
@@ -108,10 +108,10 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
// We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] on [KeyEvent.ACTION_UP]
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
- var guestEvent = context.manager.eventMap[hostEvent]
+ var guestEvent = InputManager.eventMap[hostEvent]
if (guestEvent is GuestEvent) {
- context.manager.eventMap.remove(hostEvent)
+ InputManager.eventMap.remove(hostEvent)
if (guestEvent is ButtonGuestEvent)
context.buttonMap[guestEvent.button]?.update()
@@ -121,9 +121,9 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
guestEvent = ButtonGuestEvent(context.id, item.button)
- context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
+ InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
- context.manager.eventMap[hostEvent] = guestEvent
+ InputManager.eventMap[hostEvent] = guestEvent
item.update()
@@ -186,10 +186,10 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
axisRunnable = Runnable {
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
- var guestEvent = context.manager.eventMap[hostEvent]
+ var guestEvent = InputManager.eventMap[hostEvent]
if (guestEvent is GuestEvent) {
- context.manager.eventMap.remove(hostEvent)
+ InputManager.eventMap.remove(hostEvent)
if (guestEvent is ButtonGuestEvent)
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
@@ -199,9 +199,9 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
guestEvent = ButtonGuestEvent(controller.id, item.button, threshold)
- context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
+ InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
- context.manager.eventMap[hostEvent] = guestEvent
+ InputManager.eventMap[hostEvent] = guestEvent
item.update()
diff --git a/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt b/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt
index c3493111..410ddcc1 100644
--- a/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt
+++ b/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt
@@ -17,6 +17,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import emu.skyline.R
import emu.skyline.adapter.ControllerGeneralItem
import emu.skyline.input.ControllerActivity
+import emu.skyline.input.InputManager
import kotlinx.android.synthetic.main.rumble_dialog.*
/**
@@ -24,7 +25,7 @@ import kotlinx.android.synthetic.main.rumble_dialog.*
*
* @param item This is used to hold the [ControllerGeneralItem] between instances
*/
-class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment() {
+class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralItem? = null) : BottomSheetDialogFragment() {
/**
* This inflates the layout of the dialog after initial view creation
*/
@@ -46,9 +47,9 @@ class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment
override fun onActivityCreated(savedInstanceState : Bundle?) {
super.onActivityCreated(savedInstanceState)
- if (context is ControllerActivity) {
+ if (item != null && context is ControllerActivity) {
val context = requireContext() as ControllerActivity
- val controller = context.manager.controllers[context.id]!!
+ val controller = InputManager.controllers[context.id]!!
// Set up the reset button to clear out [Controller.rumbleDevice] when pressed
rumble_reset.setOnClickListener {
diff --git a/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt b/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt
index 4a61f677..ae2f6d81 100644
--- a/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt
+++ b/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt
@@ -28,7 +28,7 @@ import kotlin.math.max
*
* @param item This is used to hold the [ControllerStickItem] between instances
*/
-class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment() {
+class StickDialog @JvmOverloads constructor(val item : ControllerStickItem? = null) : BottomSheetDialogFragment() {
/**
* This enumerates all of the stages this dialog can be in
*/
@@ -219,9 +219,9 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
override fun onActivityCreated(savedInstanceState : Bundle?) {
super.onActivityCreated(savedInstanceState)
- if (context is ControllerActivity) {
+ if (item != null && context is ControllerActivity) {
val context = requireContext() as ControllerActivity
- val controller = context.manager.controllers[context.id]!!
+ val controller = InputManager.controllers[context.id]!!
// View focus handling so all input is always directed to this view
view?.requestFocus()
@@ -236,13 +236,13 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
for (polarity in booleanArrayOf(true, false)) {
val guestEvent = AxisGuestEvent(context.id, axis, polarity)
- context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
+ InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
}
}
val guestEvent = ButtonGuestEvent(context.id, item.stick.button)
- context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
+ InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
item.update()
@@ -281,7 +281,7 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
((event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON) && event.keyCode != KeyEvent.KEYCODE_BACK) || event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK)) && event.repeatCount == 0 -> {
if (stage == DialogStage.Stick) {
// When the stick is being previewed after everything is mapped we do a lookup into [InputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly
- when (val guestEvent = context.manager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
+ when (val guestEvent = InputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
is ButtonGuestEvent -> {
if (guestEvent.button == item.stick.button) {
if (event.action == KeyEvent.ACTION_DOWN) {
@@ -346,10 +346,10 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
// We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] and add it to [ignoredEvents] on [KeyEvent.ACTION_UP]
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
- var guestEvent = context.manager.eventMap[hostEvent]
+ var guestEvent = InputManager.eventMap[hostEvent]
if (guestEvent is GuestEvent) {
- context.manager.eventMap.remove(hostEvent)
+ InputManager.eventMap.remove(hostEvent)
if (guestEvent is ButtonGuestEvent)
context.buttonMap[guestEvent.button]?.update()
@@ -364,9 +364,9 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
else -> null
}
- context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
+ InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
- context.manager.eventMap[hostEvent] = guestEvent
+ InputManager.eventMap[hostEvent] = guestEvent
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!))
@@ -420,11 +420,13 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
axesHistory[axisItem.index] = value
var polarity = value >= 0
- val guestEvent = context.manager.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)] ?: if (value == 0f) {
- polarity = false
- context.manager.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)]
- } else {
- null
+ val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
+ InputManager.eventMap[hostEvent] ?: if (value == 0f) {
+ polarity = false
+ InputManager.eventMap[hostEvent.copy(polarity = false)]
+ } else {
+ null
+ }
}
when (guestEvent) {
@@ -528,10 +530,10 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
axisRunnable = Runnable {
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
- var guestEvent = context.manager.eventMap[hostEvent]
+ var guestEvent = InputManager.eventMap[hostEvent]
if (guestEvent is GuestEvent) {
- context.manager.eventMap.remove(hostEvent)
+ InputManager.eventMap.remove(hostEvent)
if (guestEvent is ButtonGuestEvent)
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
@@ -548,9 +550,9 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
else -> null
}
- context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
+ InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
- context.manager.eventMap[hostEvent] = guestEvent
+ InputManager.eventMap[hostEvent] = guestEvent
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!, axisPolarity))
diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt
new file mode 100644
index 00000000..9573d939
--- /dev/null
+++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt
@@ -0,0 +1,137 @@
+/*
+ * SPDX-License-Identifier: MPL-2.0
+ * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
+ */
+
+package emu.skyline.input.onscreen
+
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import androidx.core.content.ContextCompat
+import emu.skyline.input.ButtonId
+import kotlin.math.absoluteValue
+import kotlin.math.roundToInt
+
+abstract class OnScreenButton(
+ onScreenControllerView : OnScreenControllerView,
+ val buttonId : ButtonId,
+ private val defaultRelativeX : Float,
+ private val defaultRelativeY : Float,
+ private val defaultRelativeWidth : Float,
+ private val defaultRelativeHeight : Float,
+ drawableId : Int
+) {
+ companion object {
+ /**
+ * Aspect ratio the default values were based on
+ */
+ const val CONFIGURED_ASPECT_RATIO = 2074f / 874f
+ }
+
+ val config = if (onScreenControllerView.isInEditMode) ControllerConfigurationDummy(defaultRelativeX, defaultRelativeY)
+ else ControllerConfigurationImpl(onScreenControllerView.context, buttonId, defaultRelativeX, defaultRelativeY)
+
+ protected val drawable = ContextCompat.getDrawable(onScreenControllerView.context, drawableId)!!
+
+ private val buttonSymbolPaint = Paint().apply { color = Color.GRAY }
+ private val textBoundsRect = Rect()
+
+ var relativeX = config.relativeX
+ var relativeY = config.relativeY
+ private val relativeWidth get() = defaultRelativeWidth * config.globalScale
+ private val relativeHeight get() = defaultRelativeHeight * config.globalScale
+
+ var width = 0
+ var height = 0
+
+ protected val adjustedHeight get() = width / CONFIGURED_ASPECT_RATIO
+ protected val heightDiff get() = (height - adjustedHeight).absoluteValue
+
+ protected val itemWidth get() = width * relativeWidth
+ private val itemHeight get() = adjustedHeight * relativeHeight
+
+ val currentX get() = width * relativeX
+ val currentY get() = adjustedHeight * relativeY + heightDiff
+
+ private val left get() = currentX - itemWidth / 2f
+ private val top get() = currentY - itemHeight / 2f
+
+ protected val currentBounds
+ get() = Rect(
+ left.roundToInt(),
+ top.roundToInt(),
+ (left + itemWidth).roundToInt(),
+ (top + itemHeight).roundToInt()
+ )
+
+ var touchPointerId = -1
+
+ var isEditing = false
+ private set
+
+ protected open fun renderCenteredText(
+ canvas : Canvas,
+ text : String,
+ size : Float,
+ x : Float,
+ y : Float
+ ) {
+ buttonSymbolPaint.apply {
+ textSize = size
+ textAlign = Paint.Align.LEFT
+ getTextBounds(text, 0, text.length, textBoundsRect)
+ }
+ canvas.drawText(
+ text,
+ x - textBoundsRect.width() / 2f - textBoundsRect.left,
+ y + textBoundsRect.height() / 2f - textBoundsRect.bottom,
+ buttonSymbolPaint
+ )
+ }
+
+ open fun render(canvas : Canvas) {
+ val bounds = currentBounds
+ drawable.apply {
+ this.bounds = bounds
+ draw(canvas)
+ }
+
+ renderCenteredText(canvas, buttonId.short!!, itemWidth.coerceAtMost(itemHeight) * 0.4f, bounds.centerX().toFloat(), bounds.centerY().toFloat())
+ }
+
+ abstract fun isTouched(x : Float, y : Float) : Boolean
+
+ abstract fun onFingerDown(x : Float, y : Float)
+
+ abstract fun onFingerUp(x : Float, y : Float)
+
+ fun loadConfigValues() {
+ relativeX = config.relativeX
+ relativeY = config.relativeY
+ }
+
+ fun startEdit() {
+ isEditing = true
+ }
+
+ open fun edit(x : Float, y : Float) {
+ relativeX = x / width
+ relativeY = (y - heightDiff) / adjustedHeight
+ }
+
+ fun endEdit() {
+ config.relativeX = relativeX
+ config.relativeY = relativeY
+ isEditing = false
+ }
+
+ open fun resetRelativeValues() {
+ config.relativeX = defaultRelativeX
+ config.relativeY = defaultRelativeY
+
+ relativeX = defaultRelativeX
+ relativeY = defaultRelativeY
+ }
+}
diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenConfiguration.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenConfiguration.kt
new file mode 100644
index 00000000..9d716ccf
--- /dev/null
+++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenConfiguration.kt
@@ -0,0 +1,68 @@
+/*
+ * SPDX-License-Identifier: MPL-2.0
+ * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
+ */
+
+package emu.skyline.input.onscreen
+
+import android.content.Context
+import emu.skyline.input.ButtonId
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+interface ControllerConfiguration {
+ var globalScale : Float
+ var relativeX : Float
+ var relativeY : Float
+}
+
+class ControllerConfigurationDummy(
+ defaultRelativeX : Float,
+ defaultRelativeY : Float
+) : ControllerConfiguration {
+ override var globalScale = 1f
+ override var relativeX = defaultRelativeX
+ override var relativeY = defaultRelativeY
+}
+
+class ControllerConfigurationImpl(
+ private val context : Context,
+ private val buttonId : ButtonId,
+ defaultRelativeX : Float,
+ defaultRelativeY : Float
+) : ControllerConfiguration {
+ override var globalScale by ControllerPrefs(context, "on_screen_controller_", Float::class.java, 1f)
+
+ private inline fun config(default : T) = ControllerPrefs(context, "${buttonId.name}_", T::class.java, default)
+
+ override var relativeX by config(defaultRelativeX)
+ override var relativeY by config(defaultRelativeY)
+}
+
+@Suppress("UNCHECKED_CAST")
+private class ControllerPrefs(context : Context, private val prefix : String, private val clazz : Class, private val default : T) : ReadWriteProperty {
+ companion object {
+ const val CONTROLLER_CONFIG = "controller_config"
+ }
+
+ private val prefs = context.getSharedPreferences(CONTROLLER_CONFIG, Context.MODE_PRIVATE)
+
+ override fun setValue(thisRef : Any, property : KProperty<*>, value : T) {
+ prefs.edit().apply {
+ when (clazz) {
+ Float::class.java,
+ java.lang.Float::class.java -> putFloat(prefix + property.name, value as Float)
+ else -> error("Unsupported type $clazz ${Float::class.java}")
+ }
+ }.apply()
+ }
+
+ override fun getValue(thisRef : Any, property : KProperty<*>) : T =
+ prefs.let {
+ when (clazz) {
+ Float::class.java,
+ java.lang.Float::class.java -> it.getFloat(prefix + property.name, default as Float)
+ else -> error("Unsupported type $clazz ${Float::class.java}")
+ }
+ } as T
+}
diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenControllerView.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenControllerView.kt
new file mode 100644
index 00000000..f12d4d54
--- /dev/null
+++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenControllerView.kt
@@ -0,0 +1,204 @@
+/*
+ * SPDX-License-Identifier: MPL-2.0
+ * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
+ */
+
+package emu.skyline.input.onscreen
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.PointF
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnTouchListener
+import emu.skyline.input.ButtonId
+import emu.skyline.input.ButtonState
+import kotlin.math.roundToLong
+
+typealias OnButtonStateChangedListener = (buttonId : ButtonId, state : ButtonState) -> Unit
+typealias OnStickStateChangedListener = (buttonId : ButtonId, position : PointF) -> Unit
+
+class OnScreenControllerView @JvmOverloads constructor(
+ context : Context,
+ attrs : AttributeSet? = null,
+ defStyleAttr : Int = 0,
+ defStyleRes : Int = 0
+) : View(context, attrs, defStyleAttr, defStyleRes) {
+ private val controls = Controls(this)
+ private var onButtonStateChangedListener : OnButtonStateChangedListener? = null
+ private var onStickStateChangedListener : OnStickStateChangedListener? = null
+ private val joystickAnimators = mutableMapOf()
+
+ override fun onDraw(canvas : Canvas) {
+ super.onDraw(canvas)
+
+ (controls.circularButtons + controls.rectangularButtons + controls.triggerButtons + controls.joysticks).forEach {
+ it.width = width
+ it.height = height
+ it.render(canvas)
+ }
+ }
+
+ private val playingTouchHandler = OnTouchListener { _, event ->
+ var handled = false
+ val actionIndex = event.actionIndex
+ val pointerId = event.getPointerId(actionIndex)
+ val x by lazy { event.getX(actionIndex) }
+ val y by lazy { event.getY(actionIndex) }
+
+ (controls.circularButtons + controls.rectangularButtons + controls.triggerButtons).forEach { button ->
+ when (event.action and event.actionMasked) {
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP -> {
+ if (pointerId == button.touchPointerId) {
+ button.touchPointerId = -1
+ button.onFingerUp(x, y)
+ onButtonStateChangedListener?.invoke(button.buttonId, ButtonState.Released)
+ handled = true
+ }
+ }
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN -> {
+ if (button.isTouched(x, y)) {
+ button.touchPointerId = pointerId
+ button.onFingerDown(x, y)
+ performClick()
+ onButtonStateChangedListener?.invoke(button.buttonId, ButtonState.Pressed)
+ handled = true
+ }
+ }
+ }
+ }
+
+ for (joystick in controls.joysticks) {
+ when (event.actionMasked) {
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP,
+ MotionEvent.ACTION_CANCEL -> {
+ if (pointerId == joystick.touchPointerId) {
+ joystick.touchPointerId = -1
+
+ val position = PointF(joystick.currentX, joystick.currentY)
+ val radius = joystick.radius
+ val outerToInner = joystick.outerToInner()
+ val outerToInnerLength = outerToInner.length()
+ val direction = outerToInner.normalize()
+ val duration = (150f * outerToInnerLength / radius).roundToLong()
+ joystickAnimators[joystick] = ValueAnimator.ofFloat(outerToInnerLength, 0f).apply {
+ addUpdateListener { animation ->
+ val value = animation.animatedValue as Float
+ val vector = direction.multiply(value)
+ val newPosition = position.add(vector)
+ joystick.onFingerMoved(newPosition.x, newPosition.y)
+ onStickStateChangedListener?.invoke(joystick.buttonId, vector.multiply(1f / radius))
+ invalidate()
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationCancel(animation : Animator?) {
+ super.onAnimationCancel(animation)
+ onAnimationEnd(animation)
+ onStickStateChangedListener?.invoke(joystick.buttonId, PointF(0f, 0f))
+ }
+
+ override fun onAnimationEnd(animation : Animator?) {
+ super.onAnimationEnd(animation)
+ joystick.onFingerUp(event.x, event.y)
+ invalidate()
+ }
+ })
+ setDuration(duration)
+ start()
+ }
+ handled = true
+ }
+ }
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN -> {
+ if (joystick.isTouched(x, y)) {
+ joystickAnimators[joystick]?.cancel()
+ joystickAnimators[joystick] = null
+ joystick.touchPointerId = pointerId
+ joystick.onFingerDown(x, y)
+ performClick()
+ handled = true
+ }
+ }
+ MotionEvent.ACTION_MOVE -> {
+ for (i in 0 until event.pointerCount) {
+ if (event.getPointerId(i) == joystick.touchPointerId) {
+ val centerToPoint = joystick.onFingerMoved(event.getX(i), event.getY(i))
+ onStickStateChangedListener?.invoke(joystick.buttonId, centerToPoint)
+ handled = true
+ }
+ }
+ }
+ }
+ }
+
+ handled.also { if (it) invalidate() }
+ }
+
+ private val editingTouchHandler = OnTouchListener { _, event ->
+ (controls.circularButtons + controls.rectangularButtons + controls.triggerButtons + controls.joysticks).any { button ->
+ when (event.actionMasked) {
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP,
+ MotionEvent.ACTION_CANCEL -> {
+ if (button.isEditing) {
+ button.endEdit()
+ return@any true
+ }
+ }
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN -> {
+ if (button.isTouched(event.x, event.y)) {
+ button.startEdit()
+ performClick()
+ return@any true
+ }
+ }
+ MotionEvent.ACTION_MOVE -> {
+ if (button.isEditing) {
+ button.edit(event.x, event.y)
+ return@any true
+ }
+ }
+ }
+ false
+ }.also { handled -> if (handled) invalidate() }
+ }
+
+ init {
+ setOnTouchListener(playingTouchHandler)
+ }
+
+ fun setEditMode(editMode : Boolean) = setOnTouchListener(if (editMode) editingTouchHandler else playingTouchHandler)
+
+ fun resetControls() {
+ (controls.circularButtons + controls.rectangularButtons + controls.triggerButtons + controls.joysticks).forEach { it.resetRelativeValues() }
+ controls.globalScale = 1f
+ invalidate()
+ }
+
+ fun increaseScale() {
+ controls.globalScale *= 1.1f
+ invalidate()
+ }
+
+ fun decreaseScale() {
+ controls.globalScale *= 0.9f
+ invalidate()
+ }
+
+ fun setOnButtonStateChangedListener(listener : OnButtonStateChangedListener) {
+ onButtonStateChangedListener = listener
+ }
+
+ fun setOnStickStateChangedListener(listener : OnStickStateChangedListener) {
+ onStickStateChangedListener = listener
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenItemDefinitions.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenItemDefinitions.kt
new file mode 100644
index 00000000..2ed0c01b
--- /dev/null
+++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenItemDefinitions.kt
@@ -0,0 +1,205 @@
+/*
+ * SPDX-License-Identifier: MPL-2.0
+ * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
+ */
+
+package emu.skyline.input.onscreen
+
+import android.graphics.Canvas
+import android.graphics.PointF
+import androidx.core.graphics.minus
+import emu.skyline.R
+import emu.skyline.input.ButtonId
+import emu.skyline.input.ButtonId.*
+import kotlin.math.roundToInt
+
+open class CircularButton(
+ onScreenControllerView : OnScreenControllerView,
+ buttonId : ButtonId,
+ defaultRelativeX : Float,
+ defaultRelativeY : Float,
+ defaultRelativeRadiusToX : Float,
+ drawableId : Int = R.drawable.ic_button
+) : OnScreenButton(
+ onScreenControllerView,
+ buttonId,
+ defaultRelativeX,
+ defaultRelativeY,
+ defaultRelativeRadiusToX * 2f,
+ defaultRelativeRadiusToX * CONFIGURED_ASPECT_RATIO * 2f,
+ drawableId
+) {
+ val radius get() = itemWidth / 2f
+
+ override fun isTouched(x : Float, y : Float) : Boolean = PointF(currentX, currentY).minus(PointF(x, y)).length() <= radius
+
+ override fun onFingerDown(x : Float, y : Float) {
+ drawable.alpha = (255 * 0.5f).roundToInt()
+ }
+
+ override fun onFingerUp(x : Float, y : Float) {
+ drawable.alpha = 255
+ }
+}
+
+class JoystickButton(
+ onScreenControllerView : OnScreenControllerView,
+ buttonId : ButtonId,
+ defaultRelativeX : Float,
+ defaultRelativeY : Float,
+ defaultRelativeRadiusToX : Float
+) : CircularButton(
+ onScreenControllerView,
+ buttonId,
+ defaultRelativeX,
+ defaultRelativeY,
+ defaultRelativeRadiusToX,
+ R.drawable.ic_stick_circle
+) {
+
+ private val innerButton = CircularButton(onScreenControllerView, buttonId, config.relativeX, config.relativeY, defaultRelativeRadiusToX * 0.75f, R.drawable.ic_stick)
+
+ override fun renderCenteredText(canvas : Canvas, text : String, size : Float, x : Float, y : Float) = Unit
+
+ override fun render(canvas : Canvas) {
+ super.render(canvas)
+
+ innerButton.width = width
+ innerButton.height = height
+ innerButton.render(canvas)
+ }
+
+ override fun onFingerDown(x : Float, y : Float) {
+ relativeX = x / width
+ relativeY = (y - heightDiff) / adjustedHeight
+ innerButton.relativeX = relativeX
+ innerButton.relativeY = relativeY
+ }
+
+ override fun onFingerUp(x : Float, y : Float) {
+ loadConfigValues()
+ innerButton.relativeX = relativeX
+ innerButton.relativeY = relativeY
+ }
+
+ fun onFingerMoved(x : Float, y : Float) : PointF {
+ val position = PointF(currentX, currentY)
+ var finger = PointF(x, y)
+ val outerToInner = finger.minus(position)
+ val distance = outerToInner.length()
+ if (distance >= radius) {
+ finger = position.add(outerToInner.multiply(1f / distance * radius))
+ }
+
+ innerButton.relativeX = finger.x / width
+ innerButton.relativeY = (finger.y - heightDiff) / adjustedHeight
+ return finger.minus(position).multiply(1f / radius)
+ }
+
+ fun outerToInner() = PointF(innerButton.currentX, innerButton.currentY).minus(PointF(currentX, currentY))
+
+ override fun edit(x : Float, y : Float) {
+ super.edit(x, y)
+ innerButton.relativeX = relativeX
+ innerButton.relativeY = relativeY
+ }
+
+ override fun resetRelativeValues() {
+ super.resetRelativeValues()
+
+ innerButton.relativeX = relativeX
+ innerButton.relativeY = relativeY
+ }
+}
+
+open class RectangularButton(
+ onScreenControllerView : OnScreenControllerView,
+ buttonId : ButtonId,
+ defaultRelativeX : Float,
+ defaultRelativeY : Float,
+ defaultRelativeWidth : Float,
+ defaultRelativeHeight : Float,
+ drawableId : Int = R.drawable.ic_rectangular_button
+) : OnScreenButton(
+ onScreenControllerView,
+ buttonId,
+ defaultRelativeX,
+ defaultRelativeY,
+ defaultRelativeWidth,
+ defaultRelativeHeight,
+ drawableId
+) {
+ override fun isTouched(x : Float, y : Float) =
+ currentBounds.contains(x.roundToInt(), y.roundToInt())
+
+ override fun onFingerDown(x : Float, y : Float) {
+ drawable.alpha = (255 * 0.5f).roundToInt()
+ }
+
+ override fun onFingerUp(x : Float, y : Float) {
+ drawable.alpha = 255
+ }
+}
+
+class TriggerButton(
+ onScreenControllerView : OnScreenControllerView,
+ buttonId : ButtonId,
+ defaultRelativeX : Float,
+ defaultRelativeY : Float,
+ defaultRelativeWidth : Float,
+ defaultRelativeHeight : Float
+) : RectangularButton(
+ onScreenControllerView,
+ buttonId,
+ defaultRelativeX,
+ defaultRelativeY,
+ defaultRelativeWidth,
+ defaultRelativeHeight,
+ when (buttonId) {
+ ZL -> R.drawable.ic_trigger_button_left
+
+ ZR -> R.drawable.ic_trigger_button_right
+
+ else -> error("Unsupported trigger button")
+ }
+)
+
+class Controls(onScreenControllerView : OnScreenControllerView) {
+ val circularButtons = listOf(
+ CircularButton(onScreenControllerView, A, 0.95f, 0.65f, 0.025f),
+ CircularButton(onScreenControllerView, B, 0.9f, 0.75f, 0.025f),
+ CircularButton(onScreenControllerView, X, 0.9f, 0.55f, 0.025f),
+ CircularButton(onScreenControllerView, Y, 0.85f, 0.65f, 0.025f),
+ CircularButton(onScreenControllerView, DpadLeft, 0.2f, 0.65f, 0.025f),
+ CircularButton(onScreenControllerView, DpadUp, 0.25f, 0.55f, 0.025f),
+ CircularButton(onScreenControllerView, DpadRight, 0.3f, 0.65f, 0.025f),
+ CircularButton(onScreenControllerView, DpadDown, 0.25f, 0.75f, 0.025f),
+ CircularButton(onScreenControllerView, Plus, 0.57f, 0.75f, 0.025f),
+ CircularButton(onScreenControllerView, Minus, 0.43f, 0.75f, 0.025f),
+ CircularButton(onScreenControllerView, Menu, 0.5f, 0.75f, 0.025f)
+ )
+
+ val joysticks = listOf(
+ JoystickButton(onScreenControllerView, LeftStick, 0.1f, 0.8f, 0.05f),
+ JoystickButton(onScreenControllerView, RightStick, 0.75f, 0.6f, 0.05f)
+ )
+
+ val rectangularButtons = listOf(
+ RectangularButton(onScreenControllerView, L, 0.1f, 0.25f, 0.075f, 0.08f),
+ RectangularButton(onScreenControllerView, R, 0.9f, 0.25f, 0.075f, 0.08f)
+ )
+
+ val triggerButtons = listOf(
+ TriggerButton(onScreenControllerView, ZL, 0.1f, 0.1f, 0.075f, 0.08f),
+ TriggerButton(onScreenControllerView, ZR, 0.9f, 0.1f, 0.075f, 0.08f)
+ )
+
+ /**
+ * We can take any of the global scale variables from the buttons
+ */
+ var globalScale
+ get() = circularButtons[0].config.globalScale
+ set(value) {
+ circularButtons[0].config.globalScale = value
+ }
+}
diff --git a/app/src/main/java/emu/skyline/input/onscreen/PointExtensions.kt b/app/src/main/java/emu/skyline/input/onscreen/PointExtensions.kt
new file mode 100644
index 00000000..282353b5
--- /dev/null
+++ b/app/src/main/java/emu/skyline/input/onscreen/PointExtensions.kt
@@ -0,0 +1,20 @@
+/*
+ * SPDX-License-Identifier: MPL-2.0
+ * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
+ */
+
+package emu.skyline.input.onscreen
+
+import android.graphics.PointF
+
+fun PointF.add(p : PointF) = PointF(x, y).apply {
+ x += p.x
+ y += p.y
+}
+
+fun PointF.multiply(scalar : Float) = PointF(x, y).apply {
+ x *= scalar
+ y *= scalar
+}
+
+fun PointF.normalize() = multiply(1f / length())
diff --git a/app/src/main/java/emu/skyline/preference/ControllerPreference.kt b/app/src/main/java/emu/skyline/preference/ControllerPreference.kt
index 045ddbbd..4005ded0 100644
--- a/app/src/main/java/emu/skyline/preference/ControllerPreference.kt
+++ b/app/src/main/java/emu/skyline/preference/ControllerPreference.kt
@@ -12,21 +12,18 @@ import android.util.AttributeSet
import androidx.preference.Preference
import androidx.preference.Preference.SummaryProvider
import emu.skyline.R
-import emu.skyline.SettingsActivity
import emu.skyline.input.ControllerActivity
import emu.skyline.input.InputManager
/**
* This preference is used to launch [ControllerActivity] using a preference
*/
-class ControllerPreference @JvmOverloads constructor(context : Context?, attrs : AttributeSet? = null, defStyleAttr : Int = R.attr.preferenceStyle) : Preference(context, attrs, defStyleAttr), ActivityResultDelegate {
+class ControllerPreference @JvmOverloads constructor(context : Context, attrs : AttributeSet? = null, defStyleAttr : Int = R.attr.preferenceStyle) : Preference(context, attrs, defStyleAttr), ActivityResultDelegate {
/**
* The index of the controller this preference manages
*/
private var index = -1
- private var inputManager : InputManager? = null
-
override var requestCode = 0
init {
@@ -45,12 +42,8 @@ class ControllerPreference @JvmOverloads constructor(context : Context?, attrs :
if (key == null)
key = "controller_$index"
- title = "${context?.getString(R.string.config_controller)} #${index + 1}"
-
- if (context is SettingsActivity) {
- inputManager = context.inputManager
- summaryProvider = SummaryProvider { context.inputManager.controllers[index]?.type?.stringRes?.let { context.getString(it) } }
- }
+ title = "${context.getString(R.string.config_controller)} #${index + 1}"
+ summaryProvider = SummaryProvider { InputManager.controllers[index]!!.type.stringRes.let { context.getString(it) } }
}
/**
@@ -62,7 +55,7 @@ class ControllerPreference @JvmOverloads constructor(context : Context?, attrs :
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
if (this.requestCode == requestCode) {
- inputManager?.syncObjects()
+ InputManager.syncObjects()
notifyChanged()
}
}
diff --git a/app/src/main/java/emu/skyline/views/CustomLinearLayout.kt b/app/src/main/java/emu/skyline/views/CustomLinearLayout.kt
index 25831e5a..2cdcd705 100644
--- a/app/src/main/java/emu/skyline/views/CustomLinearLayout.kt
+++ b/app/src/main/java/emu/skyline/views/CustomLinearLayout.kt
@@ -8,7 +8,6 @@ package emu.skyline.views
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
-import android.util.Log
import android.view.View
import android.widget.LinearLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
diff --git a/app/src/main/res/drawable/ic_button.xml b/app/src/main/res/drawable/ic_button.xml
index ba8ea434..84e5688b 100644
--- a/app/src/main/res/drawable/ic_button.xml
+++ b/app/src/main/res/drawable/ic_button.xml
@@ -1,8 +1,8 @@
-
+ android:shape="oval">
+
+ android:width="2.5dp"
+ android:color="#A0FFFFFF" />
diff --git a/app/src/main/res/drawable/ic_rectangular_button.xml b/app/src/main/res/drawable/ic_rectangular_button.xml
new file mode 100644
index 00000000..251af221
--- /dev/null
+++ b/app/src/main/res/drawable/ic_rectangular_button.xml
@@ -0,0 +1,15 @@
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_stick.xml b/app/src/main/res/drawable/ic_stick.xml
index 45db0273..6b1011a1 100644
--- a/app/src/main/res/drawable/ic_stick.xml
+++ b/app/src/main/res/drawable/ic_stick.xml
@@ -2,31 +2,26 @@
-
-
+
+ android:width="2dp"
+ android:color="#A0FFFFFF" />
+ android:width="25dp"
+ android:height="25dp" />
+ android:bottom="10dp"
+ android:left="10dp"
+ android:right="10dp"
+ android:top="10dp" />
-
-
+ android:width="30dp"
+ android:height="30dp" />
diff --git a/app/src/main/res/drawable/ic_stick_circle.xml b/app/src/main/res/drawable/ic_stick_circle.xml
index ba8ea434..84e5688b 100644
--- a/app/src/main/res/drawable/ic_stick_circle.xml
+++ b/app/src/main/res/drawable/ic_stick_circle.xml
@@ -1,8 +1,8 @@
-
+ android:shape="oval">
+
+ android:width="2.5dp"
+ android:color="#A0FFFFFF" />
diff --git a/app/src/main/res/drawable/ic_trigger_button_left.xml b/app/src/main/res/drawable/ic_trigger_button_left.xml
new file mode 100644
index 00000000..6b2ab5f7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_trigger_button_left.xml
@@ -0,0 +1,19 @@
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_trigger_button_right.xml b/app/src/main/res/drawable/ic_trigger_button_right.xml
new file mode 100644
index 00000000..5403d742
--- /dev/null
+++ b/app/src/main/res/drawable/ic_trigger_button_right.xml
@@ -0,0 +1,19 @@
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/emu_activity.xml b/app/src/main/res/layout/emu_activity.xml
index 3f3c368f..1c083858 100644
--- a/app/src/main/res/layout/emu_activity.xml
+++ b/app/src/main/res/layout/emu_activity.xml
@@ -1,5 +1,5 @@
-
+ android:layout_height="match_parent" />
+
+
-
-
+