mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-27 15:35:29 +03:00
Addition of Controller Configuration UI
This commit adds in the UI for Controller Configuration to Settings, in addition to introducing the storage and loading of aforementioned configurations to a file that can be saved/loaded at runtime. This commit also fixes updating of individual fields in Settings when changed from an external activity.
This commit is contained in:
parent
8e1f8ae7e9
commit
75d485a9a7
3
.idea/inspectionProfiles/Project_Default.xml
generated
3
.idea/inspectionProfiles/Project_Default.xml
generated
@ -349,6 +349,7 @@
|
||||
<inspection_tool class="AndroidLintUsableSpace" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AndroidLintUseAlpha2" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AndroidLintUseCheckPermission" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AndroidLintUseCompatLoadingForDrawables" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="AndroidLintUseCompoundDrawables" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AndroidLintUseOfBundledGooglePlayServices" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AndroidLintUseRequireInsteadOfGet" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
@ -2479,7 +2480,7 @@
|
||||
<inspection_tool class="UnusedReturnValue" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnusedSymbol" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnusedUnaryOperator" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="UnusedValue" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnusedValue" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="UpperCaseFieldNameNotConstant" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UseBulkOperation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UseCompareMethod" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
|
53
.idea/runConfigurations/Controller_Configuration.xml
generated
Normal file
53
.idea/runConfigurations/Controller_Configuration.xml
generated
Normal file
@ -0,0 +1,53 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Controller Configuration" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
|
||||
<module name="app" />
|
||||
<option name="DEPLOY" value="true" />
|
||||
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
|
||||
<option name="DEPLOY_AS_INSTANT" value="false" />
|
||||
<option name="ARTIFACT_NAME" value="" />
|
||||
<option name="PM_INSTALL_OPTIONS" value="" />
|
||||
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
|
||||
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
|
||||
<option name="MODE" value="specific_activity" />
|
||||
<option name="CLEAR_LOGCAT" value="false" />
|
||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
||||
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
|
||||
<option name="FORCE_STOP_RUNNING_APP" value="true" />
|
||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
|
||||
<option name="DEBUGGER_TYPE" value="Java" />
|
||||
<Auto>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Auto>
|
||||
<Hybrid>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Hybrid>
|
||||
<Java />
|
||||
<Native>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Native>
|
||||
<Profilers>
|
||||
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
|
||||
</Profilers>
|
||||
<option name="DEEP_LINK" value="" />
|
||||
<option name="ACTIVITY_CLASS" value="emu.skyline.input.ControllerActivity" />
|
||||
<method v="2">
|
||||
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
53
.idea/runConfigurations/Setting.xml
generated
Normal file
53
.idea/runConfigurations/Setting.xml
generated
Normal file
@ -0,0 +1,53 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Setting" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
|
||||
<module name="app" />
|
||||
<option name="DEPLOY" value="true" />
|
||||
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
|
||||
<option name="DEPLOY_AS_INSTANT" value="false" />
|
||||
<option name="ARTIFACT_NAME" value="" />
|
||||
<option name="PM_INSTALL_OPTIONS" value="" />
|
||||
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
|
||||
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
|
||||
<option name="MODE" value="specific_activity" />
|
||||
<option name="CLEAR_LOGCAT" value="false" />
|
||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
||||
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
|
||||
<option name="FORCE_STOP_RUNNING_APP" value="true" />
|
||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
|
||||
<option name="DEBUGGER_TYPE" value="Java" />
|
||||
<Auto>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Auto>
|
||||
<Hybrid>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Hybrid>
|
||||
<Java />
|
||||
<Native>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Native>
|
||||
<Profilers>
|
||||
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
|
||||
</Profilers>
|
||||
<option name="DEEP_LINK" value="" />
|
||||
<option name="ACTIVITY_CLASS" value="emu.skyline.SettingsActivity" />
|
||||
<method v="2">
|
||||
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
3
.idea/scopes/SkylineXml.xml
generated
Normal file
3
.idea/scopes/SkylineXml.xml
generated
Normal file
@ -0,0 +1,3 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<scope name="SkylineXml" pattern="file[app]:src/main/res/layout/*||file[app]:src/main/res/menu/*||file[app]:src/main/res/values/*||file[app]:src/main/res/values-night/*||file[app]:src/main/res/xml/*" />
|
||||
</component>
|
@ -2,11 +2,14 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="emu.skyline">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00030001"
|
||||
android:required="true" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:extractNativeLibs="true"
|
||||
@ -32,6 +35,7 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name="emu.skyline.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/settings"
|
||||
android:parentActivityName="emu.skyline.MainActivity">
|
||||
<meta-data
|
||||
@ -43,19 +47,30 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="emu.skyline.SettingsActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="emu.skyline.input.ControllerActivity"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="emu.skyline.SettingsActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="emu.skyline.EmulationActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:launchMode="singleInstance"
|
||||
android:screenOrientation="landscape"
|
||||
android:theme="@style/EmuTheme"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="emu.skyline.MainActivity" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data
|
||||
android:mimeType="application/nro"
|
||||
android:pathPattern=".*\\.nro"
|
||||
@ -65,6 +80,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data
|
||||
android:mimeType="text/plain"
|
||||
android:pathPattern=".*\\.nro"
|
||||
|
@ -170,7 +170,7 @@ namespace skyline::service {
|
||||
}
|
||||
session->serviceStatus = type::KSession::ServiceStatus::Closed;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void ServiceManager::SyncRequestHandler(KHandle handle) {
|
||||
auto session = state.process->GetHandle<type::KSession>(handle);
|
||||
|
@ -15,9 +15,9 @@ import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import emu.skyline.input.AxisId
|
||||
import emu.skyline.input.ButtonId
|
||||
import emu.skyline.input.ButtonState
|
||||
import emu.skyline.input.NpadAxis
|
||||
import emu.skyline.input.NpadButton
|
||||
import emu.skyline.loader.getRomFormat
|
||||
import kotlinx.android.synthetic.main.emu_activity.*
|
||||
import java.io.File
|
||||
@ -231,26 +231,26 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
|
||||
else -> return false
|
||||
}
|
||||
|
||||
val buttonMap : Map<Int, NpadButton> = mapOf(
|
||||
KeyEvent.KEYCODE_BUTTON_A to NpadButton.A,
|
||||
KeyEvent.KEYCODE_BUTTON_B to NpadButton.B,
|
||||
KeyEvent.KEYCODE_BUTTON_X to NpadButton.X,
|
||||
KeyEvent.KEYCODE_BUTTON_Y to NpadButton.Y,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBL to NpadButton.LeftStick,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBR to NpadButton.RightStick,
|
||||
KeyEvent.KEYCODE_BUTTON_L1 to NpadButton.L,
|
||||
KeyEvent.KEYCODE_BUTTON_R1 to NpadButton.R,
|
||||
KeyEvent.KEYCODE_BUTTON_L2 to NpadButton.ZL,
|
||||
KeyEvent.KEYCODE_BUTTON_R2 to NpadButton.ZR,
|
||||
KeyEvent.KEYCODE_BUTTON_START to NpadButton.Plus,
|
||||
KeyEvent.KEYCODE_BUTTON_SELECT to NpadButton.Minus,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN to NpadButton.DpadDown,
|
||||
KeyEvent.KEYCODE_DPAD_UP to NpadButton.DpadUp,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT to NpadButton.DpadLeft,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT to NpadButton.DpadRight)
|
||||
val buttonMap : Map<Int, ButtonId> = mapOf(
|
||||
KeyEvent.KEYCODE_BUTTON_A to ButtonId.A,
|
||||
KeyEvent.KEYCODE_BUTTON_B to ButtonId.B,
|
||||
KeyEvent.KEYCODE_BUTTON_X to ButtonId.X,
|
||||
KeyEvent.KEYCODE_BUTTON_Y to ButtonId.Y,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBL to ButtonId.LeftStick,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBR to ButtonId.RightStick,
|
||||
KeyEvent.KEYCODE_BUTTON_L1 to ButtonId.L,
|
||||
KeyEvent.KEYCODE_BUTTON_R1 to ButtonId.R,
|
||||
KeyEvent.KEYCODE_BUTTON_L2 to ButtonId.ZL,
|
||||
KeyEvent.KEYCODE_BUTTON_R2 to ButtonId.ZR,
|
||||
KeyEvent.KEYCODE_BUTTON_START to ButtonId.Plus,
|
||||
KeyEvent.KEYCODE_BUTTON_SELECT to ButtonId.Minus,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN to ButtonId.DpadDown,
|
||||
KeyEvent.KEYCODE_DPAD_UP to ButtonId.DpadUp,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT to ButtonId.DpadLeft,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT to ButtonId.DpadRight)
|
||||
|
||||
return try {
|
||||
setButtonState(buttonMap.getValue(event.keyCode).value(), action.ordinal);
|
||||
setButtonState(buttonMap.getValue(event.keyCode).value(), action.ordinal)
|
||||
true
|
||||
} catch (ignored : NoSuchElementException) {
|
||||
super.dispatchKeyEvent(event)
|
||||
@ -273,13 +273,13 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
|
||||
override fun dispatchGenericMotionEvent(event : MotionEvent) : Boolean {
|
||||
if ((event.source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD ||
|
||||
(event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) {
|
||||
val hatXMap : Map<Float, NpadButton> = mapOf(
|
||||
-1.0f to NpadButton.DpadLeft,
|
||||
+1.0f to NpadButton.DpadRight)
|
||||
val hatXMap : Map<Float, ButtonId> = mapOf(
|
||||
-1.0f to ButtonId.DpadLeft,
|
||||
+1.0f to ButtonId.DpadRight)
|
||||
|
||||
val hatYMap : Map<Float, NpadButton> = mapOf(
|
||||
-1.0f to NpadButton.DpadUp,
|
||||
+1.0f to NpadButton.DpadDown)
|
||||
val hatYMap : Map<Float, ButtonId> = mapOf(
|
||||
-1.0f to ButtonId.DpadUp,
|
||||
+1.0f to ButtonId.DpadDown)
|
||||
|
||||
if (controllerHatX != event.getAxisValue(MotionEvent.AXIS_HAT_X)) {
|
||||
if (event.getAxisValue(MotionEvent.AXIS_HAT_X) == 0.0f)
|
||||
@ -305,11 +305,11 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
if ((event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.action == MotionEvent.ACTION_MOVE) {
|
||||
val axisMap : Map<Int, NpadAxis> = mapOf(
|
||||
MotionEvent.AXIS_X to NpadAxis.LX,
|
||||
MotionEvent.AXIS_Y to NpadAxis.LY,
|
||||
MotionEvent.AXIS_Z to NpadAxis.RX,
|
||||
MotionEvent.AXIS_RZ to NpadAxis.RY)
|
||||
val axisMap : Map<Int, AxisId> = mapOf(
|
||||
MotionEvent.AXIS_X to AxisId.LX,
|
||||
MotionEvent.AXIS_Y to AxisId.LY,
|
||||
MotionEvent.AXIS_Z to AxisId.RX,
|
||||
MotionEvent.AXIS_RZ to AxisId.RY)
|
||||
|
||||
//TODO: Digital inputs based off of analog sticks
|
||||
event.device.motionRanges.forEach {
|
||||
|
@ -9,7 +9,10 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import emu.skyline.input.InputManager
|
||||
import emu.skyline.preference.ControllerPreference
|
||||
import kotlinx.android.synthetic.main.titlebar.*
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
@ -19,11 +22,23 @@ class SettingsActivity : AppCompatActivity() {
|
||||
private val preferenceFragment : PreferenceFragment = PreferenceFragment()
|
||||
|
||||
/**
|
||||
* This initializes [toolbar] and [R.id.settings]
|
||||
* This is an instance of [InputManager] used by [ControllerPreference]
|
||||
*/
|
||||
lateinit var inputManager : InputManager
|
||||
|
||||
/**
|
||||
* The key of the element to force a refresh when [onActivityResult] is called
|
||||
*/
|
||||
var refreshKey : String? = null
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@ -36,11 +51,17 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to refresh the preferences after [emu.skyline.preference.FolderActivity] has returned
|
||||
* This is used to refresh the preferences after [emu.skyline.preference.FolderActivity] or [emu.skyline.input.ControllerActivity] has returned
|
||||
*/
|
||||
public override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
preferenceFragment.refreshPreferences()
|
||||
|
||||
if (refreshKey != null) {
|
||||
inputManager.syncObjects()
|
||||
preferenceFragment.refreshPreference(refreshKey!!)
|
||||
|
||||
refreshKey = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,11 +69,12 @@ class SettingsActivity : AppCompatActivity() {
|
||||
*/
|
||||
class PreferenceFragment : PreferenceFragmentCompat() {
|
||||
/**
|
||||
* This clears the preference screen and reloads all preferences
|
||||
* This forces refreshing a certain preference by indirectly calling [Preference.notifyChanged]
|
||||
*/
|
||||
fun refreshPreferences() {
|
||||
preferenceScreen = null
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
fun refreshPreference(key : String) {
|
||||
val preference = preferenceManager.findPreference<Preference>(key)!!
|
||||
preference.isSelectable = !preference.isSelectable
|
||||
preference.isSelectable = !preference.isSelectable
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,14 +98,12 @@ internal class AppAdapter(val layoutType : LayoutType, private val gridSpan : In
|
||||
}
|
||||
|
||||
/**
|
||||
* This function binds the item at [position] to the supplied [viewHolder]
|
||||
* This function binds the item at [position] to the supplied [holder]
|
||||
*/
|
||||
override fun onBindViewHolder(viewHolder : RecyclerView.ViewHolder, position : Int) {
|
||||
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is AppItem) {
|
||||
val holder = viewHolder as ItemViewHolder
|
||||
|
||||
if (item is AppItem && holder is ItemViewHolder) {
|
||||
holder.title.text = item.title
|
||||
holder.subtitle.text = item.subTitle ?: missingString
|
||||
|
||||
@ -122,9 +120,7 @@ internal class AppAdapter(val layoutType : LayoutType, private val gridSpan : In
|
||||
setOnClickListener { onClick.invoke(item) }
|
||||
setOnLongClickListener { true.also { onLongClick.invoke(item) } }
|
||||
}
|
||||
} else if (item is BaseHeader) {
|
||||
val holder = viewHolder as HeaderViewHolder
|
||||
|
||||
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
||||
holder.header!!.text = item.title
|
||||
}
|
||||
}
|
||||
|
228
app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt
Normal file
228
app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt
Normal file
@ -0,0 +1,228 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.BaseItem
|
||||
import emu.skyline.input.*
|
||||
|
||||
/**
|
||||
* This is a class that holds everything relevant to a single item in the controller configuration list
|
||||
*
|
||||
* @param content The main line of text describing what the item is
|
||||
* @param subContent The secondary line of text to show data more specific data about the item
|
||||
*/
|
||||
abstract class ControllerItem(var content : String, var subContent : String) : BaseItem() {
|
||||
/**
|
||||
* The underlying adapter this item is contained within
|
||||
*/
|
||||
var adapter : ControllerAdapter? = null
|
||||
|
||||
/**
|
||||
* The position of this item in the adapter
|
||||
*/
|
||||
var position : Int? = null
|
||||
|
||||
/**
|
||||
* This function updates the visible contents of the item
|
||||
*/
|
||||
fun update(content : String?, subContent : String?) {
|
||||
if (content != null)
|
||||
this.content = content
|
||||
|
||||
if (subContent != null)
|
||||
this.subContent = subContent
|
||||
|
||||
position?.let { adapter?.notifyItemChanged(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used as a generic function to update the contents of the item
|
||||
*/
|
||||
abstract fun update()
|
||||
}
|
||||
|
||||
/**
|
||||
* This item is used to display the [type] of the currently active controller
|
||||
*/
|
||||
class ControllerTypeItem(val context : Context, val type : ControllerType) : ControllerItem(context.getString(R.string.controller_type), context.getString(type.stringRes)) {
|
||||
/**
|
||||
* This function just updates [subContent] based on [type]
|
||||
*/
|
||||
override fun update() = update(null, context.getString(type.stringRes))
|
||||
}
|
||||
|
||||
/**
|
||||
* This item is used to display general settings items regarding controller
|
||||
*
|
||||
* @param type The type of controller setting this item is displaying
|
||||
*/
|
||||
class ControllerGeneralItem(val context : ControllerActivity, val type : GeneralType) : ControllerItem(context.getString(type.stringRes), getSummary(context, type)) {
|
||||
companion object {
|
||||
/**
|
||||
* 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]!!
|
||||
|
||||
return when (type) {
|
||||
GeneralType.PartnerJoyCon -> {
|
||||
val partner = (controller as JoyConLeftController).partnerId
|
||||
|
||||
if (partner != null)
|
||||
"${context.getString(R.string.controller)} #${partner + 1}"
|
||||
else
|
||||
context.getString(R.string.none)
|
||||
}
|
||||
GeneralType.RumbleDevice -> controller.rumbleDevice?.second ?: context.getString(R.string.none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function just updates [subContent] based on [getSummary]
|
||||
*/
|
||||
override fun update() = update(null, getSummary(context, type))
|
||||
}
|
||||
|
||||
/**
|
||||
* This item is used to display a particular [button] mapping for the controller
|
||||
*/
|
||||
class ControllerButtonItem(val context : ControllerActivity, val button : ButtonId) : ControllerItem(button.long?.let { context.getString(it) } ?: button.toString(), getSummary(context, button)) {
|
||||
companion object {
|
||||
/**
|
||||
* This returns the summary for [button] by doing a reverse-lookup in [InputManager.eventMap]
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function just updates [subContent] based on [getSummary]
|
||||
*/
|
||||
override fun update() = update(null, getSummary(context, button))
|
||||
}
|
||||
|
||||
/**
|
||||
* This item is used to display all information regarding a [stick] and it's mappings for the controller
|
||||
*/
|
||||
class ControllerStickItem(val context : ControllerActivity, val stick : StickId) : ControllerItem(stick.toString(), getSummary(context, stick)) {
|
||||
companion object {
|
||||
/**
|
||||
* This returns the summary for [stick] by doing reverse-lookups in [InputManager.eventMap]
|
||||
*/
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function just updates [subContent] based on [getSummary]
|
||||
*/
|
||||
override fun update() = update(null, getSummary(context, stick))
|
||||
}
|
||||
|
||||
/**
|
||||
* This adapter is used to create a list which handles having a simple view
|
||||
*/
|
||||
class ControllerAdapter(val context : Context) : HeaderAdapter<ControllerItem?, BaseHeader, RecyclerView.ViewHolder>() {
|
||||
/**
|
||||
* This adds a header to the view with the contents of [string]
|
||||
*/
|
||||
fun addHeader(string : String) {
|
||||
super.addHeader(BaseHeader(string))
|
||||
}
|
||||
|
||||
/**
|
||||
* This functions sets [ControllerItem.adapter] and delegates the call to [HeaderAdapter.addItem]
|
||||
*/
|
||||
fun addItem(item : ControllerItem) {
|
||||
item.adapter = this
|
||||
super.addItem(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* The ViewHolder used by items is used to hold the views associated with an item
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param title The TextView associated with the title
|
||||
* @param subtitle The TextView associated with the subtitle
|
||||
* @param item The View containing the two other views
|
||||
*/
|
||||
class ItemViewHolder(val parent : View, var title : TextView, var subtitle : TextView, var item : View) : RecyclerView.ViewHolder(parent)
|
||||
|
||||
/**
|
||||
* The ViewHolder used by headers is used to hold the views associated with an headers
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param header The TextView associated with the header
|
||||
*/
|
||||
private class HeaderViewHolder(val parent : View, var header : TextView? = null) : RecyclerView.ViewHolder(parent)
|
||||
|
||||
/**
|
||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||
*/
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
var holder : RecyclerView.ViewHolder? = null
|
||||
|
||||
if (viewType == ElementType.Item.ordinal) {
|
||||
val view = inflater.inflate(R.layout.controller_item, parent, false)
|
||||
holder = ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle), view.findViewById(R.id.controller_item))
|
||||
|
||||
if (context is View.OnClickListener)
|
||||
holder.item.setOnClickListener(context as View.OnClickListener)
|
||||
} else if (viewType == ElementType.Header.ordinal) {
|
||||
val view = inflater.inflate(R.layout.section_item, parent, false)
|
||||
holder = HeaderViewHolder(view)
|
||||
|
||||
holder.header = view.findViewById(R.id.text_title)
|
||||
}
|
||||
|
||||
return holder!!
|
||||
}
|
||||
|
||||
/**
|
||||
* This function binds the item at [position] to the supplied [holder]
|
||||
*/
|
||||
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is ControllerItem && holder is ItemViewHolder) {
|
||||
item.position = position
|
||||
|
||||
holder.title.text = item.content
|
||||
holder.subtitle.text = item.subContent
|
||||
|
||||
holder.parent.tag = item
|
||||
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
||||
holder.header?.text = item.title
|
||||
}
|
||||
}
|
||||
}
|
@ -17,13 +17,12 @@ import java.io.*
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
/**
|
||||
* An enumeration of the type of elements in this adapter
|
||||
*/
|
||||
enum class ElementType(val type : Int) {
|
||||
Header(0x0),
|
||||
Item(0x1)
|
||||
enum class ElementType {
|
||||
Header,
|
||||
Item,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,7 +131,7 @@ abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, Vie
|
||||
* @param position The position of the element
|
||||
*/
|
||||
override fun getItemViewType(position : Int) : Int {
|
||||
return elementArray[visibleArray[position]]!!.elementType.type
|
||||
return elementArray[visibleArray[position]]!!.elementType.ordinal
|
||||
}
|
||||
|
||||
/**
|
||||
@ -194,7 +193,7 @@ abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, Vie
|
||||
for (index in elementArray.indices) {
|
||||
val item = elementArray[index]!!
|
||||
|
||||
if (item is BaseItem) {
|
||||
if (item is BaseItem && item.key() != null) {
|
||||
keyIndex.append(keyArray.size, index)
|
||||
keyArray.add(item.key()!!.toLowerCase(Locale.getDefault()))
|
||||
}
|
||||
|
@ -100,14 +100,12 @@ internal class LogAdapter internal constructor(val context : Context, val compac
|
||||
}
|
||||
|
||||
/**
|
||||
* This function binds the item at [position] to the supplied [viewHolder]
|
||||
* This function binds the item at [position] to the supplied [holder]
|
||||
*/
|
||||
override fun onBindViewHolder(viewHolder : RecyclerView.ViewHolder, position : Int) {
|
||||
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is LogItem) {
|
||||
val holder = viewHolder as ItemViewHolder
|
||||
|
||||
if (item is LogItem && holder is ItemViewHolder) {
|
||||
holder.title.text = item.message
|
||||
holder.subtitle?.text = item.level
|
||||
|
||||
@ -115,9 +113,7 @@ internal class LogAdapter internal constructor(val context : Context, val compac
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
|
||||
Toast.makeText(holder.itemView.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else if (item is BaseHeader) {
|
||||
val holder = viewHolder as HeaderViewHolder
|
||||
|
||||
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
||||
holder.header.text = item.title
|
||||
}
|
||||
}
|
||||
|
@ -15,5 +15,5 @@ abstract class BaseItem : BaseElement(ElementType.Item) {
|
||||
/**
|
||||
* This function returns a string used for searching
|
||||
*/
|
||||
abstract fun key() : String?
|
||||
open fun key() : String? = null
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.input
|
||||
|
||||
/**
|
||||
* This is a generic interface for all Button classes to implement
|
||||
*/
|
||||
interface ButtonId {
|
||||
/**
|
||||
* This should return the value of the Button according to what libskyline expects
|
||||
*/
|
||||
fun value() : Long
|
||||
}
|
||||
|
||||
enum class ButtonState(val state : Boolean) {
|
||||
Released(false),
|
||||
Pressed(true),
|
||||
}
|
72
app/src/main/java/emu/skyline/input/Controller.kt
Normal file
72
app/src/main/java/emu/skyline/input/Controller.kt
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.input
|
||||
|
||||
import emu.skyline.R
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* This enumerates the types of Controller that can be emulated
|
||||
*
|
||||
* @param stringRes The string resource of the controller's name
|
||||
* @param firstController If the type only applies to the first controller
|
||||
*/
|
||||
enum class ControllerType(val stringRes : Int, val firstController : Boolean, val sticks : Array<StickId> = arrayOf(), val buttons : Array<ButtonId> = arrayOf()) {
|
||||
None(R.string.none, false),
|
||||
HandheldProController(R.string.handheld_procon, true, arrayOf(StickId.Left, StickId.Right), arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y, ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR, ButtonId.Plus, ButtonId.Minus)),
|
||||
ProController(R.string.procon, false, arrayOf(StickId.Left, StickId.Right), arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y, ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR, ButtonId.Plus, ButtonId.Minus)),
|
||||
JoyConLeft(R.string.ljoycon, false, arrayOf(StickId.Left), arrayOf(ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.ZL, ButtonId.Minus, ButtonId.LeftSL, ButtonId.LeftSR)),
|
||||
JoyConRight(R.string.rjoycon, false, arrayOf(StickId.Right), arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y, ButtonId.R, ButtonId.ZR, ButtonId.Plus, ButtonId.RightSL, ButtonId.RightSR)),
|
||||
}
|
||||
|
||||
/**
|
||||
* The enumerates the type of general settings for a Controller
|
||||
*
|
||||
* @param stringRes The string resource for the setting
|
||||
* @param compatibleControllers An array of the types of compatible controllers
|
||||
*/
|
||||
enum class GeneralType(val stringRes : Int, val compatibleControllers : Array<ControllerType>? = null) {
|
||||
PartnerJoyCon(R.string.partner_joycon, arrayOf(ControllerType.JoyConLeft)),
|
||||
RumbleDevice(R.string.rumble_device),
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the base class for all controllers, when controllers require to store more variables it'll be stored here
|
||||
*
|
||||
* @param id The ID of the controller
|
||||
* @param type The type of the controller
|
||||
* @param rumbleDevice The device descriptor and the name of the device rumble/force-feedback will be passed onto
|
||||
*/
|
||||
open class Controller(val id : Int, var type : ControllerType, var rumbleDevice : Pair<String, String>? = null) : Serializable {
|
||||
/**
|
||||
* The current version of this class so that different versions won't be deserialized mistakenly
|
||||
*/
|
||||
private val serialVersionUID = 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* This Controller class is for the Handheld-ProCon controller that change based on the operation mode
|
||||
*/
|
||||
class HandheldController(id : Int) : Controller(id, ControllerType.HandheldProController)
|
||||
|
||||
/**
|
||||
* This Controller class is for the Pro Controller (ProCon)
|
||||
*/
|
||||
class ProController(id : Int) : Controller(id, ControllerType.ProController)
|
||||
|
||||
/**
|
||||
* This Controller class is for the left Joy-Con controller
|
||||
*
|
||||
* @param partnerId The ID of the corresponding right Joy-Con if this is a pair
|
||||
*/
|
||||
class JoyConLeftController(id : Int, var partnerId : Int? = null) : Controller(id, ControllerType.JoyConLeft)
|
||||
|
||||
/**
|
||||
* This Controller class is for the right Joy-Con controller
|
||||
*
|
||||
* @param partnerId The ID of the corresponding left Joy-Con if this is a pair
|
||||
*/
|
||||
class JoyConRightController(id : Int, var partnerId : Int? = null) : Controller(id, ControllerType.JoyConRight)
|
256
app/src/main/java/emu/skyline/input/ControllerActivity.kt
Normal file
256
app/src/main/java/emu/skyline/input/ControllerActivity.kt
Normal file
@ -0,0 +1,256 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.input
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.*
|
||||
import emu.skyline.input.dialog.ButtonDialog
|
||||
import emu.skyline.input.dialog.RumbleDialog
|
||||
import emu.skyline.input.dialog.StickDialog
|
||||
import kotlinx.android.synthetic.main.controller_activity.*
|
||||
import kotlinx.android.synthetic.main.titlebar.*
|
||||
|
||||
/**
|
||||
* This activity is used to change the settings for a specific controller
|
||||
*/
|
||||
class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
||||
/**
|
||||
* The index of the controller this activity manages
|
||||
*/
|
||||
var id : Int = -1
|
||||
|
||||
/**
|
||||
* The adapter used by [controller_list] to hold all the items
|
||||
*/
|
||||
val adapter = ControllerAdapter(this)
|
||||
|
||||
/**
|
||||
* The [InputManager] class handles loading/saving the input data
|
||||
*/
|
||||
lateinit var manager : InputManager
|
||||
|
||||
/**
|
||||
* This is a map between a button and it's corresponding [ControllerItem] in [adapter]
|
||||
*/
|
||||
val buttonMap = mutableMapOf<ButtonId, ControllerItem>()
|
||||
|
||||
/**
|
||||
* This is a map between an axis and it's corresponding [ControllerStickItem] in [adapter]
|
||||
*/
|
||||
val axisMap = mutableMapOf<AxisId, ControllerStickItem>()
|
||||
|
||||
/**
|
||||
* This function updates the [adapter] based on information from [manager]
|
||||
*/
|
||||
private fun update() {
|
||||
adapter.clear()
|
||||
|
||||
val controller = manager.controllers[id]!!
|
||||
|
||||
adapter.addItem(ControllerTypeItem(this, controller.type))
|
||||
|
||||
if (controller.type == ControllerType.None)
|
||||
return
|
||||
|
||||
var wroteTitle = false
|
||||
|
||||
for (item in GeneralType.values()) {
|
||||
if (item.compatibleControllers == null || item.compatibleControllers.contains(controller.type)) {
|
||||
if (!wroteTitle) {
|
||||
adapter.addHeader(getString(R.string.general))
|
||||
wroteTitle = true
|
||||
}
|
||||
|
||||
adapter.addItem(ControllerGeneralItem(this, item))
|
||||
}
|
||||
}
|
||||
|
||||
wroteTitle = false
|
||||
|
||||
for (stick in controller.type.sticks) {
|
||||
if (!wroteTitle) {
|
||||
adapter.addHeader(getString(R.string.sticks))
|
||||
wroteTitle = true
|
||||
}
|
||||
|
||||
val stickItem = ControllerStickItem(this, stick)
|
||||
|
||||
adapter.addItem(stickItem)
|
||||
buttonMap[stick.button] = stickItem
|
||||
axisMap[stick.xAxis] = stickItem
|
||||
axisMap[stick.yAxis] = stickItem
|
||||
}
|
||||
|
||||
val dpadButtons = Pair(R.string.dpad, arrayOf(ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight))
|
||||
val faceButtons = Pair(R.string.face_buttons, arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y))
|
||||
val shoulderTriggerButtons = Pair(R.string.shoulder_trigger, arrayOf(ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR))
|
||||
val shoulderRailButtons = Pair(R.string.shoulder_rail, arrayOf(ButtonId.LeftSL, ButtonId.LeftSR, ButtonId.RightSL, ButtonId.RightSR))
|
||||
|
||||
val buttonArrays = arrayOf(dpadButtons, faceButtons, shoulderTriggerButtons, shoulderRailButtons)
|
||||
|
||||
for (buttonArray in buttonArrays) {
|
||||
wroteTitle = false
|
||||
|
||||
for (button in controller.type.buttons.filter { it in buttonArray.second }) {
|
||||
if (!wroteTitle) {
|
||||
adapter.addHeader(getString(buttonArray.first))
|
||||
wroteTitle = true
|
||||
}
|
||||
|
||||
val buttonItem = ControllerButtonItem(this, button)
|
||||
|
||||
adapter.addItem(buttonItem)
|
||||
buttonMap[button] = buttonItem
|
||||
}
|
||||
}
|
||||
|
||||
wroteTitle = false
|
||||
|
||||
for (button in controller.type.buttons.filterNot { item -> buttonArrays.any { item in it.second } }.plus(ButtonId.Menu)) {
|
||||
if (!wroteTitle) {
|
||||
adapter.addHeader(getString(R.string.misc_buttons))
|
||||
wroteTitle = true
|
||||
}
|
||||
|
||||
val buttonItem = ControllerButtonItem(this, button)
|
||||
|
||||
adapter.addItem(buttonItem)
|
||||
buttonMap[button] = buttonItem
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This initializes all of the elements in the activity
|
||||
*/
|
||||
override fun onCreate(state : Bundle?) {
|
||||
super.onCreate(state)
|
||||
|
||||
manager = InputManager(this)
|
||||
|
||||
id = intent.getIntExtra("index", 0)
|
||||
|
||||
if (id < 0 || id > 7)
|
||||
throw IllegalArgumentException()
|
||||
|
||||
title = "${getString(R.string.config_controller)} #${id + 1}"
|
||||
|
||||
setContentView(R.layout.controller_activity)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener {
|
||||
if (!it)
|
||||
toolbar_layout.setExpanded(false)
|
||||
}
|
||||
|
||||
controller_list.layoutManager = LinearLayoutManager(this)
|
||||
controller_list.adapter = adapter
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
/**
|
||||
* This causes the input file to be synced when the activity has been paused
|
||||
*/
|
||||
override fun onPause() {
|
||||
manager.syncFile()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
/**
|
||||
* This handles the onClick events for the items in the activity
|
||||
*/
|
||||
override fun onClick(v : View?) {
|
||||
when (val tag = v!!.tag) {
|
||||
is ControllerTypeItem -> {
|
||||
val type = manager.controllers[id]!!.type
|
||||
|
||||
val types = ControllerType.values().filter { !it.firstController || id == 0 }
|
||||
val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(tag.content)
|
||||
.setSingleChoiceItems(typeNames, types.indexOf(type)) { dialog, typeIndex ->
|
||||
manager.controllers[id] = when (types[typeIndex]) {
|
||||
ControllerType.None -> Controller(id, ControllerType.None)
|
||||
ControllerType.HandheldProController -> HandheldController(id)
|
||||
ControllerType.ProController -> ProController(id)
|
||||
ControllerType.JoyConLeft -> JoyConLeftController(id)
|
||||
ControllerType.JoyConRight -> JoyConRightController(id)
|
||||
}
|
||||
|
||||
update()
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
is ControllerGeneralItem -> {
|
||||
when (tag.type) {
|
||||
GeneralType.PartnerJoyCon -> {
|
||||
val controller = manager.controllers[id] as JoyConLeftController
|
||||
|
||||
val rJoyCons = manager.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
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(tag.content)
|
||||
.setSingleChoiceItems(rJoyConNames, partnerNameIndex) { dialog, index ->
|
||||
(manager.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
|
||||
|
||||
tag.update()
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
GeneralType.RumbleDevice -> {
|
||||
val dialog = RumbleDialog(tag)
|
||||
dialog.show(supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ControllerButtonItem -> {
|
||||
val dialog = ButtonDialog(tag)
|
||||
dialog.show(supportFragmentManager, null)
|
||||
}
|
||||
|
||||
is ControllerStickItem -> {
|
||||
val dialog = StickDialog(tag)
|
||||
dialog.show(supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This handles on calling [onBackPressed] when [KeyEvent.KEYCODE_BUTTON_B] is lifted
|
||||
*/
|
||||
override fun onKeyUp(keyCode : Int, event : KeyEvent?) : Boolean {
|
||||
if (keyCode == KeyEvent.KEYCODE_BUTTON_B) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
}
|
156
app/src/main/java/emu/skyline/input/GuestEvent.kt
Normal file
156
app/src/main/java/emu/skyline/input/GuestEvent.kt
Normal file
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.input
|
||||
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* This enumerates all of the buttons that the emulator recognizes
|
||||
*/
|
||||
enum class ButtonId(val short : String? = null, val long : Int? = null) {
|
||||
A("A", emu.skyline.R.string.a_button),
|
||||
B("B", emu.skyline.R.string.b_button),
|
||||
X("X", emu.skyline.R.string.x_button),
|
||||
Y("Y", emu.skyline.R.string.y_button),
|
||||
LeftStick("L"),
|
||||
RightStick("R"),
|
||||
L("L", emu.skyline.R.string.left_shoulder),
|
||||
R("R", emu.skyline.R.string.right_shoulder),
|
||||
ZL("ZL", emu.skyline.R.string.left_trigger),
|
||||
ZR("ZR", emu.skyline.R.string.right_trigger),
|
||||
Plus("+", emu.skyline.R.string.plus_button),
|
||||
Minus("-", emu.skyline.R.string.minus_button),
|
||||
DpadLeft("◀", emu.skyline.R.string.left),
|
||||
DpadUp("▲", emu.skyline.R.string.up),
|
||||
DpadRight("▶", emu.skyline.R.string.right),
|
||||
DpadDown("▼", emu.skyline.R.string.down),
|
||||
LeftStickLeft,
|
||||
LeftStickUp,
|
||||
LeftStickRight,
|
||||
LeftStickDown,
|
||||
RightStickLeft,
|
||||
RightStickUp,
|
||||
RightStickRight,
|
||||
RightStickDown,
|
||||
LeftSL("SL", emu.skyline.R.string.left_shoulder),
|
||||
LeftSR("SR", emu.skyline.R.string.right_shoulder),
|
||||
RightSL("SL", emu.skyline.R.string.left_shoulder),
|
||||
RightSR("SR", emu.skyline.R.string.right_shoulder),
|
||||
Menu("⌂", emu.skyline.R.string.emu_menu_button);
|
||||
|
||||
/**
|
||||
* This returns the value as setting the [ordinal]-th bit in a [Long]
|
||||
*/
|
||||
fun value() : Long {
|
||||
return (1.toLong()) shl ordinal
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This enumerates the states of a button and denotes their Boolean values in [state]
|
||||
*/
|
||||
enum class ButtonState(val state : Boolean) {
|
||||
Released(false),
|
||||
Pressed(true),
|
||||
}
|
||||
|
||||
/**
|
||||
* This enumerates all of the axes on a controller that the emulator recognizes
|
||||
*/
|
||||
enum class AxisId {
|
||||
RX,
|
||||
RY,
|
||||
LX,
|
||||
LY,
|
||||
}
|
||||
|
||||
/**
|
||||
* This enumerates all the sticks on a controller with all their components
|
||||
*
|
||||
* @param xAxis The [AxisId] corresponding to movement on the X-axis for the stick
|
||||
* @param yAxis The [AxisId] corresponding to movement on the Y-axis for the stick
|
||||
* @param button The [ButtonId] of the button activated when the stick is pressed
|
||||
*/
|
||||
enum class StickId(val xAxis : AxisId, val yAxis : AxisId, val button : ButtonId) {
|
||||
Left(AxisId.LX, AxisId.LY, ButtonId.LeftStick),
|
||||
Right(AxisId.RX, AxisId.RY, ButtonId.RightStick);
|
||||
|
||||
override fun toString() = "$name Stick"
|
||||
}
|
||||
|
||||
/**
|
||||
* This an abstract class for all guest events that is inherited by all other event classes
|
||||
*
|
||||
* @param id The ID of the guest controller this event corresponds to
|
||||
*/
|
||||
abstract class GuestEvent(val id : Int) : Serializable {
|
||||
/**
|
||||
* 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 is used for all guest events that correspond to a button
|
||||
*
|
||||
* @param button The ID of the button that this represents
|
||||
* @param threshold The threshold of a corresponding [MotionHostEvent]'s axis value for this to be "pressed"
|
||||
*/
|
||||
class ButtonGuestEvent(id : Int, val button : ButtonId, val threshold : Float = 0f) : GuestEvent(id) {
|
||||
/**
|
||||
* This does some basic equality checking for the type of [other] and all members in the class except [threshold] as that is irrelevant for a lookup
|
||||
*/
|
||||
override fun equals(other : Any?) : Boolean = if (other is ButtonGuestEvent) this.id == other.id && this.button == other.button else false
|
||||
|
||||
/**
|
||||
* This computes the hash for all members of the class except [threshold] as that is irrelevant for a lookup
|
||||
*/
|
||||
override fun hashCode() : Int = Objects.hash(id, button)
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used for all guest events that correspond to a specific pole of an axis
|
||||
*
|
||||
* @param axis The ID of the axis that this represents
|
||||
* @param polarity The polarity of the axis this represents
|
||||
* @param max The maximum recorded value of the corresponding [MotionHostEvent] to scale the axis value
|
||||
*/
|
||||
class AxisGuestEvent(id : Int, val axis : AxisId, val polarity : Boolean, var max : Float = 1f) : GuestEvent(id) {
|
||||
/**
|
||||
* This does some basic equality checking for the type of [other] and all members in the class except [max] as that is irrelevant for a lookup
|
||||
*/
|
||||
override fun equals(other : Any?) : Boolean = if (other is AxisGuestEvent) this.id == other.id && this.axis == other.axis && this.polarity == other.polarity else false
|
||||
|
||||
/**
|
||||
* This computes the hash for all members of the class except [max] as that is irrelevant for a lookup
|
||||
*/
|
||||
override fun hashCode() : Int = Objects.hash(id, axis, polarity)
|
||||
|
||||
/**
|
||||
* This is used to retrieve the scaled value/update the maximum value of this axis
|
||||
*
|
||||
* @param axis The unscaled value of the axis to scale
|
||||
* @return The scaled value of this axis
|
||||
*/
|
||||
fun value(axis : Float) : Float {
|
||||
if (max == 1f) return axis
|
||||
|
||||
val axisAbs = abs(axis)
|
||||
if (axisAbs >= max) {
|
||||
max = axisAbs
|
||||
return 1f
|
||||
}
|
||||
|
||||
return axis + (axis * (1f - max))
|
||||
}
|
||||
}
|
69
app/src/main/java/emu/skyline/input/HostEvent.kt
Normal file
69
app/src/main/java/emu/skyline/input/HostEvent.kt
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
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 {
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
class KeyHostEvent(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)
|
||||
}
|
||||
|
||||
class MotionHostEvent(descriptor : String = "", val axis : Int, val polarity : Boolean) : HostEvent(descriptor) {
|
||||
/**
|
||||
* 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)
|
||||
}
|
108
app/src/main/java/emu/skyline/input/InputManager.kt
Normal file
108
app/src/main/java/emu/skyline/input/InputManager.kt
Normal file
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.input
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import java.io.*
|
||||
|
||||
/**
|
||||
* This class is used to manage all transactions with storing/retrieving data in relation to input
|
||||
*/
|
||||
class InputManager constructor(val context : Context) {
|
||||
/**
|
||||
* The underlying [File] object with the input data
|
||||
*/
|
||||
private val file = File("${context.applicationInfo.dataDir}/input.bin")
|
||||
|
||||
/**
|
||||
* A [HashMap] of all the controllers that contains their metadata
|
||||
*/
|
||||
lateinit var controllers : HashMap<Int, Controller>
|
||||
|
||||
/**
|
||||
* A [HashMap] between all [HostEvent]s and their corresponding [GuestEvent]s
|
||||
*/
|
||||
lateinit var eventMap : HashMap<HostEvent?, GuestEvent?>
|
||||
|
||||
init {
|
||||
var readFile = false
|
||||
|
||||
try {
|
||||
if (file.exists() && file.length() != 0L) {
|
||||
syncObjects()
|
||||
readFile = true
|
||||
}
|
||||
} 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))
|
||||
|
||||
eventMap = hashMapOf()
|
||||
|
||||
syncFile()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function syncs the class with data from [file]
|
||||
*/
|
||||
fun syncObjects() {
|
||||
val fileInput = FileInputStream(file)
|
||||
val objectInput = ObjectInputStream(fileInput)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
controllers = objectInput.readObject() as HashMap<Int, Controller>
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
eventMap = objectInput.readObject() as HashMap<HostEvent?, GuestEvent?>
|
||||
}
|
||||
|
||||
/**
|
||||
* This function syncs [file] with data from the class and eliminates unused value from the map
|
||||
*/
|
||||
fun syncFile() {
|
||||
val fileOutput = FileOutputStream(file)
|
||||
val objectOutput = ObjectOutputStream(fileOutput)
|
||||
|
||||
for (controller in controllers.values) {
|
||||
for (button in ButtonId.values()) {
|
||||
if (button != ButtonId.Menu && !(controller.type.buttons.contains(button) || controller.type.sticks.any { it.button == button })) {
|
||||
val guestEvent = ButtonGuestEvent(controller.id, button)
|
||||
|
||||
eventMap.filterValues { it is ButtonGuestEvent && it == guestEvent }.keys.forEach { eventMap.remove(it) }
|
||||
}
|
||||
}
|
||||
|
||||
for (stick in StickId.values()) {
|
||||
if (!controller.type.sticks.contains(stick)) {
|
||||
for (axis in arrayOf(stick.xAxis, stick.yAxis)) {
|
||||
for (polarity in booleanArrayOf(true, false)) {
|
||||
val guestEvent = AxisGuestEvent(controller.id, axis, polarity)
|
||||
|
||||
eventMap.filterValues { it is AxisGuestEvent && it == guestEvent }.keys.forEach { eventMap.remove(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
objectOutput.writeObject(controllers)
|
||||
objectOutput.writeObject(eventMap)
|
||||
|
||||
objectOutput.flush()
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.input
|
||||
|
||||
/**
|
||||
* This enumerates all buttons on an NPad controller
|
||||
*/
|
||||
enum class NpadButton : ButtonId {
|
||||
A,
|
||||
B,
|
||||
X,
|
||||
Y,
|
||||
LeftStick,
|
||||
RightStick,
|
||||
L,
|
||||
R,
|
||||
ZL,
|
||||
ZR,
|
||||
Plus,
|
||||
Minus,
|
||||
DpadLeft,
|
||||
DpadUp,
|
||||
DpadRight,
|
||||
DpadDown,
|
||||
LeftStickLeft,
|
||||
LeftStickUp,
|
||||
LeftStickRight,
|
||||
LeftStickDown,
|
||||
RightStickLeft,
|
||||
RightStickUp,
|
||||
RightStickRight,
|
||||
RightStickDown,
|
||||
LeftSL,
|
||||
LeftSR,
|
||||
RightSL,
|
||||
RightSR;
|
||||
|
||||
/**
|
||||
* This just returns the value as setting the [ordinal]-th bit in a [Long]
|
||||
*/
|
||||
override fun value() : Long {
|
||||
return (1.toLong()) shl ordinal
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This enumerates all the axis on an NPad controller
|
||||
*/
|
||||
enum class NpadAxis {
|
||||
RX,
|
||||
RY,
|
||||
LX,
|
||||
LY,
|
||||
}
|
240
app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt
Normal file
240
app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt
Normal file
@ -0,0 +1,240 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.input.dialog
|
||||
|
||||
import android.animation.LayoutTransition
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.*
|
||||
import android.view.animation.LinearInterpolator
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.ControllerButtonItem
|
||||
import emu.skyline.input.*
|
||||
import kotlinx.android.synthetic.main.button_dialog.*
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* This dialog is used to set a device to map any buttons
|
||||
*
|
||||
* @param item This is used to hold the [ControllerButtonItem] between instances
|
||||
*/
|
||||
class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment() {
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
*/
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? = inflater.inflate(R.layout.button_dialog, container)
|
||||
|
||||
/**
|
||||
* This expands the bottom sheet so that it's fully visible
|
||||
*/
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
/**
|
||||
* This sets up all user interaction with this dialog
|
||||
*/
|
||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
if (context is ControllerActivity) {
|
||||
val context = requireContext() as ControllerActivity
|
||||
val controller = context.manager.controllers[context.id]!!
|
||||
|
||||
// View focus handling so all input is always directed to this view
|
||||
view?.requestFocus()
|
||||
view?.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
|
||||
|
||||
// Write the text for the button's icon
|
||||
button_text.text = item.button.short ?: item.button.toString()
|
||||
|
||||
// Set up the reset button to clear out all entries corresponding to this button from [InputManager.eventMap]
|
||||
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) }
|
||||
|
||||
item.update()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Ensure that layout animations are proper
|
||||
button_layout.layoutTransition.setAnimateParentHierarchy(false)
|
||||
button_layout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
|
||||
// We want the secondary progress bar to be visible through the first one
|
||||
button_seekbar.progressDrawable.alpha = 128
|
||||
|
||||
var deviceId : Int? = null // The ID of the currently selected device
|
||||
var inputId : Int? = null // The key code/axis ID of the currently selected event
|
||||
|
||||
var axisPolarity = false // The polarity of the axis for the currently selected event
|
||||
var axisRunnable : Runnable? = null // The Runnable that is used for counting down till an axis is selected
|
||||
val axisHandler = Handler(Looper.getMainLooper()) // The handler responsible for handling posting [axisRunnable]
|
||||
|
||||
// The last values of the HAT axes so that they can be ignored in [View.OnGenericMotionListener] so they are passed onto [DialogInterface.OnKeyListener] as [KeyEvent]s
|
||||
var oldDpadX = 0.0f
|
||||
var oldDpadY = 0.0f
|
||||
|
||||
dialog?.setOnKeyListener { _, _, event ->
|
||||
// We want all input events from Joysticks and Buttons except for [KeyEvent.KEYCODE_BACK] as that will should be processed elsewhere
|
||||
if (((event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON) && event.keyCode != KeyEvent.KEYCODE_BACK) || event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK)) && event.repeatCount == 0) {
|
||||
if ((deviceId != event.deviceId || inputId != event.keyCode) && event.action == KeyEvent.ACTION_DOWN) {
|
||||
// We set [deviceId] and [inputId] on [KeyEvent.ACTION_DOWN] alongside updating the views to match the action
|
||||
deviceId = event.deviceId
|
||||
inputId = event.keyCode
|
||||
|
||||
if (axisRunnable != null) {
|
||||
axisHandler.removeCallbacks(axisRunnable!!)
|
||||
axisRunnable = null
|
||||
}
|
||||
|
||||
button_icon.animate().alpha(0.75f).setDuration(50).start()
|
||||
button_text.animate().alpha(0.9f).setDuration(50).start()
|
||||
|
||||
button_title.text = getString(R.string.release_confirm)
|
||||
button_seekbar.visibility = View.GONE
|
||||
} else if (deviceId == event.deviceId && inputId == event.keyCode && event.action == KeyEvent.ACTION_UP) {
|
||||
// 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]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
context.manager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[guestEvent.button]?.update()
|
||||
else if (guestEvent is AxisGuestEvent)
|
||||
context.axisMap[guestEvent.axis]?.update()
|
||||
}
|
||||
|
||||
guestEvent = ButtonGuestEvent(context.id, item.button)
|
||||
|
||||
context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
||||
|
||||
context.manager.eventMap[hostEvent] = guestEvent
|
||||
|
||||
item.update()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
val axes = arrayOf(MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ, MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y, 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 axesHistory = arrayOfNulls<Float>(axes.size) // The last recorded value of an axis, this is used to eliminate any stagnant axes
|
||||
|
||||
view?.setOnGenericMotionListener { _, event ->
|
||||
// We retrieve the value of the HAT axes so that we can check for change and ignore any input from them so it'll be passed onto the [KeyEvent] handler
|
||||
val dpadX = event.getAxisValue(MotionEvent.AXIS_HAT_X)
|
||||
val dpadY = event.getAxisValue(MotionEvent.AXIS_HAT_Y)
|
||||
|
||||
// We want all input events from Joysticks and Buttons that are [MotionEvent.ACTION_MOVE] and not from the D-pad
|
||||
if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE && dpadX == oldDpadX && dpadY == oldDpadY) {
|
||||
// We iterate over every axis to check if any of them pass the selection threshold and if they do then select them by setting [deviceId], [inputId] and [axisPolarity]
|
||||
for (axisItem in axes.withIndex()) {
|
||||
val axis = axisItem.value
|
||||
val value = event.getAxisValue(axis)
|
||||
|
||||
// This checks the history of the axis so it we can ignore any stagnant axis
|
||||
if ((event.historySize == 0 || value == event.getHistoricalAxisValue(axis, 0)) && (axesHistory[axisItem.index]?.let { it == value } != false)) {
|
||||
axesHistory[axisItem.index] = value
|
||||
continue
|
||||
}
|
||||
|
||||
axesHistory[axisItem.index] = value
|
||||
|
||||
if (abs(value) >= 0.5 && (deviceId != event.deviceId || inputId != axis || axisPolarity != (value >= 0)) && !(axes.contains(inputId) && value == event.getAxisValue(inputId!!))) {
|
||||
deviceId = event.deviceId
|
||||
inputId = axis
|
||||
axisPolarity = value >= 0
|
||||
|
||||
button_title.text = getString(R.string.hold_confirm)
|
||||
button_seekbar.visibility = View.VISIBLE
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the currently active input is a valid axis
|
||||
if (axes.contains(inputId)) {
|
||||
val value = event.getAxisValue(inputId!!)
|
||||
val threshold = button_seekbar.progress / 100f
|
||||
|
||||
// Update the secondary progress bar in [button_seekbar] based on the axis's value
|
||||
button_seekbar.secondaryProgress = (abs(value) * 100).toInt()
|
||||
|
||||
// If the axis value crosses the threshold then post [axisRunnable] with a delay and animate the views accordingly
|
||||
if (abs(value) >= threshold) {
|
||||
if (axisRunnable == null) {
|
||||
axisRunnable = Runnable {
|
||||
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
|
||||
|
||||
var guestEvent = context.manager.eventMap[hostEvent]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
context.manager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
|
||||
else if (guestEvent is AxisGuestEvent)
|
||||
context.axisMap[(guestEvent as AxisGuestEvent).axis]?.update()
|
||||
}
|
||||
|
||||
guestEvent = ButtonGuestEvent(controller.id, item.button)
|
||||
|
||||
context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
||||
|
||||
context.manager.eventMap[hostEvent] = guestEvent
|
||||
|
||||
item.update()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
axisHandler.postDelayed(axisRunnable!!, 1000)
|
||||
}
|
||||
|
||||
button_icon.animate().alpha(0.85f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
button_text.animate().alpha(0.95f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
} else {
|
||||
// If the axis value is below the threshold, remove [axisRunnable] from it being posted and animate the views accordingly
|
||||
if (axisRunnable != null) {
|
||||
axisHandler.removeCallbacks(axisRunnable!!)
|
||||
axisRunnable = null
|
||||
}
|
||||
|
||||
button_icon.animate().alpha(0.25f).setDuration(50).start()
|
||||
button_text.animate().alpha(0.35f).setDuration(50).start()
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
oldDpadX = dpadX
|
||||
oldDpadY = dpadY
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
123
app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt
Normal file
123
app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.input.dialog
|
||||
|
||||
import android.animation.LayoutTransition
|
||||
import android.os.Bundle
|
||||
import android.os.VibrationEffect
|
||||
import android.view.*
|
||||
import android.view.animation.LinearInterpolator
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.ControllerGeneralItem
|
||||
import emu.skyline.input.ControllerActivity
|
||||
import kotlinx.android.synthetic.main.rumble_dialog.*
|
||||
|
||||
/**
|
||||
* This dialog is used to set a device to pass on any rumble/force feedback data onto
|
||||
*
|
||||
* @param item This is used to hold the [ControllerGeneralItem] between instances
|
||||
*/
|
||||
class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment() {
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
*/
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? = inflater.inflate(R.layout.rumble_dialog, container)
|
||||
|
||||
/**
|
||||
* This expands the bottom sheet so that it's fully visible
|
||||
*/
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
/**
|
||||
* This sets up all user interaction with this dialog
|
||||
*/
|
||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
if (context is ControllerActivity) {
|
||||
val context = requireContext() as ControllerActivity
|
||||
val controller = context.manager.controllers[context.id]!!
|
||||
|
||||
// Set up the reset button to clear out [Controller.rumbleDevice] when pressed
|
||||
rumble_reset.setOnClickListener {
|
||||
controller.rumbleDevice = null
|
||||
item.update()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Ensure that layout animations are proper
|
||||
rumble_layout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
rumble_controller.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
|
||||
var deviceId : Int? = null // The ID of the currently selected device
|
||||
|
||||
dialog?.setOnKeyListener { _, _, event ->
|
||||
// We want all input events from Joysticks and game pads
|
||||
if (event.isFromSource(InputDevice.SOURCE_GAMEPAD) || event.isFromSource(InputDevice.SOURCE_JOYSTICK)) {
|
||||
if (event.repeatCount == 0 && event.action == KeyEvent.ACTION_DOWN) {
|
||||
val vibrator = event.device.vibrator
|
||||
|
||||
when {
|
||||
// If the device doesn't match the currently selected device then update the UI accordingly and set [deviceId] to the current device
|
||||
deviceId != event.deviceId -> {
|
||||
rumble_controller_name.text = event.device.name
|
||||
|
||||
if (vibrator.hasVibrator()) {
|
||||
rumble_controller_supported.text = getString(R.string.supported)
|
||||
rumble_title.text = getString(R.string.confirm_button_again)
|
||||
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
} else {
|
||||
rumble_controller_supported.text = getString(R.string.not_supported)
|
||||
rumble_title.text = getString(R.string.press_any_button)
|
||||
}
|
||||
|
||||
rumble_controller_icon.animate().apply {
|
||||
interpolator = LinearInterpolator()
|
||||
duration = 100
|
||||
alpha(if (vibrator.hasVibrator()) 0.75f else 0.5f)
|
||||
start()
|
||||
}
|
||||
|
||||
deviceId = event.deviceId
|
||||
}
|
||||
|
||||
// If the currently selected device has a vibrator then go ahead and select it
|
||||
vibrator.hasVibrator() -> {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
|
||||
controller.rumbleDevice = Pair(event.device.descriptor, event.device.name)
|
||||
|
||||
item.update()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// If the currently selected device doesn't have a vibrator then dismiss the dialog entirely
|
||||
else -> {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
605
app/src/main/java/emu/skyline/input/dialog/StickDialog.kt
Normal file
605
app/src/main/java/emu/skyline/input/dialog/StickDialog.kt
Normal file
@ -0,0 +1,605 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.input.dialog
|
||||
|
||||
import android.animation.LayoutTransition
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.view.animation.LinearInterpolator
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.ControllerStickItem
|
||||
import emu.skyline.input.*
|
||||
import kotlinx.android.synthetic.main.stick_dialog.*
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* This dialog is used to set a device to map any sticks
|
||||
*
|
||||
* @param item This is used to hold the [ControllerStickItem] between instances
|
||||
*/
|
||||
class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment() {
|
||||
/**
|
||||
* This enumerates all of the stages this dialog can be in
|
||||
*/
|
||||
private enum class DialogStage(val string : Int) {
|
||||
Button(R.string.stick_button),
|
||||
YPlus(R.string.y_plus),
|
||||
YMinus(R.string.y_minus),
|
||||
XMinus(R.string.x_minus),
|
||||
XPlus(R.string.x_plus),
|
||||
Stick(R.string.stick_preview);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the current stage of the dialog
|
||||
*/
|
||||
private var stage = DialogStage.Button
|
||||
|
||||
/**
|
||||
* This is the handler of all [Runnable]s posted by the dialog
|
||||
*/
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
/**
|
||||
* This is the [Runnable] that is used for running the current stage's animation
|
||||
*/
|
||||
private var stageAnimation : Runnable? = null
|
||||
|
||||
/**
|
||||
* This is a flag that causes any running animation to immediately halt
|
||||
*/
|
||||
private var animationStop = false
|
||||
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
*/
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? {
|
||||
return requireActivity().layoutInflater.inflate(R.layout.stick_dialog, container)
|
||||
}
|
||||
|
||||
/**
|
||||
* This expands the bottom sheet so that it's fully visible
|
||||
*/
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
/**
|
||||
* This function converts [dip] (Density Independent Pixels) to normal pixels
|
||||
*/
|
||||
private fun dipToPixels(dip : Float) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, resources.displayMetrics)
|
||||
|
||||
/**
|
||||
* This function updates the animation based on the current stage and stops the currently running animation if it hasn't already
|
||||
*/
|
||||
@Suppress("LABEL_NAME_CLASH")
|
||||
fun updateAnimation() {
|
||||
animationStop = false
|
||||
stageAnimation?.let { handler.removeCallbacks(it) }
|
||||
|
||||
stick_container?.animate()?.scaleX(1f)?.scaleY(1f)?.alpha(1f)?.translationY(0f)?.translationX(0f)?.rotationX(0f)?.rotationY(0f)?.start()
|
||||
|
||||
when (stage) {
|
||||
DialogStage.Button -> {
|
||||
stageAnimation = Runnable {
|
||||
if (stage != DialogStage.Button || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.scaleX(0.85f)?.scaleY(0.85f)?.alpha(1f)?.withEndAction {
|
||||
if (stage != DialogStage.Button || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
val runnable = Runnable {
|
||||
if (stage != DialogStage.Button || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.scaleX(1f)?.scaleY(1f)?.alpha(0.85f)?.withEndAction {
|
||||
if (stage != DialogStage.Button || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
stageAnimation?.let {
|
||||
handler.postDelayed(it, 750)
|
||||
}
|
||||
}?.start()
|
||||
}
|
||||
|
||||
handler.postDelayed(runnable, 300)
|
||||
}?.start()
|
||||
}
|
||||
}
|
||||
|
||||
DialogStage.YPlus, DialogStage.YMinus -> {
|
||||
val polarity = if (stage == DialogStage.YMinus) 1 else -1
|
||||
|
||||
stageAnimation = Runnable {
|
||||
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.setDuration(300)?.translationY(dipToPixels(15f) * polarity)?.rotationX(27f * polarity)?.alpha(1f)?.withEndAction {
|
||||
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
val runnable = Runnable {
|
||||
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.setDuration(250)?.translationY(0f)?.rotationX(0f)?.alpha(0.85f)?.withEndAction {
|
||||
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
stageAnimation?.let {
|
||||
handler.postDelayed(it, 750)
|
||||
}
|
||||
}?.start()
|
||||
}
|
||||
|
||||
handler.postDelayed(runnable, 300)
|
||||
}?.start()
|
||||
}
|
||||
}
|
||||
|
||||
DialogStage.XPlus, DialogStage.XMinus -> {
|
||||
val polarity = if (stage == DialogStage.XPlus) 1 else -1
|
||||
|
||||
stageAnimation = Runnable {
|
||||
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.setDuration(300)?.translationX(dipToPixels(16.5f) * polarity)?.rotationY(27f * polarity)?.alpha(1f)?.withEndAction {
|
||||
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
val runnable = Runnable {
|
||||
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.setDuration(250)?.translationX(0f)?.rotationY(0f)?.alpha(0.85f)?.withEndAction {
|
||||
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
stageAnimation?.let {
|
||||
handler.postDelayed(it, 750)
|
||||
}
|
||||
}?.start()
|
||||
}
|
||||
|
||||
handler.postDelayed(runnable, 300)
|
||||
}?.start()
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
|
||||
stageAnimation?.let { handler.postDelayed(it, 750) }
|
||||
}
|
||||
|
||||
/**
|
||||
* This function goes to a particular stage based on the offset from the current stage
|
||||
*/
|
||||
private fun gotoStage(offset : Int = 1) {
|
||||
val ordinal = stage.ordinal + offset
|
||||
val size = DialogStage.values().size
|
||||
|
||||
if (ordinal in 0 until size) {
|
||||
stage = DialogStage.values()[ordinal]
|
||||
|
||||
stick_title.text = getString(stage.string)
|
||||
stick_subtitle.text = if (stage != DialogStage.Stick) getString(R.string.use_button_axis) else getString(R.string.use_non_stick)
|
||||
stick_icon.animate().alpha(0.25f).setDuration(50).start()
|
||||
stick_name.animate().alpha(0.35f).setDuration(50).start()
|
||||
stick_seekbar.visibility = View.GONE
|
||||
|
||||
stick_next.text = if (ordinal + 1 == size) getString(R.string.done) else getString(R.string.next)
|
||||
|
||||
updateAnimation()
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This sets up all user interaction with this dialog
|
||||
*/
|
||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
if (context is ControllerActivity) {
|
||||
val context = requireContext() as ControllerActivity
|
||||
val controller = context.manager.controllers[context.id]!!
|
||||
|
||||
// View focus handling so all input is always directed to this view
|
||||
view?.requestFocus()
|
||||
view?.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
|
||||
|
||||
// Write the text for the stick's icon
|
||||
stick_name.text = item.stick.button.short ?: item.stick.button.toString()
|
||||
|
||||
// Set up the reset button to clear out all entries corresponding to this stick from [InputManager.eventMap]
|
||||
stick_reset.setOnClickListener {
|
||||
for (axis in arrayOf(item.stick.xAxis, item.stick.yAxis)) {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
val guestEvent = ButtonGuestEvent(context.id, item.stick.button)
|
||||
|
||||
context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
||||
|
||||
item.update()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Ensure that layout animations are proper
|
||||
stick_layout.layoutTransition.setAnimateParentHierarchy(false)
|
||||
stick_layout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
|
||||
// We want the secondary progress bar to be visible through the first one
|
||||
stick_seekbar.progressDrawable.alpha = 128
|
||||
|
||||
updateAnimation()
|
||||
|
||||
var deviceId : Int? = null // The ID of the currently selected device
|
||||
var inputId : Int? = null // The key code/axis ID of the currently selected event
|
||||
|
||||
val ignoredEvents = mutableListOf<Int>() // The hashes of events that are to be ignored due to being already mapped to some component of the stick
|
||||
|
||||
var axisPolarity = false // The polarity of the axis for the currently selected event
|
||||
var axisRunnable : Runnable? = null // The Runnable that is used for counting down till an axis is selected
|
||||
|
||||
// The last values of the HAT axes so that they can be ignored in [View.OnGenericMotionListener] so they are passed onto [DialogInterface.OnKeyListener] as [KeyEvent]s
|
||||
var oldDpadX = 0.0f
|
||||
var oldDpadY = 0.0f
|
||||
|
||||
stick_next.setOnClickListener {
|
||||
gotoStage(1)
|
||||
|
||||
deviceId = null
|
||||
inputId = null
|
||||
|
||||
axisRunnable?.let { handler.removeCallbacks(it) }
|
||||
}
|
||||
|
||||
view?.setOnKeyListener { _, _, event ->
|
||||
when {
|
||||
// We want all input events from Joysticks and Buttons except for [KeyEvent.KEYCODE_BACK] as that will should be processed elsewhere
|
||||
((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)]) {
|
||||
is ButtonGuestEvent -> {
|
||||
if (guestEvent.button == item.stick.button) {
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(50)?.scaleX(0.85f)?.scaleY(0.85f)?.start()
|
||||
|
||||
stick_icon.animate().alpha(0.85f).setDuration(50).start()
|
||||
stick_name.animate().alpha(0.95f).setDuration(50).start()
|
||||
} else {
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(1f)?.scaleY(1f)?.start()
|
||||
|
||||
stick_icon.animate().alpha(0.25f).setDuration(25).start()
|
||||
stick_name.animate().alpha(0.35f).setDuration(25).start()
|
||||
}
|
||||
} else if (event.action == KeyEvent.ACTION_UP) {
|
||||
stick_next?.callOnClick()
|
||||
}
|
||||
}
|
||||
|
||||
is AxisGuestEvent -> {
|
||||
val coefficient = if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
if (guestEvent.polarity) 1 else -1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
if (guestEvent.axis == item.stick.xAxis) {
|
||||
stick_container?.translationX = dipToPixels(16.5f) * coefficient
|
||||
stick_container?.rotationY = 27f * coefficient
|
||||
} else if (guestEvent.axis == item.stick.yAxis) {
|
||||
stick_container?.translationY = dipToPixels(16.5f) * -coefficient
|
||||
stick_container?.rotationX = 27f * coefficient
|
||||
}
|
||||
}
|
||||
|
||||
null -> if (event.action == KeyEvent.ACTION_UP) stick_next?.callOnClick()
|
||||
}
|
||||
} else if (stage != DialogStage.Stick) {
|
||||
if ((deviceId != event.deviceId || inputId != event.keyCode) && event.action == KeyEvent.ACTION_DOWN && !ignoredEvents.any { it == Objects.hash(event.deviceId, event.keyCode) }) {
|
||||
// We set [deviceId] and [inputId] on [KeyEvent.ACTION_DOWN] alongside updating the views to match the action while ignoring any events in [ignoredEvents]
|
||||
deviceId = event.deviceId
|
||||
inputId = event.keyCode
|
||||
|
||||
if (axisRunnable != null) {
|
||||
handler.removeCallbacks(axisRunnable!!)
|
||||
axisRunnable = null
|
||||
}
|
||||
|
||||
animationStop = true
|
||||
|
||||
val coefficient = if (stage == DialogStage.YMinus || stage == DialogStage.XPlus) 1 else -1
|
||||
|
||||
when (stage) {
|
||||
DialogStage.Button -> stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(0.85f)?.scaleY(0.85f)?.alpha(1f)?.start()
|
||||
DialogStage.YPlus, DialogStage.YMinus -> stick_container?.animate()?.setStartDelay(0)?.setDuration(75)?.translationY(dipToPixels(16.5f) * coefficient)?.rotationX(27f * coefficient)?.alpha(1f)?.start()
|
||||
DialogStage.XPlus, DialogStage.XMinus -> stick_container?.animate()?.setStartDelay(0)?.setDuration(75)?.translationX(dipToPixels(16.5f) * coefficient)?.rotationY(27f * coefficient)?.alpha(1f)?.start()
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
|
||||
stick_icon.animate().alpha(0.85f).setDuration(50).start()
|
||||
stick_name.animate().alpha(0.95f).setDuration(50).start()
|
||||
|
||||
stick_subtitle.text = getString(R.string.release_confirm)
|
||||
stick_seekbar.visibility = View.GONE
|
||||
} else if (deviceId == event.deviceId && inputId == event.keyCode && event.action == KeyEvent.ACTION_UP) {
|
||||
// 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]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
context.manager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[guestEvent.button]?.update()
|
||||
else if (guestEvent is AxisGuestEvent)
|
||||
context.axisMap[guestEvent.axis]?.update()
|
||||
}
|
||||
|
||||
guestEvent = when (stage) {
|
||||
DialogStage.Button -> ButtonGuestEvent(controller.id, item.stick.button)
|
||||
DialogStage.YPlus, DialogStage.YMinus -> AxisGuestEvent(controller.id, item.stick.yAxis, stage == DialogStage.YPlus)
|
||||
DialogStage.XPlus, DialogStage.XMinus -> AxisGuestEvent(controller.id, item.stick.xAxis, stage == DialogStage.XPlus)
|
||||
else -> null
|
||||
}
|
||||
|
||||
context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
||||
|
||||
context.manager.eventMap[hostEvent] = guestEvent
|
||||
|
||||
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!))
|
||||
|
||||
item.update()
|
||||
|
||||
stick_next?.callOnClick()
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP -> {
|
||||
// We handle [KeyEvent.KEYCODE_BACK] by trying to go to the last stage using [gotoStage]
|
||||
gotoStage(-1)
|
||||
|
||||
deviceId = null
|
||||
inputId = null
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 axesHistory = arrayOfNulls<Float>(axes.size) // The last recorded value of an axis, this is used to eliminate any stagnant axes
|
||||
val axesMax = Array(axes.size) { 0f } // The maximum recorded value of the axis, this is to scale the axis to a stick accordingly (The value is also checked at runtime, so it's fine if this isn't the true maximum)
|
||||
|
||||
view?.setOnGenericMotionListener { _, event ->
|
||||
// We retrieve the value of the HAT axes so that we can check for change and ignore any input from them so it'll be passed onto the [KeyEvent] handler
|
||||
val dpadX = event.getAxisValue(MotionEvent.AXIS_HAT_X)
|
||||
val dpadY = event.getAxisValue(MotionEvent.AXIS_HAT_Y)
|
||||
|
||||
// We want all input events from Joysticks and Buttons that are [MotionEvent.ACTION_MOVE] and not from the D-pad
|
||||
if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE && dpadX == oldDpadX && dpadY == oldDpadY) {
|
||||
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
|
||||
for (axisItem in axes.withIndex()) {
|
||||
val axis = axisItem.value
|
||||
var value = event.getAxisValue(axis)
|
||||
|
||||
if ((event.historySize == 0 || value == event.getHistoricalAxisValue(axis, 0)) && (axesHistory[axisItem.index]?.let { it == value } != false)) {
|
||||
axesHistory[axisItem.index] = value
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
when (guestEvent) {
|
||||
is ButtonGuestEvent -> {
|
||||
if (guestEvent.button == item.stick.button) {
|
||||
if (abs(value) >= guestEvent.threshold) {
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(50)?.scaleX(0.85f)?.scaleY(0.85f)?.start()
|
||||
stick_icon.animate().alpha(0.85f).setDuration(50).start()
|
||||
stick_name.animate().alpha(0.95f).setDuration(50).start()
|
||||
} else {
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(1f)?.scaleY(1f)?.start()
|
||||
stick_icon.animate().alpha(0.25f).setDuration(25).start()
|
||||
stick_name.animate().alpha(0.35f).setDuration(25).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is AxisGuestEvent -> {
|
||||
value = guestEvent.value(value)
|
||||
|
||||
val coefficient = if (polarity) abs(value) else -abs(value)
|
||||
|
||||
if (guestEvent.axis == item.stick.xAxis) {
|
||||
stick_container?.translationX = dipToPixels(16.5f) * coefficient
|
||||
stick_container?.rotationY = 27f * coefficient
|
||||
} else if (guestEvent.axis == item.stick.yAxis) {
|
||||
stick_container?.translationY = dipToPixels(16.5f) * coefficient
|
||||
stick_container?.rotationX = 27f * -coefficient
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We iterate over every axis to check if any of them pass the selection threshold and if they do then select them by setting [deviceId], [inputId] and [axisPolarity]
|
||||
for (axisItem in axes.withIndex()) {
|
||||
val axis = axisItem.value
|
||||
val value = event.getAxisValue(axis)
|
||||
|
||||
axesMax[axisItem.index] = max(abs(value), axesMax[axisItem.index])
|
||||
|
||||
// This checks the history of the axis so it we can ignore any stagnant axis
|
||||
if ((event.historySize == 0 || value == event.getHistoricalAxisValue(axis, 0)) && (axesHistory[axisItem.index]?.let { it == value } != false)) {
|
||||
axesHistory[axisItem.index] = value
|
||||
continue
|
||||
}
|
||||
|
||||
axesHistory[axisItem.index] = value
|
||||
|
||||
if (abs(value) >= 0.5 && (deviceId != event.deviceId || inputId != axis || axisPolarity != (value >= 0)) && !ignoredEvents.any { it == Objects.hash(event.deviceId, axis, value >= 0) } && !(axes.contains(inputId) && value == event.getAxisValue(inputId!!))) {
|
||||
deviceId = event.deviceId
|
||||
inputId = axis
|
||||
axisPolarity = value >= 0
|
||||
|
||||
stick_subtitle.text = getString(R.string.hold_confirm)
|
||||
|
||||
if (stage == DialogStage.Button)
|
||||
stick_seekbar.visibility = View.VISIBLE
|
||||
|
||||
animationStop = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the currently active input is a valid axis
|
||||
if (axes.contains(inputId)) {
|
||||
val value = event.getAxisValue(inputId!!)
|
||||
val threshold = if (stage == DialogStage.Button) stick_seekbar.progress / 100f else 0.5f
|
||||
|
||||
when (stage) {
|
||||
// Update the secondary progress bar in [button_seekbar] based on the axis's value
|
||||
DialogStage.Button -> {
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(0.85f)?.scaleY(0.85f)?.alpha(1f)?.start()
|
||||
stick_seekbar.secondaryProgress = (abs(value) * 100).toInt()
|
||||
}
|
||||
|
||||
|
||||
// Update the the position of the stick in the Y-axis based on the axis's value
|
||||
DialogStage.YPlus, DialogStage.YMinus -> {
|
||||
val coefficient = if (stage == DialogStage.YMinus) abs(value) else -abs(value)
|
||||
|
||||
stick_container?.translationY = dipToPixels(16.5f) * coefficient
|
||||
stick_container?.rotationX = 27f * -coefficient
|
||||
}
|
||||
|
||||
// Update the the position of the stick in the X-axis based on the axis's value
|
||||
DialogStage.XPlus, DialogStage.XMinus -> {
|
||||
val coefficient = if (stage == DialogStage.XPlus) abs(value) else -abs(value)
|
||||
|
||||
stick_container?.translationX = dipToPixels(16.5f) * coefficient
|
||||
stick_container?.rotationY = 27f * coefficient
|
||||
}
|
||||
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
|
||||
// If the axis value crosses the threshold then post [axisRunnable] with a delay and animate the views accordingly
|
||||
if (abs(value) >= threshold) {
|
||||
if (axisRunnable == null) {
|
||||
axisRunnable = Runnable {
|
||||
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
|
||||
|
||||
var guestEvent = context.manager.eventMap[hostEvent]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
context.manager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
|
||||
else if (guestEvent is AxisGuestEvent)
|
||||
context.axisMap[(guestEvent as AxisGuestEvent).axis]?.update()
|
||||
}
|
||||
|
||||
val max = axesMax[axes.indexOf(inputId!!)]
|
||||
|
||||
guestEvent = when (stage) {
|
||||
DialogStage.Button -> ButtonGuestEvent(controller.id, item.stick.button, threshold)
|
||||
DialogStage.YPlus, DialogStage.YMinus -> AxisGuestEvent(controller.id, item.stick.yAxis, stage == DialogStage.YPlus, max)
|
||||
DialogStage.XPlus, DialogStage.XMinus -> AxisGuestEvent(controller.id, item.stick.xAxis, stage == DialogStage.XPlus, max)
|
||||
else -> null
|
||||
}
|
||||
|
||||
context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
||||
|
||||
context.manager.eventMap[hostEvent] = guestEvent
|
||||
|
||||
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!, axisPolarity))
|
||||
|
||||
axisRunnable = null
|
||||
|
||||
item.update()
|
||||
|
||||
stick_next?.callOnClick()
|
||||
}
|
||||
|
||||
handler.postDelayed(axisRunnable!!, 1000)
|
||||
}
|
||||
|
||||
stick_icon.animate().alpha(0.85f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
stick_name.animate().alpha(0.95f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
} else {
|
||||
// If the axis value is below the threshold, remove [axisRunnable] from it being posted and animate the views accordingly
|
||||
if (axisRunnable != null) {
|
||||
handler.removeCallbacks(axisRunnable!!)
|
||||
axisRunnable = null
|
||||
}
|
||||
|
||||
if (stage == DialogStage.Button)
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(10)?.scaleX(1f)?.scaleY(1f)?.alpha(1f)?.start()
|
||||
|
||||
stick_icon.animate().alpha(0.25f).setDuration(50).start()
|
||||
stick_name.animate().alpha(0.35f).setDuration(50).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
oldDpadX = dpadX
|
||||
oldDpadY = dpadY
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.preference
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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
|
||||
|
||||
/**
|
||||
* This preference is used to launch [ControllerActivity] using a preference
|
||||
*/
|
||||
class ControllerPreference : Preference {
|
||||
/**
|
||||
* The index of the controller this preference manages
|
||||
*/
|
||||
private var index : Int = -1
|
||||
|
||||
constructor(context : Context?, attrs : AttributeSet?, defStyleAttr : Int) : super(context, attrs, defStyleAttr) {
|
||||
for (i in 0 until attrs!!.attributeCount) {
|
||||
val attr = attrs.getAttributeName(i)
|
||||
|
||||
if (attr.equals("index", ignoreCase = true)) {
|
||||
index = attrs.getAttributeValue(i).toInt()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (index == -1)
|
||||
throw IllegalArgumentException()
|
||||
|
||||
if (key == null)
|
||||
key = "controller_$index"
|
||||
|
||||
title = "${context?.getString(R.string.config_controller)} #${index + 1}"
|
||||
|
||||
if (context is SettingsActivity)
|
||||
summaryProvider = SummaryProvider<ControllerPreference> { _ -> context.inputManager.controllers[index]?.type?.stringRes?.let { context.getString(it) } }
|
||||
}
|
||||
|
||||
constructor(context : Context?, attrs : AttributeSet?) : this(context, attrs, R.attr.preferenceStyle)
|
||||
|
||||
constructor(context : Context?) : this(context, null)
|
||||
|
||||
/**
|
||||
* This launches [ControllerActivity] on click to configure the controller
|
||||
*/
|
||||
override fun onClick() {
|
||||
if (context is SettingsActivity)
|
||||
(context as SettingsActivity).refreshKey = key
|
||||
|
||||
val intent = Intent(context, ControllerActivity::class.java)
|
||||
intent.putExtra("index", index)
|
||||
(context as Activity).startActivityForResult(intent, 0)
|
||||
}
|
||||
}
|
@ -11,7 +11,9 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.SummaryProvider
|
||||
import androidx.preference.R
|
||||
import emu.skyline.SettingsActivity
|
||||
|
||||
/**
|
||||
* This preference shows the decoded URI of it's preference and launches [FolderActivity]
|
||||
@ -23,7 +25,10 @@ class FolderPreference : Preference {
|
||||
private var mDirectory : String? = null
|
||||
|
||||
constructor(context : Context?, attrs : AttributeSet?, defStyleAttr : Int) : super(context, attrs, defStyleAttr) {
|
||||
summaryProvider = SimpleSummaryProvider()
|
||||
summaryProvider = SummaryProvider<FolderPreference> { preference ->
|
||||
preference.onSetInitialValue(null)
|
||||
Uri.decode(preference.mDirectory) ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
constructor(context : Context?, attrs : AttributeSet?) : this(context, attrs, R.attr.preferenceStyle)
|
||||
@ -34,6 +39,9 @@ class FolderPreference : Preference {
|
||||
* This launches [FolderActivity] on click to change the directory
|
||||
*/
|
||||
override fun onClick() {
|
||||
if (context is SettingsActivity)
|
||||
(context as SettingsActivity).refreshKey = key
|
||||
|
||||
val intent = Intent(context, FolderActivity::class.java)
|
||||
(context as Activity).startActivityForResult(intent, 0)
|
||||
}
|
||||
@ -44,16 +52,4 @@ class FolderPreference : Preference {
|
||||
override fun onSetInitialValue(defaultValue : Any?) {
|
||||
mDirectory = getPersistedString(defaultValue as String?)
|
||||
}
|
||||
|
||||
/**
|
||||
* This [Preference.SummaryProvider] is used to set the summary for URI values
|
||||
*/
|
||||
private class SimpleSummaryProvider : SummaryProvider<FolderPreference> {
|
||||
/**
|
||||
* This returns the decoded URI of the directory as the summary
|
||||
*/
|
||||
override fun provideSummary(preference : FolderPreference) : CharSequence {
|
||||
return Uri.decode(preference.mDirectory) ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
9
app/src/main/res/drawable-night/ic_controller.xml
Normal file
9
app/src/main/res/drawable-night/ic_controller.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M15,7.5V2H9v5.5l3,3 3,-3zM7.5,9H2v6h5.5l3,-3 -3,-3zM9,16.5V22h6v-5.5l-3,-3 -3,3zM16.5,9l-3,3 3,3H22V9h-5.5z" />
|
||||
</vector>
|
8
app/src/main/res/drawable/ic_button.xml
Normal file
8
app/src/main/res/drawable/ic_button.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#A0000000" />
|
||||
<stroke
|
||||
android:width="2.5dp"
|
||||
android:color="@android:color/black" />
|
||||
</shape>
|
9
app/src/main/res/drawable/ic_controller.xml
Normal file
9
app/src/main/res/drawable/ic_controller.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15,7.5V2H9v5.5l3,3 3,-3zM7.5,9H2v6h5.5l3,-3 -3,-3zM9,16.5V22h6v-5.5l-3,-3 -3,3zM16.5,9l-3,3 3,3H22V9h-5.5z" />
|
||||
</vector>
|
32
app/src/main/res/drawable/ic_stick.xml
Normal file
32
app/src/main/res/drawable/ic_stick.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="#A0000000" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@android:color/black" />
|
||||
<size
|
||||
android:width="25dp"
|
||||
android:height="25dp" />
|
||||
<padding
|
||||
android:bottom="10dp"
|
||||
android:left="10dp"
|
||||
android:right="10dp"
|
||||
android:top="10dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<!--
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="#B0FFFFFF" />
|
||||
-->
|
||||
<solid android:color="#50000000" />
|
||||
<size
|
||||
android:width="30dp"
|
||||
android:height="30dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
8
app/src/main/res/drawable/ic_stick_circle.xml
Normal file
8
app/src/main/res/drawable/ic_stick_circle.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#A0000000" />
|
||||
<stroke
|
||||
android:width="2.5dp"
|
||||
android:color="@android:color/black" />
|
||||
</shape>
|
80
app/src/main/res/layout/button_dialog.xml
Normal file
80
app/src/main/res/layout/button_dialog.xml
Normal file
@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/button_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:animateLayoutChanges="true"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:focusedByDefault="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/button_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:animateLayoutChanges="true"
|
||||
android:text="@string/use_button_axis"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/button_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/button_icon"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:alpha="0.15"
|
||||
android:contentDescription="@string/buttons"
|
||||
android:outlineProvider="bounds"
|
||||
android:src="@drawable/ic_button" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/button_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignStart="@id/button_icon"
|
||||
android:layout_alignTop="@id/button_icon"
|
||||
android:layout_alignEnd="@id/button_icon"
|
||||
android:layout_alignBottom="@id/button_icon"
|
||||
android:alpha="0.25"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="27sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/button_seekbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:max="100"
|
||||
android:progress="25"
|
||||
android:secondaryProgressTint="@color/colorPrimaryDark"
|
||||
android:secondaryProgressTintMode="screen"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_reset"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/reset" />
|
||||
|
||||
</LinearLayout>
|
17
app/src/main/res/layout/controller_activity.xml
Normal file
17
app/src/main/res/layout/controller_activity.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<include layout="@layout/titlebar" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/controller_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
30
app/src/main/res/layout/controller_item.xml
Normal file
30
app/src/main/res/layout/controller_item.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/controller_item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="15dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="false"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
tools:ignore="RelativeOverlap" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/text_title"
|
||||
android:layout_alignStart="@id/text_title"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||
android:textColor="@android:color/tertiary_text_light" />
|
||||
</RelativeLayout>
|
75
app/src/main/res/layout/rumble_dialog.xml
Normal file
75
app/src/main/res/layout/rumble_dialog.xml
Normal file
@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/rumble_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:animateLayoutChanges="true"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:focusedByDefault="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rumble_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:animateLayoutChanges="true"
|
||||
android:text="@string/press_any_button"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/rumble_controller"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/rumble_controller_icon"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:alpha="0.25"
|
||||
android:contentDescription="@string/controller"
|
||||
android:src="@drawable/ic_controller" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rumble_controller_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@id/rumble_controller_icon"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_toEndOf="@id/rumble_controller_icon"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rumble_controller_supported"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/rumble_controller_name"
|
||||
android:layout_alignStart="@id/rumble_controller_name"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||
android:textColor="@android:color/tertiary_text_light"
|
||||
android:textSize="13sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/rumble_reset"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/reset" />
|
||||
|
||||
</LinearLayout>
|
125
app/src/main/res/layout/stick_dialog.xml
Normal file
125
app/src/main/res/layout/stick_dialog.xml
Normal file
@ -0,0 +1,125 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/stick_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:animateLayoutChanges="true"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:focusedByDefault="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stick_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:text="@string/stick_button"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stick_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:animateLayoutChanges="true"
|
||||
android:text="@string/use_button_axis"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/stick_circle_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/stick_circle_icon"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:alpha="0.4"
|
||||
android:contentDescription="@string/buttons"
|
||||
android:outlineProvider="bounds"
|
||||
android:src="@drawable/ic_stick_circle" />
|
||||
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/stick_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignStart="@id/stick_circle_icon"
|
||||
android:layout_alignTop="@id/stick_circle_icon"
|
||||
android:layout_alignEnd="@id/stick_circle_icon"
|
||||
android:layout_alignBottom="@id/stick_circle_icon">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/stick_icon"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_margin="15dp"
|
||||
android:alpha="0.4"
|
||||
android:contentDescription="@string/buttons"
|
||||
android:outlineProvider="bounds"
|
||||
android:src="@drawable/ic_stick" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stick_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignStart="@id/stick_icon"
|
||||
android:layout_alignTop="@id/stick_icon"
|
||||
android:layout_alignEnd="@id/stick_icon"
|
||||
android:layout_alignBottom="@id/stick_icon"
|
||||
android:alpha="0.55"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="27sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/stick_seekbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:max="100"
|
||||
android:progress="25"
|
||||
android:secondaryProgressTint="@color/colorPrimaryDark"
|
||||
android:secondaryProgressTintMode="screen"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end">
|
||||
|
||||
<Button
|
||||
android:id="@+id/stick_reset"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/reset" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/stick_next"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/next" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
@ -1,4 +1,4 @@
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name">Skyline</string>
|
||||
<!-- Common -->
|
||||
<string name="search">Search</string>
|
||||
@ -24,10 +24,14 @@
|
||||
<!-- Settings -->
|
||||
<string name="emulator">Emulator</string>
|
||||
<string name="search_location">Search Location</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="layout_type">Game Display Layout</string>
|
||||
<string name="select_action">Always Show Game Information</string>
|
||||
<string name="select_action_desc_on">Game information will be shown on clicking a game</string>
|
||||
<string name="select_action_desc_off">Game information will only be shown on long-clicking a game</string>
|
||||
<string name="perf_stats">Show Performance Statistics</string>
|
||||
<string name="perf_stats_desc_off">Performance Statistics will not be shown</string>
|
||||
<string name="perf_stats_desc_on">Performance Statistics will be shown in the top-left corner</string>
|
||||
<string name="log_level">Log Level</string>
|
||||
<string name="log_compact">Compact Logs</string>
|
||||
<string name="log_compact_desc_on">Logs will be displayed in a compact form factor</string>
|
||||
@ -36,9 +40,64 @@
|
||||
<string name="use_docked">Use Docked Mode</string>
|
||||
<string name="handheld_enabled">The system will emulate being in handheld mode</string>
|
||||
<string name="docked_enabled">The system will emulate being in docked mode</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="username">Username</string>
|
||||
<string name="username_default">@string/app_name</string>
|
||||
<!-- Input -->
|
||||
<string name="input">Input</string>
|
||||
<string name="show_osc">Show On-Screen Controls</string>
|
||||
<string name="osc_not_shown">On-Screen Controls won\'t be shown</string>
|
||||
<string name="osc_shown">On-Screen Controls will be shown</string>
|
||||
<string name="controller">Controller</string>
|
||||
<string name="config_controller">Configure Controller</string>
|
||||
<string name="controller_type">Controller Type</string>
|
||||
<string name="none">None</string>
|
||||
<string name="handheld_procon">Handheld + Pro Controller</string>
|
||||
<string name="procon">Pro Controller</string>
|
||||
<string name="ljoycon">Left JoyCon</string>
|
||||
<string name="rjoycon">Right JoyCon</string>
|
||||
<string name="general">General</string>
|
||||
<string name="partner_joycon">Partner Joy-Con</string>
|
||||
<string name="rumble_device">Rumble Device</string>
|
||||
<string name="supported">Supported</string>
|
||||
<string name="not_supported">Not Supported</string>
|
||||
<string name="press_any_button">Press any button on a controller</string>
|
||||
<string name="confirm_button_again">Confirm choice by pressing a button again</string>
|
||||
<string name="reset">Reset</string>
|
||||
<string name="buttons">Buttons</string>
|
||||
<string name="use_button_axis">Use any button or axis on a controller</string>
|
||||
<string name="release_confirm">Release to confirm selection</string>
|
||||
<string name="hold_confirm">Hold to confirm selection</string>
|
||||
<string name="sticks">Sticks</string>
|
||||
<string name="stick_button">Stick Button</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="x_plus">Stick X+ Axis (Right)</string>
|
||||
<string name="y_plus">Stick Y+ Axis (Up)</string>
|
||||
<string name="x_minus">Stick X- Axis (Left)</string>
|
||||
<string name="y_minus">Stick Y- Axis (Down)</string>
|
||||
<string name="a_button">A</string>
|
||||
<string name="b_button">B</string>
|
||||
<string name="x_button">X</string>
|
||||
<string name="y_button">Y</string>
|
||||
<string name="left_shoulder">Left Shoulder</string>
|
||||
<string name="right_shoulder">Right Shoulder</string>
|
||||
<string name="left_trigger">Left Trigger</string>
|
||||
<string name="right_trigger">Right Trigger</string>
|
||||
<string name="plus_button">Plus</string>
|
||||
<string name="minus_button">Minus</string>
|
||||
<string name="emu_menu_button">Emulator Menu</string>
|
||||
<string name="stick_preview">Stick Preview</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="use_non_stick">Use any unmapped button to finish</string>
|
||||
<string name="button">Button</string>
|
||||
<string name="up">Up</string>
|
||||
<string name="down">Down</string>
|
||||
<string name="left">Left</string>
|
||||
<string name="right">Right</string>
|
||||
<string name="dpad">D-pad</string>
|
||||
<string name="face_buttons">Face Buttons</string>
|
||||
<string name="shoulder_trigger"><![CDATA[Shoulder & Trigger Buttons]]></string>
|
||||
<string name="shoulder_rail">Shoulder Buttons on Joy-Con Rail</string>
|
||||
<string name="misc_buttons">Miscellaneous Buttons</string>
|
||||
<!-- Licenses -->
|
||||
<string name="licenses">Licenses</string>
|
||||
<string name="skyline_license_description">The license of Skyline (MPL 2.0)</string>
|
||||
@ -66,7 +125,7 @@
|
||||
<string name="roboto_description">We use Roboto as our FOSS shared font replacement for Nintendo\'s extended character set (Apache License 2.0)</string>
|
||||
<string name="source_sans_pro">Source Sans Pro</string>
|
||||
<string name="source_sans_pro_description">We use Source Sans Pro as our FOSS shared font replacement for Extended Chinese (SIL Open Font License 1.1)</string>
|
||||
<string name="perf_stats">Show Performance Statistics</string>
|
||||
<string name="perf_stats_desc_off">Performance Statistics will not be shown</string>
|
||||
<string name="perf_stats_desc_on">Performance Statistics will be shown in the top-left corner</string>
|
||||
<!-- Misc -->
|
||||
<!--suppress AndroidLintUnusedResources -->
|
||||
<string name="expand_button_title" tools:override="true">Expand</string>
|
||||
</resources>
|
||||
|
@ -9,6 +9,11 @@
|
||||
<item name="colorOnSecondary">@color/colorOnSecondary</item>
|
||||
</style>
|
||||
|
||||
<style name="EmuTheme" parent="AppTheme">
|
||||
<item name="android:windowTranslucentStatus">true</item>
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.ActionBar" parent="">
|
||||
<item name="android:textColorPrimary">@color/colorOnPrimary</item>
|
||||
<item name="android:textColorSecondary">@color/colorOnPrimary</item>
|
||||
|
@ -1,19 +1,3 @@
|
||||
<!--
|
||||
~ Copyright 2018 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<PreferenceCategory
|
||||
@ -21,8 +5,7 @@
|
||||
android:title="@string/emulator">
|
||||
<emu.skyline.preference.FolderPreference
|
||||
app:key="search_location"
|
||||
app:title="@string/search_location"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
app:title="@string/search_location" />
|
||||
<emu.skyline.preference.ThemePreference
|
||||
android:defaultValue="2"
|
||||
android:entries="@array/app_theme"
|
||||
@ -78,6 +61,25 @@
|
||||
app:key="operation_mode"
|
||||
app:title="@string/use_docked" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:key="category_input"
|
||||
android:title="@string/input"
|
||||
app:initialExpandedChildrenCount="4">
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="true"
|
||||
android:summaryOff="@string/osc_not_shown"
|
||||
android:summaryOn="@string/osc_shown"
|
||||
app:key="show_osc"
|
||||
app:title="@string/show_osc" />
|
||||
<emu.skyline.preference.ControllerPreference index="0" />
|
||||
<emu.skyline.preference.ControllerPreference index="1" />
|
||||
<emu.skyline.preference.ControllerPreference index="2" />
|
||||
<emu.skyline.preference.ControllerPreference index="3" />
|
||||
<emu.skyline.preference.ControllerPreference index="4" />
|
||||
<emu.skyline.preference.ControllerPreference index="5" />
|
||||
<emu.skyline.preference.ControllerPreference index="6" />
|
||||
<emu.skyline.preference.ControllerPreference index="7" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:key="category_licenses"
|
||||
android:title="@string/licenses"
|
||||
|
Loading…
Reference in New Issue
Block a user