mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-15 03:37:09 -04:00
feat: three-finger tap toggles Steam's on-screen keyboard in game mode
This commit is contained in:
1 parent
06bd0de4d7
commit
ffab8d7568
4 files changed
+179
-12
No files matched your search
@@ -162,6 +162,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
private var watching = true
|
||||
private lateinit var touchpad: TouchpadGestures
|
||||
private lateinit var threeFingerTap: com.droiddeck.launcher.input.ThreeFingerTap
|
||||
private var touchMode by mutableStateOf(SessionPrefs.TOUCH_AUTO)
|
||||
private var cursorPos by mutableStateOf(androidx.compose.ui.geometry.Offset(-100f, -100f))
|
||||
private var cursorVisible by mutableStateOf(false)
|
||||
@@ -397,6 +398,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onScreenControls = OnScreenControls(this, bridge, onKeyboard = ::togglePcKeyboard).also { root.addView(it) }
|
||||
keyboard = KeyboardHost(this).also { root.addView(it) }
|
||||
touchpad = TouchpadGestures(PointerGestures.slop(this), pointerListener)
|
||||
threeFingerTap = com.droiddeck.launcher.input.ThreeFingerTap(PointerGestures.slop(this) * 2)
|
||||
val noCursor = android.view.PointerIcon.getSystemIcon(this, android.view.PointerIcon.TYPE_NULL)
|
||||
root.pointerIcon = noCursor
|
||||
surfaceView.pointerIcon = noCursor
|
||||
@@ -1954,6 +1956,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (event.isFromSource(android.view.InputDevice.SOURCE_MOUSE)) return onMouse(event)
|
||||
if (routedTouchMode == SessionPrefs.TOUCH_OFF) return true
|
||||
if (SessionState.mode == SessionService.MODE_STEAM && SessionState.steamUi != "desktop") {
|
||||
val wasActive = threeFingerTap.active
|
||||
if (threeFingerTap.onTouch(event)) padBridge?.triggerSteamKeyboard()
|
||||
if (threeFingerTap.active && !wasActive) {
|
||||
if (usingTouchpad()) touchpad.cancel() else WaylandCompositor.nativeSendTouch(3, -1, 0, 0)
|
||||
}
|
||||
if (threeFingerTap.active || wasActive) return true
|
||||
}
|
||||
if (usingTouchpad()) {
|
||||
val rect = drawnRect() ?: return false
|
||||
if (touchpad.bounds != rect) {
|
||||
|
||||
@@ -54,7 +54,8 @@ public final class PadBridge {
|
||||
private boolean systemGuidePressed;
|
||||
private boolean systemQamPressed;
|
||||
private boolean qamChordActive;
|
||||
private boolean qamSyntheticAPressed;
|
||||
private int chordButton = -1;
|
||||
private boolean chordButtonPressed;
|
||||
private boolean qamTapPressed;
|
||||
private int qamChordGeneration;
|
||||
private volatile Runnable onPlayerInput;
|
||||
@@ -113,7 +114,7 @@ public final class PadBridge {
|
||||
systemGuidePressed = false;
|
||||
systemQamPressed = false;
|
||||
qamChordActive = false;
|
||||
qamSyntheticAPressed = false;
|
||||
chordButtonPressed = false;
|
||||
qamTapPressed = false;
|
||||
qamChordGeneration++;
|
||||
if (open) {
|
||||
@@ -317,7 +318,7 @@ public final class PadBridge {
|
||||
boolean qamStarted = qamPressed && !systemQamPressed;
|
||||
systemGuidePressed = guidePressed;
|
||||
systemQamPressed = qamPressed;
|
||||
if (qamStarted && !nativeQam()) startQamChord();
|
||||
if (qamStarted && !nativeQam()) startChord(PadState.A);
|
||||
else publish();
|
||||
}
|
||||
|
||||
@@ -343,7 +344,7 @@ public final class PadBridge {
|
||||
|
||||
public synchronized void triggerQam() {
|
||||
if (!nativeQam()) {
|
||||
startQamChord();
|
||||
startChord(PadState.A);
|
||||
return;
|
||||
}
|
||||
if (qamTapPressed) return;
|
||||
@@ -359,12 +360,17 @@ public final class PadBridge {
|
||||
publish();
|
||||
}
|
||||
|
||||
private void startQamChord() {
|
||||
public synchronized void triggerSteamKeyboard() {
|
||||
startChord(PadState.X);
|
||||
}
|
||||
|
||||
private void startChord(int button) {
|
||||
if (qamChordActive) return;
|
||||
qamChordActive = true;
|
||||
chordButton = button;
|
||||
int generation = ++qamChordGeneration;
|
||||
publish();
|
||||
chordHandler.postDelayed(() -> pressQamA(generation), qamGuideLeadMs());
|
||||
chordHandler.postDelayed(() -> pressChordButton(generation), qamGuideLeadMs());
|
||||
}
|
||||
|
||||
static long qamGuideLeadMs() {
|
||||
@@ -373,16 +379,16 @@ public final class PadBridge {
|
||||
return Math.max(QAM_GUIDE_LEAD_MIN_MS, Math.min(QAM_GUIDE_LEAD_MAX_MS, frame * QAM_GUIDE_LEAD_FRAMES));
|
||||
}
|
||||
|
||||
private synchronized void pressQamA(int generation) {
|
||||
private synchronized void pressChordButton(int generation) {
|
||||
if (generation != qamChordGeneration || !qamChordActive) return;
|
||||
qamSyntheticAPressed = true;
|
||||
chordButtonPressed = true;
|
||||
publish();
|
||||
chordHandler.postDelayed(() -> releaseQamA(generation), QAM_A_HOLD_MS);
|
||||
chordHandler.postDelayed(() -> releaseChordButton(generation), QAM_A_HOLD_MS);
|
||||
}
|
||||
|
||||
private synchronized void releaseQamA(int generation) {
|
||||
private synchronized void releaseChordButton(int generation) {
|
||||
if (generation != qamChordGeneration || !qamChordActive) return;
|
||||
qamSyntheticAPressed = false;
|
||||
chordButtonPressed = false;
|
||||
publish();
|
||||
chordHandler.postDelayed(() -> releaseQamGuide(generation), QAM_GUIDE_TAIL_MS);
|
||||
}
|
||||
@@ -491,7 +497,7 @@ public final class PadBridge {
|
||||
effectiveState.copyFrom(state);
|
||||
if (guide) effectiveState.press(PadState.GUIDE, true);
|
||||
if (qam) effectiveState.press(PadState.QAM, true);
|
||||
if (qamSyntheticAPressed) effectiveState.press(PadState.A, true);
|
||||
if (chordButtonPressed) effectiveState.press(chordButton, true);
|
||||
writer.writePad(effectiveState);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.droiddeck.launcher.input
|
||||
|
||||
import android.view.MotionEvent
|
||||
import kotlin.math.hypot
|
||||
|
||||
class ThreeFingerTap(private val slopPx: Float) {
|
||||
var active = false
|
||||
private set
|
||||
|
||||
private val starts = HashMap<Int, Pair<Float, Float>>()
|
||||
private var downAt = 0L
|
||||
private var threeAt = 0L
|
||||
private var valid = false
|
||||
|
||||
fun onTouch(event: MotionEvent): Boolean {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
starts.clear()
|
||||
active = false
|
||||
valid = true
|
||||
downAt = event.eventTime
|
||||
note(event, event.actionIndex)
|
||||
}
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
note(event, event.actionIndex)
|
||||
if (event.pointerCount > 3) valid = false
|
||||
if (event.pointerCount == 3 && !active) {
|
||||
active = true
|
||||
threeAt = event.eventTime
|
||||
if (threeAt - downAt > LAND_MS) valid = false
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> for (i in 0 until event.pointerCount) {
|
||||
val start = starts[event.getPointerId(i)] ?: continue
|
||||
if (hypot(event.getX(i) - start.first, event.getY(i) - start.second) > slopPx) valid = false
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
val fired = active && valid && event.eventTime - threeAt < TAP_MS
|
||||
active = false
|
||||
return fired
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> active = false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun note(event: MotionEvent, index: Int) {
|
||||
starts[event.getPointerId(index)] = event.getX(index) to event.getY(index)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val LAND_MS = 250L
|
||||
const val TAP_MS = 350L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.droiddeck.launcher.input
|
||||
|
||||
import android.view.MotionEvent
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
class ThreeFingerTapTest {
|
||||
private val tap = ThreeFingerTap(8f)
|
||||
private var time = 1000L
|
||||
private var downTime = 0L
|
||||
private val a = 1 to (400f to 300f)
|
||||
private val b = 2 to (500f to 300f)
|
||||
private val c = 3 to (600f to 300f)
|
||||
|
||||
@Test fun quickThreeFingerTapFires() {
|
||||
assertEquals(listOf(false, false, false, false, false, true), threeFingerTap())
|
||||
}
|
||||
|
||||
@Test fun activeOnceTheThirdFingerLandsUntilTheLastLifts() {
|
||||
touch(MotionEvent.ACTION_DOWN, 0, a)
|
||||
touch(MotionEvent.ACTION_POINTER_DOWN, 1, a, b)
|
||||
assertFalse(tap.active)
|
||||
touch(MotionEvent.ACTION_POINTER_DOWN, 2, a, b, c)
|
||||
assertTrue(tap.active)
|
||||
touch(MotionEvent.ACTION_POINTER_UP, 2, a, b, c)
|
||||
touch(MotionEvent.ACTION_POINTER_UP, 1, a, b)
|
||||
assertTrue(tap.active)
|
||||
touch(MotionEvent.ACTION_UP, 0, a)
|
||||
assertFalse(tap.active)
|
||||
}
|
||||
|
||||
@Test fun twoFingerTapDoesNotFire() {
|
||||
touch(MotionEvent.ACTION_DOWN, 0, a)
|
||||
touch(MotionEvent.ACTION_POINTER_DOWN, 1, a, b)
|
||||
touch(MotionEvent.ACTION_POINTER_UP, 1, a, b)
|
||||
assertFalse(touch(MotionEvent.ACTION_UP, 0, a))
|
||||
}
|
||||
|
||||
@Test fun heldTooLongDoesNotFire() {
|
||||
assertFalse(threeFingerTap(hold = 600).last())
|
||||
}
|
||||
|
||||
@Test fun swipeDoesNotFire() {
|
||||
touch(MotionEvent.ACTION_DOWN, 0, a)
|
||||
touch(MotionEvent.ACTION_POINTER_DOWN, 1, a, b)
|
||||
touch(MotionEvent.ACTION_POINTER_DOWN, 2, a, b, c)
|
||||
touch(MotionEvent.ACTION_MOVE, 0, a, b, 3 to (600f to 400f))
|
||||
touch(MotionEvent.ACTION_POINTER_UP, 2, a, b, c)
|
||||
touch(MotionEvent.ACTION_POINTER_UP, 1, a, b)
|
||||
assertFalse(touch(MotionEvent.ACTION_UP, 0, a))
|
||||
}
|
||||
|
||||
@Test fun fingersLandingFarApartDoNotFire() {
|
||||
touch(MotionEvent.ACTION_DOWN, 0, a)
|
||||
time += 500
|
||||
touch(MotionEvent.ACTION_POINTER_DOWN, 1, a, b)
|
||||
touch(MotionEvent.ACTION_POINTER_DOWN, 2, a, b, c)
|
||||
touch(MotionEvent.ACTION_POINTER_UP, 2, a, b, c)
|
||||
touch(MotionEvent.ACTION_POINTER_UP, 1, a, b)
|
||||
assertFalse(touch(MotionEvent.ACTION_UP, 0, a))
|
||||
}
|
||||
|
||||
private fun threeFingerTap(hold: Long = 100): List<Boolean> {
|
||||
val out = mutableListOf<Boolean>()
|
||||
out += touch(MotionEvent.ACTION_DOWN, 0, a)
|
||||
time += 20
|
||||
out += touch(MotionEvent.ACTION_POINTER_DOWN, 1, a, b)
|
||||
time += 20
|
||||
out += touch(MotionEvent.ACTION_POINTER_DOWN, 2, a, b, c)
|
||||
time += hold
|
||||
out += touch(MotionEvent.ACTION_POINTER_UP, 2, a, b, c)
|
||||
out += touch(MotionEvent.ACTION_POINTER_UP, 1, a, b)
|
||||
out += touch(MotionEvent.ACTION_UP, 0, a)
|
||||
return out
|
||||
}
|
||||
|
||||
private fun touch(action: Int, index: Int, vararg pointers: Pair<Int, Pair<Float, Float>>): Boolean {
|
||||
if (action == MotionEvent.ACTION_DOWN) downTime = time
|
||||
val properties = pointers.map { (id, _) ->
|
||||
MotionEvent.PointerProperties().apply { this.id = id; toolType = MotionEvent.TOOL_TYPE_FINGER }
|
||||
}.toTypedArray()
|
||||
val coords = pointers.map { (_, at) ->
|
||||
MotionEvent.PointerCoords().apply { x = at.first; y = at.second; pressure = 1f; size = 1f }
|
||||
}.toTypedArray()
|
||||
val masked = action or (index shl MotionEvent.ACTION_POINTER_INDEX_SHIFT)
|
||||
val event = MotionEvent.obtain(downTime, time, masked, pointers.size, properties, coords, 0, 0, 1f, 1f, 0, 0, 0, 0)
|
||||
return tap.onTouch(event).also { event.recycle() }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user