mirror of https://git.suyu.dev/suyu/suyu
Merge pull request #13034 from t895/map-all-the-inputs
android: Input mappingmerge-requests/60/head
commit
e7146309de
@ -0,0 +1,416 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.input
|
||||||
|
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.NativeButton
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.InputType
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.ButtonName
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
import org.yuzu.yuzu_emu.utils.ParamPackage
|
||||||
|
import android.view.InputDevice
|
||||||
|
|
||||||
|
object NativeInput {
|
||||||
|
/**
|
||||||
|
* Default controller id for each device
|
||||||
|
*/
|
||||||
|
const val Player1Device = 0
|
||||||
|
const val Player2Device = 1
|
||||||
|
const val Player3Device = 2
|
||||||
|
const val Player4Device = 3
|
||||||
|
const val Player5Device = 4
|
||||||
|
const val Player6Device = 5
|
||||||
|
const val Player7Device = 6
|
||||||
|
const val Player8Device = 7
|
||||||
|
const val ConsoleDevice = 8
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button states
|
||||||
|
*/
|
||||||
|
object ButtonState {
|
||||||
|
const val RELEASED = 0
|
||||||
|
const val PRESSED = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if pro controller isn't available and handheld is.
|
||||||
|
* Intended to check where the input overlay should direct its inputs.
|
||||||
|
*/
|
||||||
|
external fun isHandheldOnly(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles button press events for a gamepad.
|
||||||
|
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
|
||||||
|
* @param port Port determined by controller connection order.
|
||||||
|
* @param buttonId The Android Keycode corresponding to this event.
|
||||||
|
* @param action Mask identifying which action is happening (button pressed down, or button released).
|
||||||
|
*/
|
||||||
|
external fun onGamePadButtonEvent(
|
||||||
|
guid: String,
|
||||||
|
port: Int,
|
||||||
|
buttonId: Int,
|
||||||
|
action: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles axis movement events.
|
||||||
|
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
|
||||||
|
* @param port Port determined by controller connection order.
|
||||||
|
* @param axis The axis ID.
|
||||||
|
* @param value Value along the given axis.
|
||||||
|
*/
|
||||||
|
external fun onGamePadAxisEvent(guid: String, port: Int, axis: Int, value: Float)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles motion events.
|
||||||
|
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
|
||||||
|
* @param port Port determined by controller connection order.
|
||||||
|
* @param deltaTimestamp The finger id corresponding to this event.
|
||||||
|
* @param xGyro The value of the x-axis for the gyroscope.
|
||||||
|
* @param yGyro The value of the y-axis for the gyroscope.
|
||||||
|
* @param zGyro The value of the z-axis for the gyroscope.
|
||||||
|
* @param xAccel The value of the x-axis for the accelerometer.
|
||||||
|
* @param yAccel The value of the y-axis for the accelerometer.
|
||||||
|
* @param zAccel The value of the z-axis for the accelerometer.
|
||||||
|
*/
|
||||||
|
external fun onGamePadMotionEvent(
|
||||||
|
guid: String,
|
||||||
|
port: Int,
|
||||||
|
deltaTimestamp: Long,
|
||||||
|
xGyro: Float,
|
||||||
|
yGyro: Float,
|
||||||
|
zGyro: Float,
|
||||||
|
xAccel: Float,
|
||||||
|
yAccel: Float,
|
||||||
|
zAccel: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals and load a nfc tag
|
||||||
|
* @param data Byte array containing all the data from a nfc tag.
|
||||||
|
*/
|
||||||
|
external fun onReadNfcTag(data: ByteArray?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes current loaded nfc tag.
|
||||||
|
*/
|
||||||
|
external fun onRemoveNfcTag()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles touch press events.
|
||||||
|
* @param fingerId The finger id corresponding to this event.
|
||||||
|
* @param xAxis The value of the x-axis on the touchscreen.
|
||||||
|
* @param yAxis The value of the y-axis on the touchscreen.
|
||||||
|
*/
|
||||||
|
external fun onTouchPressed(fingerId: Int, xAxis: Float, yAxis: Float)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles touch movement.
|
||||||
|
* @param fingerId The finger id corresponding to this event.
|
||||||
|
* @param xAxis The value of the x-axis on the touchscreen.
|
||||||
|
* @param yAxis The value of the y-axis on the touchscreen.
|
||||||
|
*/
|
||||||
|
external fun onTouchMoved(fingerId: Int, xAxis: Float, yAxis: Float)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles touch release events.
|
||||||
|
* @param fingerId The finger id corresponding to this event
|
||||||
|
*/
|
||||||
|
external fun onTouchReleased(fingerId: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a button input to the global virtual controllers.
|
||||||
|
* @param port Port determined by controller connection order.
|
||||||
|
* @param button The [NativeButton] corresponding to this event.
|
||||||
|
* @param action Mask identifying which action is happening (button pressed down, or button released).
|
||||||
|
*/
|
||||||
|
fun onOverlayButtonEvent(port: Int, button: NativeButton, action: Int) =
|
||||||
|
onOverlayButtonEventImpl(port, button.int, action)
|
||||||
|
|
||||||
|
private external fun onOverlayButtonEventImpl(port: Int, buttonId: Int, action: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a joystick input to the global virtual controllers.
|
||||||
|
* @param port Port determined by controller connection order.
|
||||||
|
* @param stick The [NativeAnalog] corresponding to this event.
|
||||||
|
* @param xAxis Value along the X axis.
|
||||||
|
* @param yAxis Value along the Y axis.
|
||||||
|
*/
|
||||||
|
fun onOverlayJoystickEvent(port: Int, stick: NativeAnalog, xAxis: Float, yAxis: Float) =
|
||||||
|
onOverlayJoystickEventImpl(port, stick.int, xAxis, yAxis)
|
||||||
|
|
||||||
|
private external fun onOverlayJoystickEventImpl(
|
||||||
|
port: Int,
|
||||||
|
stickId: Int,
|
||||||
|
xAxis: Float,
|
||||||
|
yAxis: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles motion events for the global virtual controllers.
|
||||||
|
* @param port Port determined by controller connection order
|
||||||
|
* @param deltaTimestamp The finger id corresponding to this event.
|
||||||
|
* @param xGyro The value of the x-axis for the gyroscope.
|
||||||
|
* @param yGyro The value of the y-axis for the gyroscope.
|
||||||
|
* @param zGyro The value of the z-axis for the gyroscope.
|
||||||
|
* @param xAccel The value of the x-axis for the accelerometer.
|
||||||
|
* @param yAccel The value of the y-axis for the accelerometer.
|
||||||
|
* @param zAccel The value of the z-axis for the accelerometer.
|
||||||
|
*/
|
||||||
|
external fun onDeviceMotionEvent(
|
||||||
|
port: Int,
|
||||||
|
deltaTimestamp: Long,
|
||||||
|
xGyro: Float,
|
||||||
|
yGyro: Float,
|
||||||
|
zGyro: Float,
|
||||||
|
xAccel: Float,
|
||||||
|
yAccel: Float,
|
||||||
|
zAccel: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads all input devices from the currently loaded Settings::values.players into HID Core
|
||||||
|
*/
|
||||||
|
external fun reloadInputDevices()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a controller to be used with mapping
|
||||||
|
* @param device An [InputDevice] or the input overlay wrapped with [YuzuInputDevice]
|
||||||
|
*/
|
||||||
|
external fun registerController(device: YuzuInputDevice)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the names of input devices that have been registered with the input subsystem via [registerController]
|
||||||
|
*/
|
||||||
|
external fun getInputDevices(): Array<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads all input profiles from disk. Must be called before creating a profile picker.
|
||||||
|
*/
|
||||||
|
external fun loadInputProfiles()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the names of each available input profile.
|
||||||
|
*/
|
||||||
|
external fun getInputProfileNames(): Array<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user-provided name for an input profile is valid.
|
||||||
|
* @param name User-provided name for an input profile.
|
||||||
|
* @return Whether [name] is valid or not.
|
||||||
|
*/
|
||||||
|
external fun isProfileNameValid(name: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new input profile.
|
||||||
|
* @param name The new profile's name.
|
||||||
|
* @param playerIndex Index of the player that's currently being edited. Used to write the profile
|
||||||
|
* name to this player's config.
|
||||||
|
* @return Whether creating the profile was successful or not.
|
||||||
|
*/
|
||||||
|
external fun createProfile(name: String, playerIndex: Int): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an input profile.
|
||||||
|
* @param name Name of the profile to delete.
|
||||||
|
* @param playerIndex Index of the player that's currently being edited. Used to remove the profile
|
||||||
|
* name from this player's config if they have it loaded.
|
||||||
|
* @return Whether deleting this profile was successful or not.
|
||||||
|
*/
|
||||||
|
external fun deleteProfile(name: String, playerIndex: Int): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an input profile.
|
||||||
|
* @param name Name of the input profile to load.
|
||||||
|
* @param playerIndex Index of the player that will have this profile loaded.
|
||||||
|
* @return Whether loading this profile was successful or not.
|
||||||
|
*/
|
||||||
|
external fun loadProfile(name: String, playerIndex: Int): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves an input profile.
|
||||||
|
* @param name Name of the profile to save.
|
||||||
|
* @param playerIndex Index of the player that's currently being edited. Used to write the profile
|
||||||
|
* name to this player's config.
|
||||||
|
* @return Whether saving the profile was successful or not.
|
||||||
|
*/
|
||||||
|
external fun saveProfile(name: String, playerIndex: Int): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intended to be used immediately before a call to [NativeConfig.saveControlPlayerValues]
|
||||||
|
* Must be used while per-game config is loaded.
|
||||||
|
*/
|
||||||
|
external fun loadPerGameConfiguration(
|
||||||
|
playerIndex: Int,
|
||||||
|
selectedIndex: Int,
|
||||||
|
selectedProfileName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells the input subsystem to start listening for inputs to map.
|
||||||
|
* @param type Type of input to map as shown by the int property in each [InputType].
|
||||||
|
*/
|
||||||
|
external fun beginMapping(type: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an input's [ParamPackage] as a serialized string. Used for input verification before mapping.
|
||||||
|
* Must be run after [beginMapping] and before [stopMapping].
|
||||||
|
*/
|
||||||
|
external fun getNextInput(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells the input subsystem to stop listening for inputs to map.
|
||||||
|
*/
|
||||||
|
external fun stopMapping()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a controller's mappings with auto-mapping params.
|
||||||
|
* @param playerIndex Index of the player to auto-map.
|
||||||
|
* @param deviceParams [ParamPackage] representing the device to auto-map as received
|
||||||
|
* from [getInputDevices].
|
||||||
|
* @param displayName Name of the device to auto-map as received from the "display" param in [deviceParams].
|
||||||
|
* Intended to be a way to provide a default name for a controller if the "display" param is empty.
|
||||||
|
*/
|
||||||
|
fun updateMappingsWithDefault(
|
||||||
|
playerIndex: Int,
|
||||||
|
deviceParams: ParamPackage,
|
||||||
|
displayName: String
|
||||||
|
) = updateMappingsWithDefaultImpl(playerIndex, deviceParams.serialize(), displayName)
|
||||||
|
|
||||||
|
private external fun updateMappingsWithDefaultImpl(
|
||||||
|
playerIndex: Int,
|
||||||
|
deviceParams: String,
|
||||||
|
displayName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the params for a specific button.
|
||||||
|
* @param playerIndex Index of the player to get params from.
|
||||||
|
* @param button The [NativeButton] to get params for.
|
||||||
|
* @return A [ParamPackage] representing a player's specific button.
|
||||||
|
*/
|
||||||
|
fun getButtonParam(playerIndex: Int, button: NativeButton): ParamPackage =
|
||||||
|
ParamPackage(getButtonParamImpl(playerIndex, button.int))
|
||||||
|
|
||||||
|
private external fun getButtonParamImpl(playerIndex: Int, buttonId: Int): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the params for a specific button.
|
||||||
|
* @param playerIndex Index of the player to set params for.
|
||||||
|
* @param button The [NativeButton] to set params for.
|
||||||
|
* @param param A [ParamPackage] to set.
|
||||||
|
*/
|
||||||
|
fun setButtonParam(playerIndex: Int, button: NativeButton, param: ParamPackage) =
|
||||||
|
setButtonParamImpl(playerIndex, button.int, param.serialize())
|
||||||
|
|
||||||
|
private external fun setButtonParamImpl(playerIndex: Int, buttonId: Int, param: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the params for a specific stick.
|
||||||
|
* @param playerIndex Index of the player to get params from.
|
||||||
|
* @param stick The [NativeAnalog] to get params for.
|
||||||
|
* @return A [ParamPackage] representing a player's specific stick.
|
||||||
|
*/
|
||||||
|
fun getStickParam(playerIndex: Int, stick: NativeAnalog): ParamPackage =
|
||||||
|
ParamPackage(getStickParamImpl(playerIndex, stick.int))
|
||||||
|
|
||||||
|
private external fun getStickParamImpl(playerIndex: Int, stickId: Int): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the params for a specific stick.
|
||||||
|
* @param playerIndex Index of the player to set params for.
|
||||||
|
* @param stick The [NativeAnalog] to set params for.
|
||||||
|
* @param param A [ParamPackage] to set.
|
||||||
|
*/
|
||||||
|
fun setStickParam(playerIndex: Int, stick: NativeAnalog, param: ParamPackage) =
|
||||||
|
setStickParamImpl(playerIndex, stick.int, param.serialize())
|
||||||
|
|
||||||
|
private external fun setStickParamImpl(playerIndex: Int, stickId: Int, param: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the int representation of a [ButtonName]. Tells you what to show as the mapped input for
|
||||||
|
* a button/analog/other.
|
||||||
|
* @param param A [ParamPackage] that represents a specific button's params.
|
||||||
|
* @return The [ButtonName] for [param].
|
||||||
|
*/
|
||||||
|
fun getButtonName(param: ParamPackage): ButtonName =
|
||||||
|
ButtonName.from(getButtonNameImpl(param.serialize()))
|
||||||
|
|
||||||
|
private external fun getButtonNameImpl(param: String): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets each supported [NpadStyleIndex] for a given player.
|
||||||
|
* @param playerIndex Index of the player to get supported indexes for.
|
||||||
|
* @return List of each supported [NpadStyleIndex].
|
||||||
|
*/
|
||||||
|
fun getSupportedStyleTags(playerIndex: Int): List<NpadStyleIndex> =
|
||||||
|
getSupportedStyleTagsImpl(playerIndex).map { NpadStyleIndex.from(it) }
|
||||||
|
|
||||||
|
private external fun getSupportedStyleTagsImpl(playerIndex: Int): IntArray
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the [NpadStyleIndex] for a given player.
|
||||||
|
* @param playerIndex Index of the player to get an [NpadStyleIndex] from.
|
||||||
|
* @return The [NpadStyleIndex] for a given player.
|
||||||
|
*/
|
||||||
|
fun getStyleIndex(playerIndex: Int): NpadStyleIndex =
|
||||||
|
NpadStyleIndex.from(getStyleIndexImpl(playerIndex))
|
||||||
|
|
||||||
|
private external fun getStyleIndexImpl(playerIndex: Int): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the [NpadStyleIndex] for a given player.
|
||||||
|
* @param playerIndex Index of the player to change.
|
||||||
|
* @param style The new style to set.
|
||||||
|
*/
|
||||||
|
fun setStyleIndex(playerIndex: Int, style: NpadStyleIndex) =
|
||||||
|
setStyleIndexImpl(playerIndex, style.int)
|
||||||
|
|
||||||
|
private external fun setStyleIndexImpl(playerIndex: Int, styleIndex: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a device is a controller.
|
||||||
|
* @param params [ParamPackage] for an input device retrieved from [getInputDevices]
|
||||||
|
* @return Whether the device is a controller or not.
|
||||||
|
*/
|
||||||
|
fun isController(params: ParamPackage): Boolean = isControllerImpl(params.serialize())
|
||||||
|
|
||||||
|
private external fun isControllerImpl(params: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a controller is connected
|
||||||
|
* @param playerIndex Index of the player to check.
|
||||||
|
* @return Whether the player is connected or not.
|
||||||
|
*/
|
||||||
|
external fun getIsConnected(playerIndex: Int): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects/disconnects a controller and ensures that connection order stays in-tact.
|
||||||
|
* @param playerIndex Index of the player to connect/disconnect.
|
||||||
|
* @param connected Whether to connect or disconnect this controller.
|
||||||
|
*/
|
||||||
|
fun connectControllers(playerIndex: Int, connected: Boolean = true) {
|
||||||
|
val connectedControllers = mutableListOf<Boolean>().apply {
|
||||||
|
if (connected) {
|
||||||
|
for (i in 0 until 8) {
|
||||||
|
add(i <= playerIndex)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (i in 0 until 8) {
|
||||||
|
add(i < playerIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectControllersImpl(connectedControllers.toBooleanArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private external fun connectControllersImpl(connected: BooleanArray)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all of the button and analog mappings for a player.
|
||||||
|
* @param playerIndex Index of the player that will have its mappings reset.
|
||||||
|
*/
|
||||||
|
external fun resetControllerMappings(playerIndex: Int)
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.input
|
||||||
|
|
||||||
|
import android.view.InputDevice
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.utils.InputHandler.getGUID
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
interface YuzuInputDevice {
|
||||||
|
fun getName(): String
|
||||||
|
|
||||||
|
fun getGUID(): String
|
||||||
|
|
||||||
|
fun getPort(): Int
|
||||||
|
|
||||||
|
fun getSupportsVibration(): Boolean
|
||||||
|
|
||||||
|
fun vibrate(intensity: Float)
|
||||||
|
|
||||||
|
fun getAxes(): Array<Int> = arrayOf()
|
||||||
|
fun hasKeys(keys: IntArray): BooleanArray = BooleanArray(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
class YuzuPhysicalDevice(
|
||||||
|
private val device: InputDevice,
|
||||||
|
private val port: Int,
|
||||||
|
useSystemVibrator: Boolean
|
||||||
|
) : YuzuInputDevice {
|
||||||
|
private val vibrator = if (useSystemVibrator) {
|
||||||
|
YuzuVibrator.getSystemVibrator()
|
||||||
|
} else {
|
||||||
|
YuzuVibrator.getControllerVibrator(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return device.name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getGUID(): String {
|
||||||
|
return device.getGUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPort(): Int {
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSupportsVibration(): Boolean {
|
||||||
|
return vibrator.supportsVibration()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun vibrate(intensity: Float) {
|
||||||
|
vibrator.vibrate(intensity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAxes(): Array<Int> = device.motionRanges.map { it.axis }.toTypedArray()
|
||||||
|
override fun hasKeys(keys: IntArray): BooleanArray = device.hasKeys(*keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
class YuzuInputOverlayDevice(
|
||||||
|
private val vibration: Boolean,
|
||||||
|
private val port: Int
|
||||||
|
) : YuzuInputDevice {
|
||||||
|
private val vibrator = YuzuVibrator.getSystemVibrator()
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return YuzuApplication.appContext.getString(R.string.input_overlay)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getGUID(): String {
|
||||||
|
return "00000000000000000000000000000000"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPort(): Int {
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSupportsVibration(): Boolean {
|
||||||
|
if (vibration) {
|
||||||
|
return vibrator.supportsVibration()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun vibrate(intensity: Float) {
|
||||||
|
if (vibration) {
|
||||||
|
vibrator.vibrate(intensity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.input
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.CombinedVibration
|
||||||
|
import android.os.VibrationEffect
|
||||||
|
import android.os.Vibrator
|
||||||
|
import android.os.VibratorManager
|
||||||
|
import android.view.InputDevice
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
interface YuzuVibrator {
|
||||||
|
fun supportsVibration(): Boolean
|
||||||
|
|
||||||
|
fun vibrate(intensity: Float)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getControllerVibrator(device: InputDevice): YuzuVibrator =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
YuzuVibratorManager(device.vibratorManager)
|
||||||
|
} else {
|
||||||
|
YuzuVibratorManagerCompat(device.vibrator)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSystemVibrator(): YuzuVibrator =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val vibratorManager = YuzuApplication.appContext
|
||||||
|
.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||||
|
YuzuVibratorManager(vibratorManager)
|
||||||
|
} else {
|
||||||
|
val vibrator = YuzuApplication.appContext
|
||||||
|
.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||||
|
YuzuVibratorManagerCompat(vibrator)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getVibrationEffect(intensity: Float): VibrationEffect? {
|
||||||
|
if (intensity > 0f) {
|
||||||
|
return VibrationEffect.createOneShot(
|
||||||
|
50,
|
||||||
|
(255.0 * intensity).toInt().coerceIn(1, 255)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
|
class YuzuVibratorManager(private val vibratorManager: VibratorManager) : YuzuVibrator {
|
||||||
|
override fun supportsVibration(): Boolean {
|
||||||
|
return vibratorManager.vibratorIds.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun vibrate(intensity: Float) {
|
||||||
|
val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return
|
||||||
|
vibratorManager.vibrate(CombinedVibration.createParallel(vibration))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class YuzuVibratorManagerCompat(private val vibrator: Vibrator) : YuzuVibrator {
|
||||||
|
override fun supportsVibration(): Boolean {
|
||||||
|
return vibrator.hasVibrator()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun vibrate(intensity: Float) {
|
||||||
|
val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return
|
||||||
|
vibrator.vibrate(vibration)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.input.model
|
||||||
|
|
||||||
|
enum class AnalogDirection(val int: Int, val param: String) {
|
||||||
|
Up(0, "up"),
|
||||||
|
Down(1, "down"),
|
||||||
|
Left(2, "left"),
|
||||||
|
Right(3, "right")
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.input.model
|
||||||
|
|
||||||
|
// Loosely matches the enum in common/input.h
|
||||||
|
enum class ButtonName(val int: Int) {
|
||||||
|
Invalid(1),
|
||||||
|
|
||||||
|
// This will display the engine name instead of the button name
|
||||||
|
Engine(2),
|
||||||
|
|
||||||
|
// This will display the button by value instead of the button name
|
||||||
|
Value(3);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(int: Int): ButtonName = entries.firstOrNull { it.int == int } ?: Invalid
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.input.model
|
||||||
|
|
||||||
|
// Must match the corresponding enum in input_common/main.h
|
||||||
|
enum class InputType(val int: Int) {
|
||||||
|
None(0),
|
||||||
|
Button(1),
|
||||||
|
Stick(2),
|
||||||
|
Motion(3),
|
||||||
|
Touch(4)
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.input.model
|
||||||
|
|
||||||
|
// Must match enum in src/common/settings_input.h
|
||||||
|
enum class NativeAnalog(val int: Int) {
|
||||||
|
LStick(0),
|
||||||
|
RStick(1);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(int: Int): NativeAnalog = entries.firstOrNull { it.int == int } ?: LStick
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.input.model
|
||||||
|
|
||||||
|
// Must match enum in src/common/settings_input.h
|
||||||
|
enum class NativeButton(val int: Int) {
|
||||||
|
A(0),
|
||||||
|
B(1),
|
||||||
|
X(2),
|
||||||
|
Y(3),
|
||||||
|
LStick(4),
|
||||||
|
RStick(5),
|
||||||
|
L(6),
|
||||||
|
R(7),
|
||||||
|
ZL(8),
|
||||||
|
ZR(9),
|
||||||
|
Plus(10),
|
||||||
|
Minus(11),
|
||||||
|
|
||||||
|
DLeft(12),
|
||||||
|
DUp(13),
|
||||||
|
DRight(14),
|
||||||
|
DDown(15),
|
||||||
|
|
||||||
|
SLLeft(16),
|
||||||
|
SRLeft(17),
|
||||||
|
|
||||||
|
Home(18),
|
||||||
|
Capture(19),
|
||||||
|
|
||||||
|
SLRight(20),
|
||||||
|
SRRight(21);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(int: Int): NativeButton = entries.firstOrNull { it.int == int } ?: A
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.input.model
|
||||||
|
|
||||||
|
// Must match enum in src/common/settings_input.h
|
||||||
|
enum class NativeTrigger(val int: Int) {
|
||||||
|
LTrigger(0),
|
||||||
|
RTrigger(1)
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.input.model
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
|
||||||
|
// Must match enum in src/core/hid/hid_types.h
|
||||||
|
enum class NpadStyleIndex(val int: Int, @StringRes val nameId: Int = 0) {
|
||||||
|
None(0),
|
||||||
|
Fullkey(3, R.string.pro_controller),
|
||||||
|
Handheld(4, R.string.handheld),
|
||||||
|
HandheldNES(4),
|
||||||
|
JoyconDual(5, R.string.dual_joycons),
|
||||||
|
JoyconLeft(6, R.string.left_joycon),
|
||||||
|
JoyconRight(7, R.string.right_joycon),
|
||||||
|
GameCube(8, R.string.gamecube_controller),
|
||||||
|
Pokeball(9),
|
||||||
|
NES(10),
|
||||||
|
SNES(12),
|
||||||
|
N64(13),
|
||||||
|
SegaGenesis(14),
|
||||||
|
SystemExt(32),
|
||||||
|
System(33);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(int: Int): NpadStyleIndex = entries.firstOrNull { it.int == int } ?: None
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.input.model
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class PlayerInput(
|
||||||
|
var connected: Boolean,
|
||||||
|
var buttons: Array<String>,
|
||||||
|
var analogs: Array<String>,
|
||||||
|
var motions: Array<String>,
|
||||||
|
|
||||||
|
var vibrationEnabled: Boolean,
|
||||||
|
var vibrationStrength: Int,
|
||||||
|
|
||||||
|
var bodyColorLeft: Long,
|
||||||
|
var bodyColorRight: Long,
|
||||||
|
var buttonColorLeft: Long,
|
||||||
|
var buttonColorRight: Long,
|
||||||
|
var profileName: String,
|
||||||
|
|
||||||
|
var useSystemVibrator: Boolean
|
||||||
|
) {
|
||||||
|
// It's recommended to use the generated equals() and hashCode() methods
|
||||||
|
// when using arrays in a data class
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as PlayerInput
|
||||||
|
|
||||||
|
if (connected != other.connected) return false
|
||||||
|
if (!buttons.contentEquals(other.buttons)) return false
|
||||||
|
if (!analogs.contentEquals(other.analogs)) return false
|
||||||
|
if (!motions.contentEquals(other.motions)) return false
|
||||||
|
if (vibrationEnabled != other.vibrationEnabled) return false
|
||||||
|
if (vibrationStrength != other.vibrationStrength) return false
|
||||||
|
if (bodyColorLeft != other.bodyColorLeft) return false
|
||||||
|
if (bodyColorRight != other.bodyColorRight) return false
|
||||||
|
if (buttonColorLeft != other.buttonColorLeft) return false
|
||||||
|
if (buttonColorRight != other.buttonColorRight) return false
|
||||||
|
if (profileName != other.profileName) return false
|
||||||
|
return useSystemVibrator == other.useSystemVibrator
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = connected.hashCode()
|
||||||
|
result = 31 * result + buttons.contentHashCode()
|
||||||
|
result = 31 * result + analogs.contentHashCode()
|
||||||
|
result = 31 * result + motions.contentHashCode()
|
||||||
|
result = 31 * result + vibrationEnabled.hashCode()
|
||||||
|
result = 31 * result + vibrationStrength
|
||||||
|
result = 31 * result + bodyColorLeft.hashCode()
|
||||||
|
result = 31 * result + bodyColorRight.hashCode()
|
||||||
|
result = 31 * result + buttonColorLeft.hashCode()
|
||||||
|
result = 31 * result + buttonColorRight.hashCode()
|
||||||
|
result = 31 * result + profileName.hashCode()
|
||||||
|
result = 31 * result + useSystemVibrator.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasMapping(): Boolean {
|
||||||
|
var hasMapping = false
|
||||||
|
buttons.forEach {
|
||||||
|
if (it != "[empty]") {
|
||||||
|
hasMapping = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
analogs.forEach {
|
||||||
|
if (it != "[empty]") {
|
||||||
|
hasMapping = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
motions.forEach {
|
||||||
|
if (it != "[empty]") {
|
||||||
|
hasMapping = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasMapping
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.model.view
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.yuzu.yuzu_emu.features.input.NativeInput
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.InputType
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
|
||||||
|
import org.yuzu.yuzu_emu.utils.ParamPackage
|
||||||
|
|
||||||
|
class AnalogInputSetting(
|
||||||
|
override val playerIndex: Int,
|
||||||
|
val nativeAnalog: NativeAnalog,
|
||||||
|
val analogDirection: AnalogDirection,
|
||||||
|
@StringRes titleId: Int = 0,
|
||||||
|
titleString: String = ""
|
||||||
|
) : InputSetting(titleId, titleString) {
|
||||||
|
override val type = TYPE_INPUT
|
||||||
|
override val inputType = InputType.Stick
|
||||||
|
|
||||||
|
override fun getSelectedValue(): String {
|
||||||
|
val params = NativeInput.getStickParam(playerIndex, nativeAnalog)
|
||||||
|
val analog = analogToText(params, analogDirection.param)
|
||||||
|
return getDisplayString(params, analog)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSelectedValue(param: ParamPackage) =
|
||||||
|
NativeInput.setStickParam(playerIndex, nativeAnalog, param)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.model.view
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.yuzu.yuzu_emu.utils.ParamPackage
|
||||||
|
import org.yuzu.yuzu_emu.features.input.NativeInput
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.InputType
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.NativeButton
|
||||||
|
|
||||||
|
class ButtonInputSetting(
|
||||||
|
override val playerIndex: Int,
|
||||||
|
val nativeButton: NativeButton,
|
||||||
|
@StringRes titleId: Int = 0,
|
||||||
|
titleString: String = ""
|
||||||
|
) : InputSetting(titleId, titleString) {
|
||||||
|
override val type = TYPE_INPUT
|
||||||
|
override val inputType = InputType.Button
|
||||||
|
|
||||||
|
override fun getSelectedValue(): String {
|
||||||
|
val params = NativeInput.getButtonParam(playerIndex, nativeButton)
|
||||||
|
val button = buttonToText(params)
|
||||||
|
return getDisplayString(params, button)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSelectedValue(param: ParamPackage) =
|
||||||
|
NativeInput.setButtonParam(playerIndex, nativeButton, param)
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.model.view
|
||||||
|
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.features.input.NativeInput
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
|
class InputProfileSetting(private val playerIndex: Int) :
|
||||||
|
SettingsItem(emptySetting, R.string.profile, "", 0, "") {
|
||||||
|
override val type = TYPE_INPUT_PROFILE
|
||||||
|
|
||||||
|
fun getCurrentProfile(): String =
|
||||||
|
NativeConfig.getInputSettings(true)[playerIndex].profileName
|
||||||
|
|
||||||
|
fun getProfileNames(): Array<String> = NativeInput.getInputProfileNames()
|
||||||
|
|
||||||
|
fun isProfileNameValid(name: String): Boolean = NativeInput.isProfileNameValid(name)
|
||||||
|
|
||||||
|
fun createProfile(name: String): Boolean = NativeInput.createProfile(name, playerIndex)
|
||||||
|
|
||||||
|
fun deleteProfile(name: String): Boolean = NativeInput.deleteProfile(name, playerIndex)
|
||||||
|
|
||||||
|
fun loadProfile(name: String): Boolean {
|
||||||
|
val result = NativeInput.loadProfile(name, playerIndex)
|
||||||
|
NativeInput.reloadInputDevices()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveProfile(name: String): Boolean = NativeInput.saveProfile(name, playerIndex)
|
||||||
|
}
|
@ -0,0 +1,134 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.model.view
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.features.input.NativeInput
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.ButtonName
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.InputType
|
||||||
|
import org.yuzu.yuzu_emu.utils.ParamPackage
|
||||||
|
|
||||||
|
sealed class InputSetting(
|
||||||
|
@StringRes titleId: Int,
|
||||||
|
titleString: String
|
||||||
|
) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
|
||||||
|
override val type = TYPE_INPUT
|
||||||
|
abstract val inputType: InputType
|
||||||
|
abstract val playerIndex: Int
|
||||||
|
|
||||||
|
protected val context get() = YuzuApplication.appContext
|
||||||
|
|
||||||
|
abstract fun getSelectedValue(): String
|
||||||
|
|
||||||
|
abstract fun setSelectedValue(param: ParamPackage)
|
||||||
|
|
||||||
|
protected fun getDisplayString(params: ParamPackage, control: String): String {
|
||||||
|
val deviceName = params.get("display", "")
|
||||||
|
deviceName.ifEmpty {
|
||||||
|
return context.getString(R.string.not_set)
|
||||||
|
}
|
||||||
|
return "$deviceName: $control"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDirectionName(direction: String): String =
|
||||||
|
when (direction) {
|
||||||
|
"up" -> context.getString(R.string.up)
|
||||||
|
"down" -> context.getString(R.string.down)
|
||||||
|
"left" -> context.getString(R.string.left)
|
||||||
|
"right" -> context.getString(R.string.right)
|
||||||
|
else -> direction
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun buttonToText(param: ParamPackage): String {
|
||||||
|
if (!param.has("engine")) {
|
||||||
|
return context.getString(R.string.not_set)
|
||||||
|
}
|
||||||
|
|
||||||
|
val toggle = if (param.get("toggle", false)) "~" else ""
|
||||||
|
val inverted = if (param.get("inverted", false)) "!" else ""
|
||||||
|
val invert = if (param.get("invert", "+") == "-") "-" else ""
|
||||||
|
val turbo = if (param.get("turbo", false)) "$" else ""
|
||||||
|
val commonButtonName = NativeInput.getButtonName(param)
|
||||||
|
|
||||||
|
if (commonButtonName == ButtonName.Invalid) {
|
||||||
|
return context.getString(R.string.invalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commonButtonName == ButtonName.Engine) {
|
||||||
|
return param.get("engine", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commonButtonName == ButtonName.Value) {
|
||||||
|
if (param.has("hat")) {
|
||||||
|
val hat = getDirectionName(param.get("direction", ""))
|
||||||
|
return context.getString(R.string.qualified_hat, turbo, toggle, inverted, hat)
|
||||||
|
}
|
||||||
|
if (param.has("axis")) {
|
||||||
|
val axis = param.get("axis", "")
|
||||||
|
return context.getString(
|
||||||
|
R.string.qualified_button_stick_axis,
|
||||||
|
toggle,
|
||||||
|
inverted,
|
||||||
|
invert,
|
||||||
|
axis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (param.has("button")) {
|
||||||
|
val button = param.get("button", "")
|
||||||
|
return context.getString(R.string.qualified_button, turbo, toggle, inverted, button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.getString(R.string.unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun analogToText(param: ParamPackage, direction: String): String {
|
||||||
|
if (!param.has("engine")) {
|
||||||
|
return context.getString(R.string.not_set)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param.get("engine", "") == "analog_from_button") {
|
||||||
|
return buttonToText(ParamPackage(param.get(direction, "")))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!param.has("axis_x") || !param.has("axis_y")) {
|
||||||
|
return context.getString(R.string.unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
val xAxis = param.get("axis_x", "")
|
||||||
|
val yAxis = param.get("axis_y", "")
|
||||||
|
val xInvert = param.get("invert_x", "+") == "-"
|
||||||
|
val yInvert = param.get("invert_y", "+") == "-"
|
||||||
|
|
||||||
|
if (direction == "modifier") {
|
||||||
|
return context.getString(R.string.unused)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (direction) {
|
||||||
|
"up" -> {
|
||||||
|
val yInvertString = if (yInvert) "+" else "-"
|
||||||
|
return context.getString(R.string.qualified_axis, yAxis, yInvertString)
|
||||||
|
}
|
||||||
|
|
||||||
|
"down" -> {
|
||||||
|
val yInvertString = if (yInvert) "-" else "+"
|
||||||
|
return context.getString(R.string.qualified_axis, yAxis, yInvertString)
|
||||||
|
}
|
||||||
|
|
||||||
|
"left" -> {
|
||||||
|
val xInvertString = if (xInvert) "+" else "-"
|
||||||
|
return context.getString(R.string.qualified_axis, xAxis, xInvertString)
|
||||||
|
}
|
||||||
|
|
||||||
|
"right" -> {
|
||||||
|
val xInvertString = if (xInvert) "-" else "+"
|
||||||
|
return context.getString(R.string.qualified_axis, xAxis, xInvertString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.getString(R.string.unknown)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.model.view
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
|
||||||
|
|
||||||
|
class IntSingleChoiceSetting(
|
||||||
|
private val intSetting: AbstractIntSetting,
|
||||||
|
@StringRes titleId: Int = 0,
|
||||||
|
titleString: String = "",
|
||||||
|
@StringRes descriptionId: Int = 0,
|
||||||
|
descriptionString: String = "",
|
||||||
|
val choices: Array<String>,
|
||||||
|
val values: Array<Int>
|
||||||
|
) : SettingsItem(intSetting, titleId, titleString, descriptionId, descriptionString) {
|
||||||
|
override val type = TYPE_INT_SINGLE_CHOICE
|
||||||
|
|
||||||
|
fun getValueAt(index: Int): Int =
|
||||||
|
if (values.indices.contains(index)) values[index] else -1
|
||||||
|
|
||||||
|
fun getChoiceAt(index: Int): String =
|
||||||
|
if (choices.indices.contains(index)) choices[index] else ""
|
||||||
|
|
||||||
|
fun getSelectedValue(needsGlobal: Boolean = false) = intSetting.getInt(needsGlobal)
|
||||||
|
fun setSelectedValue(value: Int) = intSetting.setInt(value)
|
||||||
|
|
||||||
|
val selectedValueIndex: Int
|
||||||
|
get() {
|
||||||
|
for (i in values.indices) {
|
||||||
|
if (values[i] == getSelectedValue()) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.model.view
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.yuzu.yuzu_emu.features.input.NativeInput
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.InputType
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
|
||||||
|
import org.yuzu.yuzu_emu.utils.ParamPackage
|
||||||
|
|
||||||
|
class ModifierInputSetting(
|
||||||
|
override val playerIndex: Int,
|
||||||
|
val nativeAnalog: NativeAnalog,
|
||||||
|
@StringRes titleId: Int = 0,
|
||||||
|
titleString: String = ""
|
||||||
|
) : InputSetting(titleId, titleString) {
|
||||||
|
override val inputType = InputType.Button
|
||||||
|
|
||||||
|
override fun getSelectedValue(): String {
|
||||||
|
val analogParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
|
||||||
|
val modifierParam = ParamPackage(analogParam.get("modifier", ""))
|
||||||
|
return buttonToText(modifierParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSelectedValue(param: ParamPackage) {
|
||||||
|
val newParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
|
||||||
|
newParam.set("modifier", param.serialize())
|
||||||
|
NativeInput.setStickParam(playerIndex, nativeAnalog, newParam)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,300 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.ui
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.graphics.drawable.Animatable2
|
||||||
|
import android.graphics.drawable.AnimatedVectorDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.InputDevice
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.databinding.DialogMappingBinding
|
||||||
|
import org.yuzu.yuzu_emu.features.input.NativeInput
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
|
||||||
|
import org.yuzu.yuzu_emu.features.input.model.NativeButton
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting
|
||||||
|
import org.yuzu.yuzu_emu.utils.InputHandler
|
||||||
|
import org.yuzu.yuzu_emu.utils.ParamPackage
|
||||||
|
|
||||||
|
class InputDialogFragment : DialogFragment() {
|
||||||
|
private var inputAccepted = false
|
||||||
|
|
||||||
|
private var position: Int = 0
|
||||||
|
|
||||||
|
private lateinit var inputSetting: InputSetting
|
||||||
|
|
||||||
|
private lateinit var binding: DialogMappingBinding
|
||||||
|
|
||||||
|
private val settingsViewModel: SettingsViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if (settingsViewModel.clickedItem == null) dismiss()
|
||||||
|
|
||||||
|
position = requireArguments().getInt(POSITION)
|
||||||
|
|
||||||
|
InputHandler.updateControllerData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
inputSetting = settingsViewModel.clickedItem as InputSetting
|
||||||
|
binding = DialogMappingBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setPositiveButton(android.R.string.cancel) { _, _ ->
|
||||||
|
NativeInput.stopMapping()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.setView(binding.root)
|
||||||
|
|
||||||
|
val playButtonMapAnimation = { twoDirections: Boolean ->
|
||||||
|
val stickAnimation: AnimatedVectorDrawable
|
||||||
|
val buttonAnimation: AnimatedVectorDrawable
|
||||||
|
binding.imageStickAnimation.apply {
|
||||||
|
val anim = if (twoDirections) {
|
||||||
|
R.drawable.stick_two_direction_anim
|
||||||
|
} else {
|
||||||
|
R.drawable.stick_one_direction_anim
|
||||||
|
}
|
||||||
|
setBackgroundResource(anim)
|
||||||
|
stickAnimation = background as AnimatedVectorDrawable
|
||||||
|
}
|
||||||
|
binding.imageButtonAnimation.apply {
|
||||||
|
setBackgroundResource(R.drawable.button_anim)
|
||||||
|
buttonAnimation = background as AnimatedVectorDrawable
|
||||||
|
}
|
||||||
|
stickAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
|
||||||
|
override fun onAnimationEnd(drawable: Drawable?) {
|
||||||
|
buttonAnimation.start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
buttonAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
|
||||||
|
override fun onAnimationEnd(drawable: Drawable?) {
|
||||||
|
stickAnimation.start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
stickAnimation.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val setting = inputSetting) {
|
||||||
|
is AnalogInputSetting -> {
|
||||||
|
when (setting.nativeAnalog) {
|
||||||
|
NativeAnalog.LStick -> builder.setTitle(
|
||||||
|
getString(R.string.map_control, getString(R.string.left_stick))
|
||||||
|
)
|
||||||
|
|
||||||
|
NativeAnalog.RStick -> builder.setTitle(
|
||||||
|
getString(R.string.map_control, getString(R.string.right_stick))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setMessage(R.string.stick_map_description)
|
||||||
|
|
||||||
|
playButtonMapAnimation.invoke(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ModifierInputSetting -> {
|
||||||
|
builder.setTitle(getString(R.string.map_control, setting.title))
|
||||||
|
.setMessage(R.string.button_map_description)
|
||||||
|
playButtonMapAnimation.invoke(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ButtonInputSetting -> {
|
||||||
|
if (setting.nativeButton == NativeButton.DUp ||
|
||||||
|
setting.nativeButton == NativeButton.DDown ||
|
||||||
|
setting.nativeButton == NativeButton.DLeft ||
|
||||||
|
setting.nativeButton == NativeButton.DRight
|
||||||
|
) {
|
||||||
|
builder.setTitle(getString(R.string.map_dpad_direction, setting.title))
|
||||||
|
} else {
|
||||||
|
builder.setTitle(getString(R.string.map_control, setting.title))
|
||||||
|
}
|
||||||
|
builder.setMessage(R.string.button_map_description)
|
||||||
|
playButtonMapAnimation.invoke(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
view.requestFocus()
|
||||||
|
view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
|
||||||
|
dialog?.setOnKeyListener { _, _, keyEvent -> onKeyEvent(keyEvent) }
|
||||||
|
binding.root.setOnGenericMotionListener { _, motionEvent -> onMotionEvent(motionEvent) }
|
||||||
|
NativeInput.beginMapping(inputSetting.inputType.int)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
|
||||||
|
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val action = when (event.action) {
|
||||||
|
KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
|
||||||
|
KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
val controllerData =
|
||||||
|
InputHandler.androidControllers[event.device.controllerNumber] ?: return false
|
||||||
|
NativeInput.onGamePadButtonEvent(
|
||||||
|
controllerData.getGUID(),
|
||||||
|
controllerData.getPort(),
|
||||||
|
event.keyCode,
|
||||||
|
action
|
||||||
|
)
|
||||||
|
onInputReceived(event.device)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onMotionEvent(event: MotionEvent): Boolean {
|
||||||
|
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
|
||||||
|
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temp workaround for DPads that give both axis and button input. The input system can't
|
||||||
|
// take in a specific axis direction for a binding so you lose half of the directions for a DPad.
|
||||||
|
|
||||||
|
val controllerData =
|
||||||
|
InputHandler.androidControllers[event.device.controllerNumber] ?: return false
|
||||||
|
event.device.motionRanges.forEach {
|
||||||
|
NativeInput.onGamePadAxisEvent(
|
||||||
|
controllerData.getGUID(),
|
||||||
|
controllerData.getPort(),
|
||||||
|
it.axis,
|
||||||
|
event.getAxisValue(it.axis)
|
||||||
|
)
|
||||||
|
onInputReceived(event.device)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onInputReceived(device: InputDevice) {
|
||||||
|
val params = ParamPackage(NativeInput.getNextInput())
|
||||||
|
if (params.has("engine") && isInputAcceptable(params) && !inputAccepted) {
|
||||||
|
inputAccepted = true
|
||||||
|
setResult(params, device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setResult(params: ParamPackage, device: InputDevice) {
|
||||||
|
NativeInput.stopMapping()
|
||||||
|
params.set("display", "${device.name} ${params.get("port", 0)}")
|
||||||
|
when (val item = settingsViewModel.clickedItem as InputSetting) {
|
||||||
|
is ModifierInputSetting,
|
||||||
|
is ButtonInputSetting -> {
|
||||||
|
// Invert DPad up and left bindings by default
|
||||||
|
val tempSetting = inputSetting as? ButtonInputSetting
|
||||||
|
if (tempSetting != null) {
|
||||||
|
if (tempSetting.nativeButton == NativeButton.DUp ||
|
||||||
|
tempSetting.nativeButton == NativeButton.DLeft &&
|
||||||
|
params.has("axis")
|
||||||
|
) {
|
||||||
|
params.set("invert", "-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item.setSelectedValue(params)
|
||||||
|
settingsViewModel.setAdapterItemChanged(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AnalogInputSetting -> {
|
||||||
|
var analogParam = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
|
||||||
|
analogParam = adjustAnalogParam(params, analogParam, item.analogDirection.param)
|
||||||
|
|
||||||
|
// Invert Y-Axis by default
|
||||||
|
analogParam.set("invert_y", "-")
|
||||||
|
|
||||||
|
item.setSelectedValue(analogParam)
|
||||||
|
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustAnalogParam(
|
||||||
|
inputParam: ParamPackage,
|
||||||
|
analogParam: ParamPackage,
|
||||||
|
buttonName: String
|
||||||
|
): ParamPackage {
|
||||||
|
// The poller returned a complete axis, so set all the buttons
|
||||||
|
if (inputParam.has("axis_x") && inputParam.has("axis_y")) {
|
||||||
|
return inputParam
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the current configuration has either no engine or an axis binding.
|
||||||
|
// Clears out the old binding and adds one with analog_from_button.
|
||||||
|
if (!analogParam.has("engine") || analogParam.has("axis_x") || analogParam.has("axis_y")) {
|
||||||
|
analogParam.clear()
|
||||||
|
analogParam.set("engine", "analog_from_button")
|
||||||
|
}
|
||||||
|
analogParam.set(buttonName, inputParam.serialize())
|
||||||
|
return analogParam
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isInputAcceptable(params: ParamPackage): Boolean {
|
||||||
|
if (InputHandler.registeredControllers.size == 1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.has("motion")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentDevice = settingsViewModel.getCurrentDeviceParams(params)
|
||||||
|
if (currentDevice.get("engine", "any") == "any") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val guidMatch = params.get("guid", "") == currentDevice.get("guid", "") ||
|
||||||
|
params.get("guid", "") == currentDevice.get("guid2", "")
|
||||||
|
return params.get("engine", "") == currentDevice.get("engine", "") &&
|
||||||
|
guidMatch &&
|
||||||
|
params.get("port", 0) == currentDevice.get("port", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "InputDialogFragment"
|
||||||
|
|
||||||
|
const val POSITION = "Position"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
inputMappingViewModel: SettingsViewModel,
|
||||||
|
setting: InputSetting,
|
||||||
|
position: Int
|
||||||
|
): InputDialogFragment {
|
||||||
|
inputMappingViewModel.clickedItem = setting
|
||||||
|
val args = Bundle()
|
||||||
|
args.putInt(POSITION, position)
|
||||||
|
val fragment = InputDialogFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.ui
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.adapters.AbstractListAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.ListItemInputProfileBinding
|
||||||
|
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
|
||||||
|
class InputProfileAdapter(options: List<ProfileItem>) :
|
||||||
|
AbstractListAdapter<ProfileItem, AbstractViewHolder<ProfileItem>>(options) {
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): AbstractViewHolder<ProfileItem> {
|
||||||
|
ListItemInputProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
.also { return InputProfileViewHolder(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class InputProfileViewHolder(val binding: ListItemInputProfileBinding) :
|
||||||
|
AbstractViewHolder<ProfileItem>(binding) {
|
||||||
|
override fun bind(model: ProfileItem) {
|
||||||
|
when (model) {
|
||||||
|
is ExistingProfileItem -> {
|
||||||
|
binding.title.text = model.name
|
||||||
|
binding.buttonNew.visibility = View.GONE
|
||||||
|
binding.buttonDelete.visibility = View.VISIBLE
|
||||||
|
binding.buttonDelete.setOnClickListener { model.deleteProfile.invoke() }
|
||||||
|
binding.buttonSave.visibility = View.VISIBLE
|
||||||
|
binding.buttonSave.setOnClickListener { model.saveProfile.invoke() }
|
||||||
|
binding.buttonLoad.visibility = View.VISIBLE
|
||||||
|
binding.buttonLoad.setOnClickListener { model.loadProfile.invoke() }
|
||||||
|
}
|
||||||
|
|
||||||
|
is NewProfileItem -> {
|
||||||
|
binding.title.text = model.name
|
||||||
|
binding.buttonNew.visibility = View.VISIBLE
|
||||||
|
binding.buttonNew.setOnClickListener { model.createNewProfile.invoke() }
|
||||||
|
binding.buttonSave.visibility = View.GONE
|
||||||
|
binding.buttonDelete.visibility = View.GONE
|
||||||
|
binding.buttonLoad.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface ProfileItem {
|
||||||
|
val name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NewProfileItem(
|
||||||
|
val createNewProfile: () -> Unit
|
||||||
|
) : ProfileItem {
|
||||||
|
override val name: String = YuzuApplication.appContext.getString(R.string.create_new_profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ExistingProfileItem(
|
||||||
|
override val name: String,
|
||||||
|
val deleteProfile: () -> Unit,
|
||||||
|
val saveProfile: () -> Unit,
|
||||||
|
val loadProfile: () -> Unit
|
||||||
|
) : ProfileItem
|
@ -0,0 +1,155 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.ui
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.databinding.DialogInputProfilesBinding
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
|
||||||
|
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||||
|
|
||||||
|
class InputProfileDialogFragment : DialogFragment() {
|
||||||
|
private var position = 0
|
||||||
|
|
||||||
|
private val settingsViewModel: SettingsViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var binding: DialogInputProfilesBinding
|
||||||
|
|
||||||
|
private lateinit var setting: InputProfileSetting
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
position = requireArguments().getInt(POSITION)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
binding = DialogInputProfilesBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
setting = settingsViewModel.clickedItem as InputProfileSetting
|
||||||
|
val options = mutableListOf<ProfileItem>().apply {
|
||||||
|
add(
|
||||||
|
NewProfileItem(
|
||||||
|
createNewProfile = {
|
||||||
|
NewInputProfileDialogFragment.newInstance(
|
||||||
|
settingsViewModel,
|
||||||
|
setting,
|
||||||
|
position
|
||||||
|
).show(parentFragmentManager, NewInputProfileDialogFragment.TAG)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val onActionDismiss = {
|
||||||
|
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
setting.getProfileNames().forEach {
|
||||||
|
add(
|
||||||
|
ExistingProfileItem(
|
||||||
|
it,
|
||||||
|
deleteProfile = {
|
||||||
|
settingsViewModel.setShouldShowDeleteProfileDialog(it)
|
||||||
|
},
|
||||||
|
saveProfile = {
|
||||||
|
if (!setting.saveProfile(it)) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.failed_to_save_profile,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
onActionDismiss.invoke()
|
||||||
|
},
|
||||||
|
loadProfile = {
|
||||||
|
if (!setting.loadProfile(it)) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.failed_to_load_profile,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
onActionDismiss.invoke()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.listProfiles.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
adapter = InputProfileAdapter(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setView(binding.root)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
settingsViewModel.shouldShowDeleteProfileDialog.collect {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
activity = requireActivity(),
|
||||||
|
titleId = R.string.delete_input_profile,
|
||||||
|
descriptionId = R.string.delete_input_profile_description,
|
||||||
|
positiveAction = {
|
||||||
|
setting.deleteProfile(it)
|
||||||
|
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||||
|
},
|
||||||
|
negativeAction = {},
|
||||||
|
negativeButtonTitleId = android.R.string.cancel
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
settingsViewModel.setShouldShowDeleteProfileDialog("")
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "InputProfileDialogFragment"
|
||||||
|
|
||||||
|
const val POSITION = "Position"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
settingsViewModel: SettingsViewModel,
|
||||||
|
profileSetting: InputProfileSetting,
|
||||||
|
position: Int
|
||||||
|
): InputProfileDialogFragment {
|
||||||
|
settingsViewModel.clickedItem = profileSetting
|
||||||
|
|
||||||
|
val args = Bundle()
|
||||||
|
args.putInt(POSITION, position)
|
||||||
|
val fragment = InputProfileDialogFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.ui
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
|
||||||
|
class NewInputProfileDialogFragment : DialogFragment() {
|
||||||
|
private var position = 0
|
||||||
|
|
||||||
|
private val settingsViewModel: SettingsViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var binding: DialogEditTextBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
position = requireArguments().getInt(POSITION)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
binding = DialogEditTextBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
val setting = settingsViewModel.clickedItem as InputProfileSetting
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.enter_profile_name)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
val profileName = binding.editText.text.toString()
|
||||||
|
if (!setting.isProfileNameValid(profileName)) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.invalid_profile_name,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
return@setPositiveButton
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setting.createProfile(profileName)) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.profile_name_already_exists,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
settingsViewModel.setAdapterItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setView(binding.root)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "NewInputProfileDialogFragment"
|
||||||
|
|
||||||
|
const val POSITION = "Position"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
settingsViewModel: SettingsViewModel,
|
||||||
|
profileSetting: InputProfileSetting,
|
||||||
|
position: Int
|
||||||
|
): NewInputProfileDialogFragment {
|
||||||
|
settingsViewModel.clickedItem = profileSetting
|
||||||
|
|
||||||
|
val args = Bundle()
|
||||||
|
args.putInt(POSITION, position)
|
||||||
|
val fragment = NewInputProfileDialogFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
|
||||||
|
class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
||||||
|
SettingViewHolder(binding.root, adapter) {
|
||||||
|
private lateinit var setting: InputProfileSetting
|
||||||
|
|
||||||
|
override fun bind(item: SettingsItem) {
|
||||||
|
setting = item as InputProfileSetting
|
||||||
|
binding.textSettingName.text = setting.title
|
||||||
|
binding.textSettingValue.text =
|
||||||
|
setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) }
|
||||||
|
|
||||||
|
binding.textSettingDescription.visibility = View.GONE
|
||||||
|
binding.buttonClear.visibility = View.GONE
|
||||||
|
binding.icon.visibility = View.GONE
|
||||||
|
binding.buttonClear.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(clicked: View) =
|
||||||
|
adapter.onInputProfileClick(setting, bindingAdapterPosition)
|
||||||
|
|
||||||
|
override fun onLongClick(clicked: View): Boolean = false
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding
|
||||||
|
import org.yuzu.yuzu_emu.features.input.NativeInput
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||||
|
|
||||||
|
class InputViewHolder(val binding: ListItemSettingInputBinding, adapter: SettingsAdapter) :
|
||||||
|
SettingViewHolder(binding.root, adapter) {
|
||||||
|
private lateinit var setting: InputSetting
|
||||||
|
|
||||||
|
override fun bind(item: SettingsItem) {
|
||||||
|
setting = item as InputSetting
|
||||||
|
binding.textSettingName.text = setting.title
|
||||||
|
binding.textSettingValue.text = setting.getSelectedValue()
|
||||||
|
|
||||||
|
binding.buttonOptions.visibility = when (item) {
|
||||||
|
is AnalogInputSetting -> {
|
||||||
|
val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
|
||||||
|
if (
|
||||||
|
param.get("engine", "") == "analog_from_button" ||
|
||||||
|
param.has("axis_x") || param.has("axis_y")
|
||||||
|
) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ButtonInputSetting -> {
|
||||||
|
val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
|
||||||
|
if (
|
||||||
|
param.has("code") || param.has("button") || param.has("hat") ||
|
||||||
|
param.has("axis")
|
||||||
|
) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ModifierInputSetting -> {
|
||||||
|
val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
|
||||||
|
if (params.has("modifier")) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonOptions.setOnClickListener(null)
|
||||||
|
binding.buttonOptions.setOnClickListener {
|
||||||
|
adapter.onInputOptionsClick(binding.buttonOptions, setting, bindingAdapterPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(clicked: View) =
|
||||||
|
adapter.onInputClick(setting, bindingAdapterPosition)
|
||||||
|
|
||||||
|
override fun onLongClick(clicked: View): Boolean =
|
||||||
|
adapter.onLongClick(setting, bindingAdapterPosition)
|
||||||
|
}
|
@ -0,0 +1,141 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
|
// Kotlin version of src/common/param_package.h
|
||||||
|
class ParamPackage(serialized: String = "") {
|
||||||
|
private val KEY_VALUE_SEPARATOR = ":"
|
||||||
|
private val PARAM_SEPARATOR = ","
|
||||||
|
|
||||||
|
private val ESCAPE_CHARACTER = "$"
|
||||||
|
private val KEY_VALUE_SEPARATOR_ESCAPE = "$0"
|
||||||
|
private val PARAM_SEPARATOR_ESCAPE = "$1"
|
||||||
|
private val ESCAPE_CHARACTER_ESCAPE = "$2"
|
||||||
|
|
||||||
|
private val EMPTY_PLACEHOLDER = "[empty]"
|
||||||
|
|
||||||
|
val data = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
val pairs = serialized.split(PARAM_SEPARATOR)
|
||||||
|
for (pair in pairs) {
|
||||||
|
val keyValue = pair.split(KEY_VALUE_SEPARATOR).toMutableList()
|
||||||
|
if (keyValue.size != 2) {
|
||||||
|
Log.error("[ParamPackage] Invalid key pair $keyValue")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
keyValue.forEachIndexed { i: Int, _: String ->
|
||||||
|
keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR_ESCAPE, KEY_VALUE_SEPARATOR)
|
||||||
|
keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR_ESCAPE, PARAM_SEPARATOR)
|
||||||
|
keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER_ESCAPE, ESCAPE_CHARACTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
set(keyValue[0], keyValue[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(params: List<Pair<String, String>>) : this() {
|
||||||
|
params.forEach {
|
||||||
|
data[it.first] = it.second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun serialize(): String {
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
return EMPTY_PLACEHOLDER
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = StringBuilder()
|
||||||
|
data.forEach {
|
||||||
|
val keyValue = mutableListOf(it.key, it.value)
|
||||||
|
keyValue.forEachIndexed { i, _ ->
|
||||||
|
keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER, ESCAPE_CHARACTER_ESCAPE)
|
||||||
|
keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR, PARAM_SEPARATOR_ESCAPE)
|
||||||
|
keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR, KEY_VALUE_SEPARATOR_ESCAPE)
|
||||||
|
}
|
||||||
|
result.append("${keyValue[0]}$KEY_VALUE_SEPARATOR${keyValue[1]}$PARAM_SEPARATOR")
|
||||||
|
}
|
||||||
|
return result.removeSuffix(PARAM_SEPARATOR).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(key: String, defaultValue: String): String =
|
||||||
|
if (has(key)) {
|
||||||
|
data[key]!!
|
||||||
|
} else {
|
||||||
|
Log.debug("[ParamPackage] key $key not found")
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(key: String, defaultValue: Int): Int =
|
||||||
|
if (has(key)) {
|
||||||
|
try {
|
||||||
|
data[key]!!.toInt()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
Log.debug("[ParamPackage] failed to convert ${data[key]!!} to int")
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.debug("[ParamPackage] key $key not found")
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.toBoolean(): Boolean =
|
||||||
|
if (this == 1) {
|
||||||
|
true
|
||||||
|
} else if (this == 0) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
throw Exception("Tried to convert a value to a boolean that was not 0 or 1!")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(key: String, defaultValue: Boolean): Boolean =
|
||||||
|
if (has(key)) {
|
||||||
|
try {
|
||||||
|
get(key, if (defaultValue) 1 else 0).toBoolean()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.debug("[ParamPackage] failed to convert ${data[key]!!} to boolean")
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.debug("[ParamPackage] key $key not found")
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(key: String, defaultValue: Float): Float =
|
||||||
|
if (has(key)) {
|
||||||
|
try {
|
||||||
|
data[key]!!.toFloat()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
Log.debug("[ParamPackage] failed to convert ${data[key]!!} to float")
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.debug("[ParamPackage] key $key not found")
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
fun set(key: String, value: String) {
|
||||||
|
data[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun set(key: String, value: Int) {
|
||||||
|
data[key] = value.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Boolean.toInt(): Int = if (this) 1 else 0
|
||||||
|
fun set(key: String, value: Boolean) {
|
||||||
|
data[key] = value.toInt().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun set(key: String, value: Float) {
|
||||||
|
data[key] = value.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun has(key: String): Boolean = data.containsKey(key)
|
||||||
|
|
||||||
|
fun erase(key: String) = data.remove(key)
|
||||||
|
|
||||||
|
fun clear() = data.clear()
|
||||||
|
}
|
@ -0,0 +1,631 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <common/fs/fs.h>
|
||||||
|
#include <common/fs/path_util.h>
|
||||||
|
#include <common/settings.h>
|
||||||
|
#include <hid_core/hid_types.h>
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include "android_config.h"
|
||||||
|
#include "common/android/android_common.h"
|
||||||
|
#include "common/android/id_cache.h"
|
||||||
|
#include "hid_core/frontend/emulated_controller.h"
|
||||||
|
#include "hid_core/hid_core.h"
|
||||||
|
#include "input_common/drivers/android.h"
|
||||||
|
#include "input_common/drivers/touch_screen.h"
|
||||||
|
#include "input_common/drivers/virtual_amiibo.h"
|
||||||
|
#include "input_common/drivers/virtual_gamepad.h"
|
||||||
|
#include "native.h"
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<AndroidConfig>> map_profiles;
|
||||||
|
|
||||||
|
bool IsHandheldOnly() {
|
||||||
|
const auto npad_style_set =
|
||||||
|
EmulationSession::GetInstance().System().HIDCore().GetSupportedStyleTag();
|
||||||
|
|
||||||
|
if (npad_style_set.fullkey == 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (npad_style_set.handheld == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !Settings::IsDockedMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path GetNameWithoutExtension(std::filesystem::path filename) {
|
||||||
|
return filename.replace_extension();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsProfileNameValid(std::string_view profile_name) {
|
||||||
|
return profile_name.find_first_of("<>:;\"/\\|,.!?*") == std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProfileExistsInFilesystem(std::string_view profile_name) {
|
||||||
|
return Common::FS::Exists(Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input" /
|
||||||
|
fmt::format("{}.ini", profile_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProfileExistsInMap(const std::string& profile_name) {
|
||||||
|
return map_profiles.find(profile_name) != map_profiles.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SaveProfile(const std::string& profile_name, std::size_t player_index) {
|
||||||
|
if (!ProfileExistsInMap(profile_name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings::values.players.GetValue()[player_index].profile_name = profile_name;
|
||||||
|
map_profiles[profile_name]->SaveAndroidControlPlayerValues(player_index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LoadProfile(std::string& profile_name, std::size_t player_index) {
|
||||||
|
if (!ProfileExistsInMap(profile_name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ProfileExistsInFilesystem(profile_name)) {
|
||||||
|
map_profiles.erase(profile_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Config, "Loading input profile `{}`", profile_name);
|
||||||
|
|
||||||
|
Settings::values.players.GetValue()[player_index].profile_name = profile_name;
|
||||||
|
map_profiles[profile_name]->ReadAndroidControlPlayerValues(player_index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyControllerConfig(size_t player_index,
|
||||||
|
const std::function<void(Core::HID::EmulatedController*)>& apply) {
|
||||||
|
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
|
||||||
|
if (player_index == 0) {
|
||||||
|
auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
|
||||||
|
auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
|
||||||
|
handheld->EnableConfiguration();
|
||||||
|
player_one->EnableConfiguration();
|
||||||
|
apply(handheld);
|
||||||
|
apply(player_one);
|
||||||
|
handheld->DisableConfiguration();
|
||||||
|
player_one->DisableConfiguration();
|
||||||
|
handheld->SaveCurrentConfig();
|
||||||
|
player_one->SaveCurrentConfig();
|
||||||
|
} else {
|
||||||
|
auto* controller = hid_core.GetEmulatedControllerByIndex(player_index);
|
||||||
|
controller->EnableConfiguration();
|
||||||
|
apply(controller);
|
||||||
|
controller->DisableConfiguration();
|
||||||
|
controller->SaveCurrentConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectController(size_t player_index, bool connected) {
|
||||||
|
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
|
||||||
|
if (player_index == 0) {
|
||||||
|
auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
|
||||||
|
auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
|
||||||
|
handheld->EnableConfiguration();
|
||||||
|
player_one->EnableConfiguration();
|
||||||
|
if (player_one->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) {
|
||||||
|
if (connected) {
|
||||||
|
handheld->Connect();
|
||||||
|
} else {
|
||||||
|
handheld->Disconnect();
|
||||||
|
}
|
||||||
|
player_one->Disconnect();
|
||||||
|
} else {
|
||||||
|
if (connected) {
|
||||||
|
player_one->Connect();
|
||||||
|
} else {
|
||||||
|
player_one->Disconnect();
|
||||||
|
}
|
||||||
|
handheld->Disconnect();
|
||||||
|
}
|
||||||
|
handheld->DisableConfiguration();
|
||||||
|
player_one->DisableConfiguration();
|
||||||
|
handheld->SaveCurrentConfig();
|
||||||
|
player_one->SaveCurrentConfig();
|
||||||
|
} else {
|
||||||
|
auto* controller = hid_core.GetEmulatedControllerByIndex(player_index);
|
||||||
|
controller->EnableConfiguration();
|
||||||
|
if (connected) {
|
||||||
|
controller->Connect();
|
||||||
|
} else {
|
||||||
|
controller->Disconnect();
|
||||||
|
}
|
||||||
|
controller->DisableConfiguration();
|
||||||
|
controller->SaveCurrentConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isHandheldOnly(JNIEnv* env,
|
||||||
|
jobject j_obj) {
|
||||||
|
return IsHandheldOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadButtonEvent(
|
||||||
|
JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_button_id, jint j_action) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetButtonState(
|
||||||
|
Common::Android::GetJString(env, j_guid), j_port, j_button_id, j_action != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadAxisEvent(
|
||||||
|
JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_stick_id, jfloat j_value) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetAxisPosition(
|
||||||
|
Common::Android::GetJString(env, j_guid), j_port, j_stick_id, j_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadMotionEvent(
|
||||||
|
JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jlong j_delta_timestamp,
|
||||||
|
jfloat j_x_gyro, jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel,
|
||||||
|
jfloat j_z_accel) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetMotionState(
|
||||||
|
Common::Android::GetJString(env, j_guid), j_port, j_delta_timestamp, j_x_gyro, j_y_gyro,
|
||||||
|
j_z_gyro, j_x_accel, j_y_accel, j_z_accel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onReadNfcTag(JNIEnv* env, jobject j_obj,
|
||||||
|
jbyteArray j_data) {
|
||||||
|
jboolean isCopy{false};
|
||||||
|
std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)),
|
||||||
|
static_cast<size_t>(env->GetArrayLength(j_data)));
|
||||||
|
|
||||||
|
if (EmulationSession::GetInstance().IsRunning()) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->LoadAmiibo(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onRemoveNfcTag(JNIEnv* env, jobject j_obj) {
|
||||||
|
if (EmulationSession::GetInstance().IsRunning()) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->CloseAmiibo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchPressed(JNIEnv* env, jobject j_obj,
|
||||||
|
jint j_id, jfloat j_x_axis,
|
||||||
|
jfloat j_y_axis) {
|
||||||
|
if (EmulationSession::GetInstance().IsRunning()) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchPressed(
|
||||||
|
j_id, j_x_axis, j_y_axis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchMoved(JNIEnv* env, jobject j_obj,
|
||||||
|
jint j_id, jfloat j_x_axis,
|
||||||
|
jfloat j_y_axis) {
|
||||||
|
if (EmulationSession::GetInstance().IsRunning()) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchMoved(
|
||||||
|
j_id, j_x_axis, j_y_axis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchReleased(JNIEnv* env, jobject j_obj,
|
||||||
|
jint j_id) {
|
||||||
|
if (EmulationSession::GetInstance().IsRunning()) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(j_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayButtonEventImpl(
|
||||||
|
JNIEnv* env, jobject j_obj, jint j_port, jint j_button_id, jint j_action) {
|
||||||
|
if (EmulationSession::GetInstance().IsRunning()) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetButtonState(
|
||||||
|
j_port, j_button_id, j_action == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayJoystickEventImpl(
|
||||||
|
JNIEnv* env, jobject j_obj, jint j_port, jint j_stick_id, jfloat j_x_axis, jfloat j_y_axis) {
|
||||||
|
if (EmulationSession::GetInstance().IsRunning()) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetStickPosition(
|
||||||
|
j_port, j_stick_id, j_x_axis, j_y_axis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onDeviceMotionEvent(
|
||||||
|
JNIEnv* env, jobject j_obj, jint j_port, jlong j_delta_timestamp, jfloat j_x_gyro,
|
||||||
|
jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, jfloat j_z_accel) {
|
||||||
|
if (EmulationSession::GetInstance().IsRunning()) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetMotionState(
|
||||||
|
j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, j_z_gyro, j_x_accel, j_y_accel,
|
||||||
|
j_z_accel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_reloadInputDevices(JNIEnv* env,
|
||||||
|
jobject j_obj) {
|
||||||
|
EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_registerController(JNIEnv* env,
|
||||||
|
jobject j_obj,
|
||||||
|
jobject j_device) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->RegisterController(j_device);
|
||||||
|
}
|
||||||
|
|
||||||
|
jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputDevices(JNIEnv* env,
|
||||||
|
jobject j_obj) {
|
||||||
|
auto devices = EmulationSession::GetInstance().GetInputSubsystem().GetInputDevices();
|
||||||
|
jobjectArray jdevices = env->NewObjectArray(devices.size(), Common::Android::GetStringClass(),
|
||||||
|
Common::Android::ToJString(env, ""));
|
||||||
|
for (size_t i = 0; i < devices.size(); ++i) {
|
||||||
|
env->SetObjectArrayElement(jdevices, i,
|
||||||
|
Common::Android::ToJString(env, devices[i].Serialize()));
|
||||||
|
}
|
||||||
|
return jdevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadInputProfiles(JNIEnv* env,
|
||||||
|
jobject j_obj) {
|
||||||
|
map_profiles.clear();
|
||||||
|
const auto input_profile_loc =
|
||||||
|
Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input";
|
||||||
|
|
||||||
|
if (Common::FS::IsDir(input_profile_loc)) {
|
||||||
|
Common::FS::IterateDirEntries(
|
||||||
|
input_profile_loc,
|
||||||
|
[&](const std::filesystem::path& full_path) {
|
||||||
|
const auto filename = full_path.filename();
|
||||||
|
const auto name_without_ext =
|
||||||
|
Common::FS::PathToUTF8String(GetNameWithoutExtension(filename));
|
||||||
|
|
||||||
|
if (filename.extension() == ".ini" && IsProfileNameValid(name_without_ext)) {
|
||||||
|
map_profiles.insert_or_assign(
|
||||||
|
name_without_ext, std::make_unique<AndroidConfig>(
|
||||||
|
name_without_ext, Config::ConfigType::InputProfile));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
Common::FS::DirEntryFilter::File);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputProfileNames(
|
||||||
|
JNIEnv* env, jobject j_obj) {
|
||||||
|
std::vector<std::string> profile_names;
|
||||||
|
profile_names.reserve(map_profiles.size());
|
||||||
|
|
||||||
|
auto it = map_profiles.cbegin();
|
||||||
|
while (it != map_profiles.cend()) {
|
||||||
|
const auto& [profile_name, config] = *it;
|
||||||
|
if (!ProfileExistsInFilesystem(profile_name)) {
|
||||||
|
it = map_profiles.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
profile_names.push_back(profile_name);
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stable_sort(profile_names.begin(), profile_names.end());
|
||||||
|
|
||||||
|
jobjectArray j_profile_names =
|
||||||
|
env->NewObjectArray(profile_names.size(), Common::Android::GetStringClass(),
|
||||||
|
Common::Android::ToJString(env, ""));
|
||||||
|
for (size_t i = 0; i < profile_names.size(); ++i) {
|
||||||
|
env->SetObjectArrayElement(j_profile_names, i,
|
||||||
|
Common::Android::ToJString(env, profile_names[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return j_profile_names;
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isProfileNameValid(JNIEnv* env,
|
||||||
|
jobject j_obj,
|
||||||
|
jstring j_name) {
|
||||||
|
return Common::Android::GetJString(env, j_name).find_first_of("<>:;\"/\\|,.!?*") ==
|
||||||
|
std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_createProfile(JNIEnv* env,
|
||||||
|
jobject j_obj,
|
||||||
|
jstring j_name,
|
||||||
|
jint j_player_index) {
|
||||||
|
auto profile_name = Common::Android::GetJString(env, j_name);
|
||||||
|
if (ProfileExistsInMap(profile_name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
map_profiles.insert_or_assign(
|
||||||
|
profile_name,
|
||||||
|
std::make_unique<AndroidConfig>(profile_name, Config::ConfigType::InputProfile));
|
||||||
|
|
||||||
|
return SaveProfile(profile_name, j_player_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_deleteProfile(JNIEnv* env,
|
||||||
|
jobject j_obj,
|
||||||
|
jstring j_name,
|
||||||
|
jint j_player_index) {
|
||||||
|
auto profile_name = Common::Android::GetJString(env, j_name);
|
||||||
|
if (!ProfileExistsInMap(profile_name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ProfileExistsInFilesystem(profile_name) ||
|
||||||
|
Common::FS::RemoveFile(map_profiles[profile_name]->GetConfigFilePath())) {
|
||||||
|
map_profiles.erase(profile_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings::values.players.GetValue()[j_player_index].profile_name = "";
|
||||||
|
return !ProfileExistsInMap(profile_name) && !ProfileExistsInFilesystem(profile_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadProfile(JNIEnv* env, jobject j_obj,
|
||||||
|
jstring j_name,
|
||||||
|
jint j_player_index) {
|
||||||
|
auto profile_name = Common::Android::GetJString(env, j_name);
|
||||||
|
return LoadProfile(profile_name, j_player_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_saveProfile(JNIEnv* env, jobject j_obj,
|
||||||
|
jstring j_name,
|
||||||
|
jint j_player_index) {
|
||||||
|
auto profile_name = Common::Android::GetJString(env, j_name);
|
||||||
|
return SaveProfile(profile_name, j_player_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadPerGameConfiguration(
|
||||||
|
JNIEnv* env, jobject j_obj, jint j_player_index, jint j_selected_index,
|
||||||
|
jstring j_selected_profile_name) {
|
||||||
|
static constexpr size_t HANDHELD_INDEX = 8;
|
||||||
|
|
||||||
|
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
|
||||||
|
Settings::values.players.SetGlobal(false);
|
||||||
|
|
||||||
|
auto profile_name = Common::Android::GetJString(env, j_selected_profile_name);
|
||||||
|
auto* emulated_controller = hid_core.GetEmulatedControllerByIndex(j_player_index);
|
||||||
|
|
||||||
|
if (j_selected_index == 0) {
|
||||||
|
Settings::values.players.GetValue()[j_player_index].profile_name = "";
|
||||||
|
if (j_player_index == 0) {
|
||||||
|
Settings::values.players.GetValue()[HANDHELD_INDEX] = {};
|
||||||
|
}
|
||||||
|
Settings::values.players.SetGlobal(true);
|
||||||
|
emulated_controller->ReloadFromSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (profile_name.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto& player = Settings::values.players.GetValue()[j_player_index];
|
||||||
|
auto& global_player = Settings::values.players.GetValue(true)[j_player_index];
|
||||||
|
player.profile_name = profile_name;
|
||||||
|
global_player.profile_name = profile_name;
|
||||||
|
// Read from the profile into the custom player settings
|
||||||
|
LoadProfile(profile_name, j_player_index);
|
||||||
|
// Make sure the controller is connected
|
||||||
|
player.connected = true;
|
||||||
|
|
||||||
|
emulated_controller->ReloadFromSettings();
|
||||||
|
|
||||||
|
if (j_player_index > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Handle Handheld cases
|
||||||
|
auto& handheld_player = Settings::values.players.GetValue()[HANDHELD_INDEX];
|
||||||
|
auto* handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
|
||||||
|
if (player.controller_type == Settings::ControllerType::Handheld) {
|
||||||
|
handheld_player = player;
|
||||||
|
} else {
|
||||||
|
handheld_player = {};
|
||||||
|
}
|
||||||
|
handheld_controller->ReloadFromSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_beginMapping(JNIEnv* env, jobject j_obj,
|
||||||
|
jint jtype) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().BeginMapping(
|
||||||
|
static_cast<InputCommon::Polling::InputType>(jtype));
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getNextInput(JNIEnv* env,
|
||||||
|
jobject j_obj) {
|
||||||
|
return Common::Android::ToJString(
|
||||||
|
env, EmulationSession::GetInstance().GetInputSubsystem().GetNextInput().Serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_stopMapping(JNIEnv* env, jobject j_obj) {
|
||||||
|
EmulationSession::GetInstance().GetInputSubsystem().StopMapping();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_updateMappingsWithDefaultImpl(
|
||||||
|
JNIEnv* env, jobject j_obj, jint j_player_index, jstring j_device_params,
|
||||||
|
jstring j_display_name) {
|
||||||
|
auto& input_subsystem = EmulationSession::GetInstance().GetInputSubsystem();
|
||||||
|
|
||||||
|
// Clear all previous mappings
|
||||||
|
for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
|
||||||
|
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
|
||||||
|
controller->SetButtonParam(button_id, {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
|
||||||
|
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
|
||||||
|
controller->SetStickParam(analog_id, {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply new mappings
|
||||||
|
auto device = Common::ParamPackage(Common::Android::GetJString(env, j_device_params));
|
||||||
|
auto button_mappings = input_subsystem.GetButtonMappingForDevice(device);
|
||||||
|
auto analog_mappings = input_subsystem.GetAnalogMappingForDevice(device);
|
||||||
|
auto display_name = Common::Android::GetJString(env, j_display_name);
|
||||||
|
for (const auto& button_mapping : button_mappings) {
|
||||||
|
const std::size_t index = button_mapping.first;
|
||||||
|
auto named_mapping = button_mapping.second;
|
||||||
|
named_mapping.Set("display", display_name);
|
||||||
|
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
|
||||||
|
controller->SetButtonParam(index, named_mapping);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const auto& analog_mapping : analog_mappings) {
|
||||||
|
const std::size_t index = analog_mapping.first;
|
||||||
|
auto named_mapping = analog_mapping.second;
|
||||||
|
named_mapping.Set("display", display_name);
|
||||||
|
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
|
||||||
|
controller->SetStickParam(index, named_mapping);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonParamImpl(JNIEnv* env,
|
||||||
|
jobject j_obj,
|
||||||
|
jint j_player_index,
|
||||||
|
jint j_button) {
|
||||||
|
return Common::Android::ToJString(env, EmulationSession::GetInstance()
|
||||||
|
.System()
|
||||||
|
.HIDCore()
|
||||||
|
.GetEmulatedControllerByIndex(j_player_index)
|
||||||
|
->GetButtonParam(j_button)
|
||||||
|
.Serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setButtonParamImpl(
|
||||||
|
JNIEnv* env, jobject j_obj, jint j_player_index, jint j_button_id, jstring j_param) {
|
||||||
|
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
|
||||||
|
controller->SetButtonParam(j_button_id,
|
||||||
|
Common::ParamPackage(Common::Android::GetJString(env, j_param)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStickParamImpl(JNIEnv* env,
|
||||||
|
jobject j_obj,
|
||||||
|
jint j_player_index,
|
||||||
|
jint j_stick) {
|
||||||
|
return Common::Android::ToJString(env, EmulationSession::GetInstance()
|
||||||
|
.System()
|
||||||
|
.HIDCore()
|
||||||
|
.GetEmulatedControllerByIndex(j_player_index)
|
||||||
|
->GetStickParam(j_stick)
|
||||||
|
.Serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStickParamImpl(
|
||||||
|
JNIEnv* env, jobject j_obj, jint j_player_index, jint j_stick_id, jstring j_param) {
|
||||||
|
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
|
||||||
|
controller->SetStickParam(j_stick_id,
|
||||||
|
Common::ParamPackage(Common::Android::GetJString(env, j_param)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonNameImpl(JNIEnv* env,
|
||||||
|
jobject j_obj,
|
||||||
|
jstring j_param) {
|
||||||
|
return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().GetButtonName(
|
||||||
|
Common::ParamPackage(Common::Android::GetJString(env, j_param))));
|
||||||
|
}
|
||||||
|
|
||||||
|
jintArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getSupportedStyleTagsImpl(
|
||||||
|
JNIEnv* env, jobject j_obj, jint j_player_index) {
|
||||||
|
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
|
||||||
|
const auto npad_style_set = hid_core.GetSupportedStyleTag();
|
||||||
|
std::vector<s32> supported_indexes;
|
||||||
|
if (npad_style_set.fullkey == 1) {
|
||||||
|
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Fullkey));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (npad_style_set.joycon_dual == 1) {
|
||||||
|
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconDual));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (npad_style_set.joycon_left == 1) {
|
||||||
|
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconLeft));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (npad_style_set.joycon_right == 1) {
|
||||||
|
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconRight));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j_player_index == 0 && npad_style_set.handheld == 1) {
|
||||||
|
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Handheld));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (npad_style_set.gamecube == 1) {
|
||||||
|
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::GameCube));
|
||||||
|
}
|
||||||
|
|
||||||
|
jintArray j_supported_indexes = env->NewIntArray(supported_indexes.size());
|
||||||
|
env->SetIntArrayRegion(j_supported_indexes, 0, supported_indexes.size(),
|
||||||
|
supported_indexes.data());
|
||||||
|
return j_supported_indexes;
|
||||||
|
}
|
||||||
|
|
||||||
|
jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStyleIndexImpl(JNIEnv* env,
|
||||||
|
jobject j_obj,
|
||||||
|
jint j_player_index) {
|
||||||
|
return static_cast<s32>(EmulationSession::GetInstance()
|
||||||
|
.System()
|
||||||
|
.HIDCore()
|
||||||
|
.GetEmulatedControllerByIndex(j_player_index)
|
||||||
|
->GetNpadStyleIndex(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStyleIndexImpl(JNIEnv* env,
|
||||||
|
jobject j_obj,
|
||||||
|
jint j_player_index,
|
||||||
|
jint j_style_index) {
|
||||||
|
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
|
||||||
|
auto type = static_cast<Core::HID::NpadStyleIndex>(j_style_index);
|
||||||
|
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
|
||||||
|
controller->SetNpadStyleIndex(type);
|
||||||
|
});
|
||||||
|
if (j_player_index == 0) {
|
||||||
|
auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
|
||||||
|
auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
|
||||||
|
ConnectController(j_player_index,
|
||||||
|
player_one->IsConnected(true) || handheld->IsConnected(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isControllerImpl(JNIEnv* env,
|
||||||
|
jobject j_obj,
|
||||||
|
jstring jparams) {
|
||||||
|
return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().IsController(
|
||||||
|
Common::ParamPackage(Common::Android::GetJString(env, jparams))));
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getIsConnected(JNIEnv* env,
|
||||||
|
jobject j_obj,
|
||||||
|
jint j_player_index) {
|
||||||
|
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
|
||||||
|
auto* controller = hid_core.GetEmulatedControllerByIndex(static_cast<size_t>(j_player_index));
|
||||||
|
if (j_player_index == 0 &&
|
||||||
|
controller->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) {
|
||||||
|
return hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld)->IsConnected(true);
|
||||||
|
}
|
||||||
|
return controller->IsConnected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_connectControllersImpl(
|
||||||
|
JNIEnv* env, jobject j_obj, jbooleanArray j_connected) {
|
||||||
|
jboolean isCopy = false;
|
||||||
|
auto j_connected_array_size = env->GetArrayLength(j_connected);
|
||||||
|
jboolean* j_connected_array = env->GetBooleanArrayElements(j_connected, &isCopy);
|
||||||
|
for (int i = 0; i < j_connected_array_size; ++i) {
|
||||||
|
ConnectController(i, j_connected_array[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_resetControllerMappings(
|
||||||
|
JNIEnv* env, jobject j_obj, jint j_player_index) {
|
||||||
|
// Clear all previous mappings
|
||||||
|
for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
|
||||||
|
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
|
||||||
|
controller->SetButtonParam(button_id, {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
|
||||||
|
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
|
||||||
|
controller->SetStickParam(analog_id, {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
@ -0,0 +1,142 @@
|
|||||||
|
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt">
|
||||||
|
<aapt:attr name="android:drawable">
|
||||||
|
<vector
|
||||||
|
android:width="1000dp"
|
||||||
|
android:height="1000dp"
|
||||||
|
android:viewportWidth="1000"
|
||||||
|
android:viewportHeight="1000">
|
||||||
|
<group android:name="_R_G">
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_0_G"
|
||||||
|
android:pivotX="100"
|
||||||
|
android:pivotY="100"
|
||||||
|
android:scaleX="4.5"
|
||||||
|
android:scaleY="4.5"
|
||||||
|
android:translateX="400"
|
||||||
|
android:translateY="400">
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_0_G_D_0_P_0"
|
||||||
|
android:fillAlpha="1"
|
||||||
|
android:fillColor="?attr/colorSecondaryContainer"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData=" M198.56 100 C198.56,154.43 154.43,198.56 100,198.56 C45.57,198.56 1.44,154.43 1.44,100 C1.44,45.57 45.57,1.44 100,1.44 C154.43,1.44 198.56,45.57 198.56,100c " />
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_0_G_D_2_P_0"
|
||||||
|
android:fillAlpha="0.8"
|
||||||
|
android:fillColor="?attr/colorOnSecondaryContainer"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData=" M50.14 151.21 C50.53,150.18 89.6,49.87 90.1,48.63 C90.1,48.63 90.67,47.2 90.67,47.2 C90.67,47.2 101.67,47.2 101.67,47.2 C101.67,47.2 112.67,47.2 112.67,47.2 C112.67,47.2 133.47,99.12 133.47,99.12 C144.91,127.68 154.32,151.17 154.38,151.33 C154.47,151.56 152.2,151.6 143.14,151.55 C143.14,151.55 131.79,151.48 131.79,151.48 C131.79,151.48 127.22,139.57 127.22,139.57 C127.22,139.57 122.65,127.66 122.65,127.66 C122.65,127.66 101.68,127.73 101.68,127.73 C101.68,127.73 80.71,127.8 80.71,127.8 C80.71,127.8 76.38,139.71 76.38,139.71 C76.38,139.71 72.06,151.62 72.06,151.62 C72.06,151.62 61.02,151.62 61.02,151.62 C50.61,151.62 50,151.55 50.14,151.22 C50.14,151.22 50.14,151.21 50.14,151.21c M115.86 110.06 C115.8,109.91 112.55,101.13 108.62,90.56 C104.7,80 101.42,71.43 101.34,71.53 C101.22,71.66 92.84,94.61 87.25,110.06 C87.17,110.29 90.13,110.34 101.56,110.34 C113,110.34 115.95,110.28 115.86,110.06c " />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group android:name="time_group" />
|
||||||
|
</vector>
|
||||||
|
</aapt:attr>
|
||||||
|
<target android:name="_R_G_L_0_G">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<set android:ordering="together">
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="100"
|
||||||
|
android:propertyName="scaleX"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:valueFrom="4.5"
|
||||||
|
android:valueTo="3.75"
|
||||||
|
android:valueType="floatType">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="100"
|
||||||
|
android:propertyName="scaleY"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:valueFrom="4.5"
|
||||||
|
android:valueTo="3.75"
|
||||||
|
android:valueType="floatType">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="234"
|
||||||
|
android:propertyName="scaleX"
|
||||||
|
android:startOffset="100"
|
||||||
|
android:valueFrom="3.75"
|
||||||
|
android:valueTo="3.75"
|
||||||
|
android:valueType="floatType">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="234"
|
||||||
|
android:propertyName="scaleY"
|
||||||
|
android:startOffset="100"
|
||||||
|
android:valueFrom="3.75"
|
||||||
|
android:valueTo="3.75"
|
||||||
|
android:valueType="floatType">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="167"
|
||||||
|
android:propertyName="scaleX"
|
||||||
|
android:startOffset="334"
|
||||||
|
android:valueFrom="3.75"
|
||||||
|
android:valueTo="4.75"
|
||||||
|
android:valueType="floatType">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="167"
|
||||||
|
android:propertyName="scaleY"
|
||||||
|
android:startOffset="334"
|
||||||
|
android:valueFrom="3.75"
|
||||||
|
android:valueTo="4.75"
|
||||||
|
android:valueType="floatType">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="67"
|
||||||
|
android:propertyName="scaleX"
|
||||||
|
android:startOffset="501"
|
||||||
|
android:valueFrom="4.75"
|
||||||
|
android:valueTo="4.5"
|
||||||
|
android:valueType="floatType">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="67"
|
||||||
|
android:propertyName="scaleY"
|
||||||
|
android:startOffset="501"
|
||||||
|
android:valueFrom="4.75"
|
||||||
|
android:valueTo="4.5"
|
||||||
|
android:valueType="floatType">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
</set>
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
<target android:name="time_group">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<set android:ordering="together">
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="1034"
|
||||||
|
android:propertyName="translateX"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="1"
|
||||||
|
android:valueType="floatType" />
|
||||||
|
</set>
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
</animated-vector>
|
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M700,480q-25,0 -42.5,-17.5T640,420q0,-25 17.5,-42.5T700,360q25,0 42.5,17.5T760,420q0,25 -17.5,42.5T700,480ZM366,480ZM280,600v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM160,720q-33,0 -56.5,-23.5T80,640v-320q0,-34 24,-57.5t58,-23.5h77l81,81L160,320v320h366L55,169l57,-57 736,736 -57,57 -185,-185L160,720ZM880,640q0,26 -14,46t-37,29l-29,-29v-366L434,320l-80,-80h446q33,0 56.5,23.5T880,320v320ZM617,503Z" />
|
||||||
|
</vector>
|
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
|
||||||
|
</vector>
|
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M21,12l-4.37,6.16C16.26,18.68 15.65,19 15,19h-3l0,-6H9v-3H3V7c0,-1.1 0.9,-2 2,-2h10c0.65,0 1.26,0.31 1.63,0.84L21,12zM10,15H7v-3H5v3H2v2h3v3h2v-3h3V15z" />
|
||||||
|
</vector>
|
@ -0,0 +1,21 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M21,5H3C1.9,5 1,5.9 1,7v10c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V7C23,5.9 22.1,5 21,5zM18,17H6V7h12V17z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M15,11.25h1.5v1.5h-1.5z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M12.5,11.25h1.5v1.5h-1.5z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M10,11.25h1.5v1.5h-1.5z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M7.5,11.25h1.5v1.5h-1.5z" />
|
||||||
|
</vector>
|
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
|
||||||
|
</vector>
|
@ -0,0 +1,118 @@
|
|||||||
|
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt">
|
||||||
|
<aapt:attr name="android:drawable">
|
||||||
|
<vector
|
||||||
|
android:width="1000dp"
|
||||||
|
android:height="1000dp"
|
||||||
|
android:viewportWidth="1000"
|
||||||
|
android:viewportHeight="1000">
|
||||||
|
<group android:name="_R_G">
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_1_G"
|
||||||
|
android:pivotX="100"
|
||||||
|
android:pivotY="100"
|
||||||
|
android:scaleX="5"
|
||||||
|
android:scaleY="5"
|
||||||
|
android:translateX="400"
|
||||||
|
android:translateY="400">
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_1_G_D_0_P_0"
|
||||||
|
android:pathData=" M100 199.39 C59.8,199.39 23.56,175.17 8.18,138.04 C-7.2,100.9 1.3,58.15 29.73,29.72 C58.15,1.3 100.9,-7.21 138.04,8.18 C175.18,23.56 199.39,59.8 199.39,100 C199.33,154.87 154.87,199.33 100,199.39c "
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeAlpha="0.6"
|
||||||
|
android:strokeColor="?attr/colorOutline"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round" />
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_0_G_T_1"
|
||||||
|
android:scaleX="5"
|
||||||
|
android:scaleY="5"
|
||||||
|
android:translateX="500"
|
||||||
|
android:translateY="500">
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_0_G"
|
||||||
|
android:translateX="-100"
|
||||||
|
android:translateY="-100">
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_0_G_D_0_P_0"
|
||||||
|
android:fillAlpha="1"
|
||||||
|
android:fillColor="?attr/colorSecondaryContainer"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData=" M100.45 28.02 C140.63,28.02 173.2,60.59 173.2,100.77 C173.2,140.95 140.63,173.52 100.45,173.52 C60.27,173.52 27.7,140.95 27.7,100.77 C27.7,60.59 60.27,28.02 100.45,28.02c " />
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_0_G_D_2_P_0"
|
||||||
|
android:fillAlpha="0.8"
|
||||||
|
android:fillColor="?attr/colorOnSecondaryContainer"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData=" M100.45 50.26 C128.62,50.26 151.46,73.1 151.46,101.28 C151.46,129.45 128.62,152.29 100.45,152.29 C72.27,152.29 49.43,129.45 49.43,101.28 C49.43,73.1 72.27,50.26 100.45,50.26c " />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group android:name="time_group" />
|
||||||
|
</vector>
|
||||||
|
</aapt:attr>
|
||||||
|
<target android:name="_R_G_L_0_G_T_1">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<set android:ordering="together">
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="267"
|
||||||
|
android:pathData="M 500,500C 500,500 364,500 364,500"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="0">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="234"
|
||||||
|
android:pathData="M 364,500C 364,500 364,500 364,500"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="267">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="133"
|
||||||
|
android:pathData="M 364,500C 364,500 525,500 525,500"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="501">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="100"
|
||||||
|
android:pathData="M 525,500C 525,500 500,500 500,500"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="634">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
</set>
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
<target android:name="time_group">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<set android:ordering="together">
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="968"
|
||||||
|
android:propertyName="translateX"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="1"
|
||||||
|
android:valueType="floatType" />
|
||||||
|
</set>
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
</animated-vector>
|
@ -0,0 +1,173 @@
|
|||||||
|
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt">
|
||||||
|
<aapt:attr name="android:drawable">
|
||||||
|
<vector
|
||||||
|
android:width="1000dp"
|
||||||
|
android:height="1000dp"
|
||||||
|
android:viewportWidth="1000"
|
||||||
|
android:viewportHeight="1000">
|
||||||
|
<group android:name="_R_G">
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_1_G"
|
||||||
|
android:pivotX="100"
|
||||||
|
android:pivotY="100"
|
||||||
|
android:scaleX="5"
|
||||||
|
android:scaleY="5"
|
||||||
|
android:translateX="400"
|
||||||
|
android:translateY="400">
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_1_G_D_0_P_0"
|
||||||
|
android:pathData=" M100 199.39 C59.8,199.39 23.56,175.17 8.18,138.04 C-7.2,100.9 1.3,58.15 29.73,29.72 C58.15,1.3 100.9,-7.21 138.04,8.18 C175.18,23.56 199.39,59.8 199.39,100 C199.33,154.87 154.87,199.33 100,199.39c "
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeAlpha="0.6"
|
||||||
|
android:strokeColor="?attr/colorOutline"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round" />
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_0_G_T_1"
|
||||||
|
android:scaleX="5"
|
||||||
|
android:scaleY="5"
|
||||||
|
android:translateX="500"
|
||||||
|
android:translateY="500">
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_0_G"
|
||||||
|
android:translateX="-100"
|
||||||
|
android:translateY="-100">
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_0_G_D_0_P_0"
|
||||||
|
android:fillAlpha="1"
|
||||||
|
android:fillColor="?attr/colorSecondaryContainer"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData=" M100.45 28.02 C140.63,28.02 173.2,60.59 173.2,100.77 C173.2,140.95 140.63,173.52 100.45,173.52 C60.27,173.52 27.7,140.95 27.7,100.77 C27.7,60.59 60.27,28.02 100.45,28.02c " />
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_0_G_D_2_P_0"
|
||||||
|
android:fillAlpha="0.8"
|
||||||
|
android:fillColor="?attr/colorOnSecondaryContainer"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData=" M100.45 50.26 C128.62,50.26 151.46,73.1 151.46,101.28 C151.46,129.45 128.62,152.29 100.45,152.29 C72.27,152.29 49.43,129.45 49.43,101.28 C49.43,73.1 72.27,50.26 100.45,50.26c " />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group android:name="time_group" />
|
||||||
|
</vector>
|
||||||
|
</aapt:attr>
|
||||||
|
<target android:name="_R_G_L_0_G_T_1">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<set android:ordering="together">
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="267"
|
||||||
|
android:pathData="M 500,500C 500,500 364,500 364,500"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="0">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="234"
|
||||||
|
android:pathData="M 364,500C 364,500 364,500 364,500"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="267">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="133"
|
||||||
|
android:pathData="M 364,500C 364,500 525,500 525,500"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="501">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="100"
|
||||||
|
android:pathData="M 525,500C 525,500 500,500 500,500"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="634">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="400"
|
||||||
|
android:pathData="M 500,500C 500,500 500,500 500,500"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="734">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="267"
|
||||||
|
android:pathData="M 500,500C 500,500 500,364 500,364"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="1134">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="234"
|
||||||
|
android:pathData="M 500,364C 500,364 500,364 500,364"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="1401">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="133"
|
||||||
|
android:pathData="M 500,364C 500,364 500,535 500,535"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="1635">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="100"
|
||||||
|
android:pathData="M 500,535C 500,535 500,500 500,500"
|
||||||
|
android:propertyName="translateXY"
|
||||||
|
android:propertyXName="translateX"
|
||||||
|
android:propertyYName="translateY"
|
||||||
|
android:startOffset="1768">
|
||||||
|
<aapt:attr name="android:interpolator">
|
||||||
|
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
|
||||||
|
</aapt:attr>
|
||||||
|
</objectAnimator>
|
||||||
|
</set>
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
<target android:name="time_group">
|
||||||
|
<aapt:attr name="android:animation">
|
||||||
|
<set android:ordering="together">
|
||||||
|
<objectAnimator
|
||||||
|
android:duration="2269"
|
||||||
|
android:propertyName="translateX"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="1"
|
||||||
|
android:valueType="floatType" />
|
||||||
|
</set>
|
||||||
|
</aapt:attr>
|
||||||
|
</target>
|
||||||
|
</animated-vector>
|
@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout 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:id="@+id/setting_body"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="72dp"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:nextFocusLeft="@id/button_options">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/text_setting_name"
|
||||||
|
style="@style/TextAppearance.Material3.HeadlineMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textSize="17sp"
|
||||||
|
app:lineHeight="22dp"
|
||||||
|
tools:text="Setting Name" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/text_setting_value"
|
||||||
|
style="@style/TextAppearance.Material3.LabelMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/spacing_small"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="13sp"
|
||||||
|
tools:text="1x" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_options"
|
||||||
|
style="?attr/materialIconButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:nextFocusRight="@id/setting_body"
|
||||||
|
app:icon="@drawable/ic_more_vert"
|
||||||
|
app:iconSize="24dp"
|
||||||
|
app:iconTint="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/list_profiles"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fadeScrollbars="false" />
|
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:defaultFocusHighlightEnabled="false"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:focusedByDefault="true"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_stick_animation"
|
||||||
|
android:layout_width="@dimen/mapping_anim_size"
|
||||||
|
android:layout_height="@dimen/mapping_anim_size"
|
||||||
|
tools:src="@drawable/stick_two_direction_anim" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_button_animation"
|
||||||
|
android:layout_width="@dimen/mapping_anim_size"
|
||||||
|
android:layout_height="@dimen/mapping_anim_size"
|
||||||
|
android:layout_marginStart="48dp"
|
||||||
|
tools:src="@drawable/button_anim" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -0,0 +1,74 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
|
||||||
|
android:focusable="false"
|
||||||
|
android:paddingHorizontal="20dp"
|
||||||
|
android:paddingVertical="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
style="@style/TextAppearance.Material3.HeadlineMedium"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
|
android:textSize="17sp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/button_layout"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:lineHeight="28dp"
|
||||||
|
tools:text="My profile" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/button_layout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_new"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/create_new_profile"
|
||||||
|
android:tooltipText="@string/create_new_profile"
|
||||||
|
app:icon="@drawable/ic_new_label" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_delete"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/delete"
|
||||||
|
android:tooltipText="@string/delete"
|
||||||
|
app:icon="@drawable/ic_delete" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_save"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/save"
|
||||||
|
android:tooltipText="@string/save"
|
||||||
|
app:icon="@drawable/ic_save" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_load"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/load"
|
||||||
|
android:tooltipText="@string/load"
|
||||||
|
app:icon="@drawable/ic_import" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout 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:id="@+id/setting_body"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="72dp"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:nextFocusRight="@id/button_options">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/text_setting_name"
|
||||||
|
style="@style/TextAppearance.Material3.HeadlineMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textSize="17sp"
|
||||||
|
app:lineHeight="22dp"
|
||||||
|
tools:text="Setting Name" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/text_setting_value"
|
||||||
|
style="@style/TextAppearance.Material3.LabelMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/spacing_small"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="13sp"
|
||||||
|
tools:text="1x" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_options"
|
||||||
|
style="?attr/materialIconButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:nextFocusLeft="@id/setting_body"
|
||||||
|
app:icon="@drawable/ic_more_vert"
|
||||||
|
app:iconSize="24dp"
|
||||||
|
app:iconTint="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/invert_axis"
|
||||||
|
android:title="@string/invert_axis"
|
||||||
|
android:visible="false" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/invert_button"
|
||||||
|
android:title="@string/invert_button"
|
||||||
|
android:visible="false" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/toggle_button"
|
||||||
|
android:title="@string/toggle_button"
|
||||||
|
android:visible="false" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/turbo_button"
|
||||||
|
android:title="@string/turbo_button"
|
||||||
|
android:visible="false" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/set_threshold"
|
||||||
|
android:title="@string/set_threshold"
|
||||||
|
android:visible="false" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/toggle_axis"
|
||||||
|
android:title="@string/toggle_axis"
|
||||||
|
android:visible="false" />
|
||||||
|
|
||||||
|
</menu>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue