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 +