diff --git a/app/src/androidTest/java/com/gaurav/avnc/ui/vnc/input/TouchHandlerTest.kt b/app/src/androidTest/java/com/gaurav/avnc/ui/vnc/input/TouchHandlerTest.kt
index 233180ff..37fc4fba 100644
--- a/app/src/androidTest/java/com/gaurav/avnc/ui/vnc/input/TouchHandlerTest.kt
+++ b/app/src/androidTest/java/com/gaurav/avnc/ui/vnc/input/TouchHandlerTest.kt
@@ -47,7 +47,19 @@ class TouchHandlerTest {
private val testPoint = PointF(10f, 10f)
@Before
- fun setup() {
+ fun setup() = setupWithPref()
+
+ private fun setupWithPref(
+ mousePassthrough: Boolean = false,
+ dragEnabled: Boolean = false,
+ doubleClickEnabled: Boolean = true,
+ ) {
+ targetPrefs.edit {
+ putBoolean("mouse_passthrough", mousePassthrough)
+ putString("gesture_drag", if (dragEnabled) "remote-scroll" else "none")
+ putString("gesture_double_tap", if (doubleClickEnabled) "double-click" else "two-left-clicks")
+ }
+
instrumentation.runOnMainSync {
mockDispatcher = mockk(relaxed = true)
touchHandler = TouchHandler(FrameView(targetConfigContext), mockDispatcher, AppPreferences(targetContext))
@@ -59,15 +71,6 @@ class TouchHandlerTest {
mockDispatcher.onXKey(0, 0, false)
}
-
- private fun setupWithPref(mousePassthrough: Boolean = false, dragEnabled: Boolean = false) {
- targetPrefs.edit {
- putBoolean("mouse_passthrough", mousePassthrough)
- putString("gesture_drag", if (dragEnabled) "remote-scroll" else "none")
- }
- setup()
- }
-
/************************* Finger Gestures *******************************************************/
@Test
@@ -78,6 +81,16 @@ class TouchHandlerTest {
verify { mockDispatcher.onTap1(testPoint) }
}
+ @Test
+ fun singleTapImmediatelySent() {
+ setupWithPref(doubleClickEnabled = false)
+
+ sendDown()
+ sendUp()
+
+ verify { mockDispatcher.onTap1(testPoint) }
+ }
+
@Test
fun doubleTap() {
sendDown()
@@ -88,6 +101,19 @@ class TouchHandlerTest {
verify { mockDispatcher.onDoubleTap(testPoint) }
}
+ @Test
+ fun doubleTapAsTwoClicks() {
+ setupWithPref(doubleClickEnabled = false)
+
+ sendDown()
+ sendUp()
+ Thread.sleep(Delay.BETWEEN_DOUBLE_TAPS)
+ sendDown()
+ sendUp()
+
+ verify(exactly = 2) { mockDispatcher.onTap1(testPoint) }
+ }
+
@Test
fun twoFingerTap() {
sendDown()
@@ -519,4 +545,4 @@ class TouchHandlerTest {
private fun sendStylusUp(p: PointF = testPoint) = sendEvent(Factory.obtainUpEvent(downEvent, p))
private fun sendStylusMove(p: PointF) = sendEvent(Factory.obtainMoveEvent(downEvent, p))
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/gaurav/avnc/ui/vnc/input/TouchHandler.kt b/app/src/main/java/com/gaurav/avnc/ui/vnc/input/TouchHandler.kt
index 90e295f7..9fe6fda8 100644
--- a/app/src/main/java/com/gaurav/avnc/ui/vnc/input/TouchHandler.kt
+++ b/app/src/main/java/com/gaurav/avnc/ui/vnc/input/TouchHandler.kt
@@ -218,7 +218,12 @@ class TouchHandler(private val frameView: FrameView, private val dispatcher: Dis
* Finger Gestures (and everything else beside mouse & stylus)
****************************************************************************************/
private val scaleDetector = ScaleGestureDetector(frameView.context, this).apply { isQuickScaleEnabled = false }
- private val gestureDetector = GestureDetectorEx(frameView.context, FingerGestureListener(), pref.input.gesture.longPressDetectionEnabled)
+ private val gestureDetector = GestureDetectorEx(
+ frameView.context,
+ FingerGestureListener(),
+ pref.input.gesture.longPressDetectionEnabled,
+ pref.input.gesture.doubleClickDetectionEnabled,
+ )
private val swipeVsScale = SwipeVsScale()
private val longPressSwipeEnabled = pref.input.gesture.longPressSwipeEnabled
private val swipeSensitivity = pref.input.gesture.swipeSensitivity
@@ -297,7 +302,12 @@ class TouchHandler(private val frameView: FrameView, private val dispatcher: Dis
* [GestureDetectorEx] is used to for this purpose. It internally uses stock
* [GestureDetector], and some custom event processing to detect more gestures.
*/
- private class GestureDetectorEx(context: Context, val listener: GestureListenerEx, val enableLongPress: Boolean) {
+ private class GestureDetectorEx(
+ context: Context,
+ val listener: GestureListenerEx,
+ val enableLongPress: Boolean,
+ enableDoubleClick: Boolean,
+ ) {
/**
* Detected gestures. Some of these come directly from stock [GestureDetector],
@@ -365,9 +375,22 @@ class TouchHandler(private val frameView: FrameView, private val dispatcher: Dis
* - (double-tap) (double-tap-swipe)
*
*/
- private val innerDetector1 = GestureDetector(context, InnerListener1())
- private val innerDetector2 = GestureDetector(context, InnerListener2()).apply { setIsLongpressEnabled(false) }
- private val innerDetector3 = GestureDetector(context, InnerListener3()).apply { setIsLongpressEnabled(false) }
+ private val innerListener1: InnerListener1 = when (enableDoubleClick) {
+ true -> DoubleTapListener1()
+ false -> RawListener1()
+ }
+ private val innerListener2: InnerListener2 = when (enableDoubleClick) {
+ true -> DoubleTapListener2()
+ false -> RawListener2()
+ }
+ private val innerListener3: InnerListener3 = when (enableDoubleClick) {
+ true -> DoubleTapListener3()
+ false -> RawListener3()
+ }
+
+ private val innerDetector1 = GestureDetector(context, innerListener1)
+ private val innerDetector2 = GestureDetector(context, innerListener2).apply { setIsLongpressEnabled(false) }
+ private val innerDetector3 = GestureDetector(context, innerListener3).apply { setIsLongpressEnabled(false) }
private var longPressDetected = false
private var doubleTapDetected = false
@@ -378,31 +401,54 @@ class TouchHandler(private val frameView: FrameView, private val dispatcher: Dis
private var cumulatedY = 0f
private val multiTapSlopSquare = 30 * 30
+ /**
+ * A [GestureDetector.OnGestureListener] just like
+ * [SimpleOnGestureListener], except it doesn’t implement
+ * [GestureDetector.OnDoubleTapListener].
+ */
+ private open class RawListener : GestureDetector.OnGestureListener {
+ override fun onDown(e: MotionEvent): Boolean = false
+ override fun onShowPress(e: MotionEvent) {}
+ override fun onSingleTapUp(e: MotionEvent): Boolean = false
+ override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float): Boolean = false
+ override fun onLongPress(e: MotionEvent) {}
+ override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean = false
+ }
- private inner class InnerListener1 : SimpleOnGestureListener() {
- override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
- listener.onSingleTapConfirmed(e)
- return true
- }
+ private interface InnerListener1 : GestureDetector.OnGestureListener
+
+ /**
+ * [InnerListener1] that waits for single taps to be confirmed, and thus
+ * can detect double taps.
+ */
+ private inner class DoubleTapListener1 : SimpleOnGestureListener(), InnerListener1 {
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean = handleSingleTap(e)
- override fun onLongPress(e: MotionEvent) {
- if (!enableLongPress)
- return
+ override fun onLongPress(e: MotionEvent) = handleLongPress(e)
- if (doubleTapDetected)
- return // Ignore long-press triggered during double-tap-swipe
+ override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean =
+ handleFling(velocityX, velocityY)
+ }
- longPressDetected = true
- listener.onLongPress(e)
- }
+ /**
+ * [InnerListener1] that doesn’t wait for single taps to be confirmed,
+ * and thus can send them immediately to the server.
+ */
+ private inner class RawListener1 : RawListener(), InnerListener1 {
+ override fun onSingleTapUp(e: MotionEvent): Boolean = handleSingleTap(e)
- override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
- listener.onFling(velocityX, velocityY)
- return true
- }
+ override fun onLongPress(e: MotionEvent) = handleLongPress(e)
+
+ override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean =
+ handleFling(velocityX, velocityY)
}
- private inner class InnerListener2 : SimpleOnGestureListener() {
+ private interface InnerListener2 : GestureDetector.OnGestureListener
+
+ /**
+ * [InnerListener2] that can detect double taps.
+ */
+ private inner class DoubleTapListener2 : SimpleOnGestureListener(), InnerListener2 {
override fun onDoubleTap(e: MotionEvent): Boolean {
doubleTapDetected = true
return true
@@ -413,10 +459,44 @@ class TouchHandler(private val frameView: FrameView, private val dispatcher: Dis
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float) = handleScroll(e1, e2, dx, dy)
}
- private inner class InnerListener3 : SimpleOnGestureListener() {
+ /**
+ * [InnerListener2] that handles double taps as two single taps.
+ */
+ private inner class RawListener2 : RawListener(), InnerListener2 {
+ override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float) = handleScroll(e1, e2, dx, dy)
+ }
+
+ private interface InnerListener3 : GestureDetector.OnGestureListener
+
+ private inner class DoubleTapListener3 : SimpleOnGestureListener(), InnerListener3 {
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float) = handleScroll(e1, e2, dx, dy)
}
+ private inner class RawListener3 : RawListener(), InnerListener3 {
+ override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float) = handleScroll(e1, e2, dx, dy)
+ }
+
+ private fun handleLongPress(e: MotionEvent) {
+ if (!enableLongPress)
+ return
+
+ if (doubleTapDetected)
+ return // Ignore long-press triggered during double-tap-swipe
+
+ longPressDetected = true
+ listener.onLongPress(e)
+ }
+
+ private fun handleSingleTap(e: MotionEvent): Boolean {
+ listener.onSingleTapConfirmed(e)
+ return true
+ }
+
+ private fun handleFling(velocityX: Float, velocityY: Float): Boolean {
+ listener.onFling(velocityX, velocityY)
+ return true
+ }
+
private fun handleScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float): Boolean {
e1 ?: return false
if (!scrolling) {
diff --git a/app/src/main/java/com/gaurav/avnc/util/AppPreferences.kt b/app/src/main/java/com/gaurav/avnc/util/AppPreferences.kt
index 0f69ab7e..c25acd83 100644
--- a/app/src/main/java/com/gaurav/avnc/util/AppPreferences.kt
+++ b/app/src/main/java/com/gaurav/avnc/util/AppPreferences.kt
@@ -55,6 +55,7 @@ class AppPreferences(context: Context) {
val swipe2; get() = prefs.getString("gesture_swipe2", "pan")!!
val swipe3; get() = prefs.getString("gesture_swipe3", "pan")!!
val doubleTapSwipe; get() = prefs.getString("gesture_double_tap_swipe", "remote-drag")!!
+ val doubleClickDetectionEnabled; get() = (doubleTap != "two-left-clicks")
val longPressSwipe; get() = prefs.getString("gesture_long_press_swipe", "none")!!
val longPressSwipeEnabled; get() = (longPressSwipe != "none" && longPress != "left-press")
val longPressDetectionEnabled; get() = (longPress != "none" || longPressSwipeEnabled)
@@ -177,4 +178,4 @@ class AppPreferences(context: Context) {
putBoolean("run_info_right_meta_keys_migrated", true)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 630e81f1..6a5d60ff 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -154,6 +154,7 @@
Par défaut
Sensibilité du défilement
Écran tactile\nEffectuer les actions au point de contact\n\nPavé tactile\nEffectuer les actions au niveau du pointeur
+ Aucun\nNe rien faire quand un appui double est détecté.\n\nDeux clics gauches\nNe pas détecter les appuis doubles, ce qui permet d’envoyer immédiatement les appuis simples au serveur.
L\'attribution d\'une action à ce geste modifiera la détection de la pression longue :
\n
\nAppuyer-tenir-lâcher → Appui long
@@ -240,4 +241,5 @@
Style de geste \'Touchpad\' est actif
Désactivé par la valeur actuelle du geste Pression longue
Faites glisser depuis le bord pour ouvrir
+ Deux clics gauches
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 98eed7fd..5ed692e7 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -75,12 +75,14 @@
- @string/pref_gesture_action_double_click
- @string/pref_gesture_action_middle_click
- @string/pref_gesture_action_right_click
+ - @string/pref_gesture_action_two_left_clicks
- none
- double-click
- middle-click
- right-click
+ - two-left-clicks
@@ -227,4 +229,4 @@
- Item 2
- Item 3
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b222d57e..4ccc6e42 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -111,6 +111,7 @@
Delaying click events can help in some rare cases if an app is not responding to clicks.
Wake-on-LAN can be used to remotely power-on a computer.\n\nFirst, configure WoL on remote computer, then enable it in AVNC.\nOnce enabled, WoL magic packet will be automatically sent before connecting to this server.
Touchscreen\nDo actions at touch-point \n\nTouchpad\nDo actions at pointer
+ None\nDon’t do anything when a double-tap is detected.\n\nTwo left-clicks\nDo not detect double taps, which allows sending single taps immediately to the server.
Assigning an action to this gesture will change Long press detection:\n\nPress-hold-release → Long press\nPress-hold-swipe → Long press and swipe
This server has been deleted
Server list is empty.\nClick \'+\' to add a server, or\nuse the top address bar to connect directly.
@@ -176,6 +177,7 @@
Drag with middle button
Back-press on server
Open keyboard
+ Two left-clicks
Theme
System
diff --git a/app/src/main/res/xml/pref_input.xml b/app/src/main/res/xml/pref_input.xml
index dc979997..43990bd6 100644
--- a/app/src/main/res/xml/pref_input.xml
+++ b/app/src/main/res/xml/pref_input.xml
@@ -24,12 +24,14 @@
app:useSimpleSummaryProvider="true"
app:widgetLayout="@layout/help_btn" />
-
-
\ No newline at end of file
+