From 012f9ce93c402a7bc2dff51013f5ea5cd9039464 Mon Sep 17 00:00:00 2001 From: El Date: Fri, 5 Jun 2026 08:47:51 +0000 Subject: [PATCH 1/9] VNC-393 Add native touch support with server-side handling and phone-style gestures. --- core/encodings.js | 1 + core/messages.js | 2 + core/rfb.js | 148 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 144 insertions(+), 7 deletions(-) diff --git a/core/encodings.js b/core/encodings.js index 4e6c97914..ad46ad2c1 100644 --- a/core/encodings.js +++ b/core/encodings.js @@ -40,6 +40,7 @@ export const encodings = { pseudoEncodingQOI: -1886, pseudoEncodingKasmDisconnectNotify: -1885, pseudoEncodingDirectMouse: -1884, + pseudoEncodingTouch: -1883, pseudoEncodingHardwareProfile0: -1170, pseudoEncodingHardwareProfile4: -1166, diff --git a/core/messages.js b/core/messages.js index f40f1d229..dc7b2af9c 100644 --- a/core/messages.js +++ b/core/messages.js @@ -4,6 +4,8 @@ export const messages = { msgTypeServerDisconnect: 186, msgTypeForceGameMode: 187, msgTypeDirectMouseEvent: 188, + msgTypeTouchSupported: 189, + msgTypeTouchEvent: 190, msgTypeUserAddedToSession: 253, msgTypeUserRemovedFromSession: 254 }; \ No newline at end of file diff --git a/core/rfb.js b/core/rfb.js index de4ee6c17..4eddbd10a 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -226,6 +226,13 @@ export default class RFB extends EventTargetMixin { this._gestureLastMagnitudeX = 0; this._gestureLastMagnitudeY = 0; + // Native (XInput2) touch state. When the server confirms touch support + // we stop synthesising gestures and instead forward raw touch points, + // letting touch-aware remote applications handle multitouch directly. + this._serverSupportsTouch = false; + this._nativeTouchActive = false; + this._activeTouchIds = new Set(); + // Secondary Displays this._supportsMultiMonitor = (typeof BroadcastChannel !== "undefined" && typeof SharedWorker !== "undefined"); if (this._supportsMultiMonitor) { @@ -249,6 +256,7 @@ export default class RFB extends EventTargetMixin { handlePointerLockError: this._handlePointerLockError.bind(this), handleWheel: this._handleWheel.bind(this), handleGesture: this._handleGesture.bind(this), + handleNativeTouch: this._handleNativeTouch.bind(this), handleFocusChange: this._handleFocusChange.bind(this), handleMouseOut: this._handleMouseOut.bind(this), handleVisibilityChange: this._handleVisibilityChange.bind(this), @@ -1391,6 +1399,14 @@ export default class RFB extends EventTargetMixin { this._canvas.addEventListener("gesturemove", this._eventHandlers.handleGesture); this._canvas.addEventListener("gestureend", this._eventHandlers.handleGesture); + // Native touch forwarding. Registered in the capture phase so that, + // once active, we can swallow the raw touch events before the gesture + // handler (attached in the bubble phase) ever sees them. + this._canvas.addEventListener("touchstart", this._eventHandlers.handleNativeTouch, true); + this._canvas.addEventListener("touchmove", this._eventHandlers.handleNativeTouch, true); + this._canvas.addEventListener("touchend", this._eventHandlers.handleNativeTouch, true); + this._canvas.addEventListener("touchcancel", this._eventHandlers.handleNativeTouch, true); + this._resendClipboardNextUserDrivenEvent = true; // WebRTC UDP datachannel inits @@ -1512,6 +1528,10 @@ export default class RFB extends EventTargetMixin { this._canvas.removeEventListener("gesturestart", this._eventHandlers.handleGesture); this._canvas.removeEventListener("gesturemove", this._eventHandlers.handleGesture); this._canvas.removeEventListener("gestureend", this._eventHandlers.handleGesture); + this._canvas.removeEventListener("touchstart", this._eventHandlers.handleNativeTouch, true); + this._canvas.removeEventListener("touchmove", this._eventHandlers.handleNativeTouch, true); + this._canvas.removeEventListener("touchend", this._eventHandlers.handleNativeTouch, true); + this._canvas.removeEventListener("touchcancel", this._eventHandlers.handleNativeTouch, true); this._canvas.removeEventListener("wheel", this._eventHandlers.handleWheel); this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse); this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse); @@ -2539,6 +2559,52 @@ export default class RFB extends EventTargetMixin { this._sendScroll(pointer.x, pointer.y, dX, dY); } + _handleNativeTouch(ev) { + // Only active once the server has confirmed touch support; otherwise + // the (still-attached) gesture handler owns these events. + if (!this._nativeTouchActive) { return; } + if (this._rfbConnectionState !== 'connected') { return; } + if (this._viewOnly) { return; } + if (!this._isPrimaryDisplay) { return; } + + // We own these touches now: stop the browser from scrolling, zooming + // or emulating mouse events. Propagation is left intact so the canvas + // can still grab focus for the on-screen keyboard. + ev.preventDefault(); + + const changed = ev.changedTouches; + for (let i = 0; i < changed.length; i++) { + const touch = changed[i]; + const pos = clientToElement(touch.clientX, touch.clientY, this._canvas); + const x = this._display.absX(pos.x); + const y = this._display.absY(pos.y); + // Browser touch identifiers can be large or negative; coerce to an + // unsigned 32-bit value to match the wire format. + const id = touch.identifier >>> 0; + + let state; + if (ev.type === 'touchstart') { + state = 0; // begin + this._activeTouchIds.add(id); + } else if (ev.type === 'touchmove') { + state = 1; // update + } else { + // touchend / touchcancel + state = 2; // end + this._activeTouchIds.delete(id); + } + + this._sendTouch(id, state, x, y); + } + } + + _sendTouch(id, state, x, y) { + if (this._rfbConnectionState !== 'connected') { return; } + if (this._viewOnly) { return; } + if (!this._isPrimaryDisplay) { return; } + RFB.messages.touchEvent(this._sock, id, state, x, y); + } + _fakeMouseMove(ev, elementX, elementY) { if (this._isPrimaryDisplay) { this._handleMouseMove(elementX, elementY, false, true); @@ -2611,13 +2677,24 @@ export default class RFB extends EventTargetMixin { this._handleTapEvent(ev, 0x2); break; case 'drag': - this._fakeMouseMove(ev, pos.x, pos.y); - - this._fakeMouseButton(pos.x, pos.y, true, 0x1); + if (this._phoneStyleTouch) { + // Phone style: a single-finger drag scrolls the + // view, so start tracking the finger position + // instead of pressing a button. + this._dragScrollLastX = pos.x; + this._dragScrollLastY = pos.y; + } else { + this._fakeMouseMove(ev, pos.x, pos.y); + this._fakeMouseButton(pos.x, pos.y, true, 0x1); + } break; case 'longpress': this._fakeMouseMove(ev, pos.x, pos.y); - this._fakeMouseButton(pos.x, pos.y, true, 0x4); + // Phone style: long-press initiates a left-button drag + // (move windows, select text). Otherwise keep the + // historic right-button hold. + this._fakeMouseButton(pos.x, pos.y, true, + this._phoneStyleTouch ? 0x1 : 0x4); break; case 'twodrag': @@ -2640,6 +2717,20 @@ export default class RFB extends EventTargetMixin { case 'threetap': break; case 'drag': + if (this._phoneStyleTouch) { + // Natural (direct) scrolling: dragging the content + // down reveals what is above it. The scroll delta + // is the negated finger movement, fed through the + // normal wheel path so any app scrolls. + this._sendScroll(pos.x, pos.y, + this._dragScrollLastX - pos.x, + this._dragScrollLastY - pos.y); + this._dragScrollLastX = pos.x; + this._dragScrollLastY = pos.y; + } else { + this._fakeMouseMove(ev, pos.x, pos.y); + } + break; case 'longpress': this._fakeMouseMove(ev, pos.x, pos.y); break; @@ -2702,12 +2793,16 @@ export default class RFB extends EventTargetMixin { case 'twodrag': break; case 'drag': - this._fakeMouseMove(ev, pos.x, pos.y); - this._fakeMouseButton(pos.x, pos.y, false, 0x1); + if (!this._phoneStyleTouch) { + this._fakeMouseMove(ev, pos.x, pos.y); + this._fakeMouseButton(pos.x, pos.y, false, 0x1); + } + // Phone style: scroll drag has no button to release. break; case 'longpress': this._fakeMouseMove(ev, pos.x, pos.y); - this._fakeMouseButton(pos.x, pos.y, false, 0x4); + this._fakeMouseButton(pos.x, pos.y, false, + this._phoneStyleTouch ? 0x1 : 0x4); break; } break; @@ -3310,6 +3405,9 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.pseudoEncodingExtendedClipboard); encs.push(encodings.pseudoEncodingKasmDisconnectNotify); encs.push(encodings.pseudoEncodingDirectMouse); + if (isTouchDevice) { + encs.push(encodings.pseudoEncodingTouch); + } if (this._hasWebp()) encs.push(encodings.pseudoEncodingWEBP); if (this._enableQOI) @@ -3850,6 +3948,9 @@ export default class RFB extends EventTargetMixin { case messages.msgTypeForceGameMode: return this._handleForceGameMode(); + case messages.msgTypeTouchSupported: + return this._handleTouchSupported(); + case messages.msgTypeVideoEncoders: return this._handleServerVideoEncoders(); @@ -4076,6 +4177,19 @@ export default class RFB extends EventTargetMixin { return true; } + _handleTouchSupported() { + // No payload — the server confirms touch support. We keep the gesture + // handler attached but switch it into a phone-style model: one finger + // scrolls, long-press drags. Scrolling is delivered as ordinary wheel + // events, so it works in every remote app, including ones that don't + // understand touch. + this._serverSupportsTouch = true; + if (isTouchDevice) { + Log.Info("Server supports touch; using phone-style touch gestures."); + } + return true; + } + _handleDisconnectNotify() { if (this._sock.rQwait("DisconnectNotify header", 8, 1)) { return false; } const flags = this._sock.rQshift8(); @@ -4704,6 +4818,26 @@ RFB.messages = { sock.flush(); }, + touchEvent(sock, id, type, x, y) { + const buff = sock._sQ; + const offset = sock._sQlen; + + // Wire format: type(1) + id(4, BE) + x(2, BE) + y(2, BE) + buff[offset] = messages.msgTypeTouchEvent; + buff[offset + 1] = type & 0xff; + buff[offset + 2] = (id >>> 24) & 0xff; + buff[offset + 3] = (id >>> 16) & 0xff; + buff[offset + 4] = (id >>> 8) & 0xff; + buff[offset + 5] = id & 0xff; + buff[offset + 6] = (x >> 8) & 0xff; + buff[offset + 7] = x & 0xff; + buff[offset + 8] = (y >> 8) & 0xff; + buff[offset + 9] = y & 0xff; + + sock._sQlen += 10; + sock.flush(); + }, + keepAlive(sock) { const buff = sock._sQ; const offset = sock._sQlen; From c313bf2838e465c07099af3f006a1af911a30e8c Mon Sep 17 00:00:00 2001 From: El Date: Wed, 10 Jun 2026 04:45:59 +0000 Subject: [PATCH 2/9] VNC-393 Refactor pointer, touch, and direct mouse event validation logic --- core/rfb.js | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 4eddbd10a..e3c0b476c 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -2470,9 +2470,8 @@ export default class RFB extends EventTargetMixin { } _sendMouse(x, y, mask) { - if (this._rfbConnectionState !== 'connected') { return; } - if (this._viewOnly) { return; } // View only, skip mouse events - if (!this._isPrimaryDisplay) { return; } + if (!this.isConnected || this._viewOnly || !this._isPrimaryDisplay) + return; if (this._pointerLock && this._directMouseEnabled) { // Direct drive: button state changes only (movement is sent raw from _handleMouse) @@ -2484,8 +2483,8 @@ export default class RFB extends EventTargetMixin { } _sendScroll(x, y, dX, dY) { - if (this._rfbConnectionState !== 'connected') { return; } - if (this._viewOnly) { return; } // View only, skip mouse events + if (!this.isConnected || this._viewOnly) // View only, skip mouse events + return; if (this._pointerLock && this._directMouseEnabled) { this._sendDirectMouse(0, 0, this._mouseButtonMask, dX, dY); @@ -2497,15 +2496,15 @@ export default class RFB extends EventTargetMixin { } _sendDirectMouse(dx, dy, buttonMask, scrollDX, scrollDY) { - if (this._rfbConnectionState !== 'connected') { return; } - if (this._viewOnly) { return; } - if (!this._isPrimaryDisplay) { return; } + if (!this.isConnected || this._viewOnly || !this._isPrimaryDisplay) + return; + RFB.messages.directMouseEvent(this._sock, dx, dy, buttonMask, scrollDX, scrollDY); } _handleWheel(ev) { - if (this._rfbConnectionState !== 'connected') { return; } - if (this._viewOnly) { return; } // View only, skip mouse events + if (!this.isConnected || this._viewOnly) // View only, skip mouse events + return; ev.stopPropagation(); ev.preventDefault(); @@ -2560,16 +2559,10 @@ export default class RFB extends EventTargetMixin { } _handleNativeTouch(ev) { - // Only active once the server has confirmed touch support; otherwise - // the (still-attached) gesture handler owns these events. - if (!this._nativeTouchActive) { return; } - if (this._rfbConnectionState !== 'connected') { return; } - if (this._viewOnly) { return; } - if (!this._isPrimaryDisplay) { return; } + // || !this._nativeTouchActive + if (!this.isConnected || this._viewOnly || !this._isPrimaryDisplay) + return; - // We own these touches now: stop the browser from scrolling, zooming - // or emulating mouse events. Propagation is left intact so the canvas - // can still grab focus for the on-screen keyboard. ev.preventDefault(); const changed = ev.changedTouches; @@ -2599,9 +2592,6 @@ export default class RFB extends EventTargetMixin { } _sendTouch(id, state, x, y) { - if (this._rfbConnectionState !== 'connected') { return; } - if (this._viewOnly) { return; } - if (!this._isPrimaryDisplay) { return; } RFB.messages.touchEvent(this._sock, id, state, x, y); } @@ -3405,9 +3395,8 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.pseudoEncodingExtendedClipboard); encs.push(encodings.pseudoEncodingKasmDisconnectNotify); encs.push(encodings.pseudoEncodingDirectMouse); - if (isTouchDevice) { + if (isTouchDevice) encs.push(encodings.pseudoEncodingTouch); - } if (this._hasWebp()) encs.push(encodings.pseudoEncodingWEBP); if (this._enableQOI) From e62a7688bbf3b03e5db8650912362caab3794e55 Mon Sep 17 00:00:00 2001 From: El Date: Wed, 10 Jun 2026 04:46:22 +0000 Subject: [PATCH 3/9] VNC-393 Return success status from codec configuration method --- core/rfb.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/rfb.js b/core/rfb.js index e3c0b476c..177b267cc 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -4157,6 +4157,8 @@ export default class RFB extends EventTargetMixin { configurations: codecConfigurations } })); + + return true; } _handleForceGameMode() { From 6b6f86d70b0d161faf86c9c3ee28d635f144fa62 Mon Sep 17 00:00:00 2001 From: El Date: Wed, 10 Jun 2026 04:46:35 +0000 Subject: [PATCH 4/9] VNC-393 Disable gesture handling when native touch support is active Remove deprecated phone-style touch handling logic temp --- core/rfb.js | 45 +++++++++------------------------------------ 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 177b267cc..4f186cde2 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -2650,6 +2650,8 @@ export default class RFB extends EventTargetMixin { } _handleGesture(ev) { + return; // native touch active — gestures disabled + let magnitude; let pos = clientToElement(ev.detail.clientX, ev.detail.clientY, @@ -2667,24 +2669,13 @@ export default class RFB extends EventTargetMixin { this._handleTapEvent(ev, 0x2); break; case 'drag': - if (this._phoneStyleTouch) { - // Phone style: a single-finger drag scrolls the - // view, so start tracking the finger position - // instead of pressing a button. - this._dragScrollLastX = pos.x; - this._dragScrollLastY = pos.y; - } else { - this._fakeMouseMove(ev, pos.x, pos.y); - this._fakeMouseButton(pos.x, pos.y, true, 0x1); - } + this._fakeMouseMove(ev, pos.x, pos.y); + + this._fakeMouseButton(pos.x, pos.y, true, 0x1); break; case 'longpress': this._fakeMouseMove(ev, pos.x, pos.y); - // Phone style: long-press initiates a left-button drag - // (move windows, select text). Otherwise keep the - // historic right-button hold. - this._fakeMouseButton(pos.x, pos.y, true, - this._phoneStyleTouch ? 0x1 : 0x4); + this._fakeMouseButton(pos.x, pos.y, true, 0x4); break; case 'twodrag': @@ -2707,20 +2698,6 @@ export default class RFB extends EventTargetMixin { case 'threetap': break; case 'drag': - if (this._phoneStyleTouch) { - // Natural (direct) scrolling: dragging the content - // down reveals what is above it. The scroll delta - // is the negated finger movement, fed through the - // normal wheel path so any app scrolls. - this._sendScroll(pos.x, pos.y, - this._dragScrollLastX - pos.x, - this._dragScrollLastY - pos.y); - this._dragScrollLastX = pos.x; - this._dragScrollLastY = pos.y; - } else { - this._fakeMouseMove(ev, pos.x, pos.y); - } - break; case 'longpress': this._fakeMouseMove(ev, pos.x, pos.y); break; @@ -2783,16 +2760,12 @@ export default class RFB extends EventTargetMixin { case 'twodrag': break; case 'drag': - if (!this._phoneStyleTouch) { - this._fakeMouseMove(ev, pos.x, pos.y); - this._fakeMouseButton(pos.x, pos.y, false, 0x1); - } - // Phone style: scroll drag has no button to release. + this._fakeMouseMove(ev, pos.x, pos.y); + this._fakeMouseButton(pos.x, pos.y, false, 0x1); break; case 'longpress': this._fakeMouseMove(ev, pos.x, pos.y); - this._fakeMouseButton(pos.x, pos.y, false, - this._phoneStyleTouch ? 0x1 : 0x4); + this._fakeMouseButton(pos.x, pos.y, false, 0x4); break; } break; From 5a1f156b27db96cab654f5ec60b8dd8d2af84afc Mon Sep 17 00:00:00 2001 From: El Date: Thu, 18 Jun 2026 10:34:40 +0000 Subject: [PATCH 5/9] VNC-393 Remove unused Babel plugin for dynamic imports --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 657ed6a1b..ea970e029 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "devDependencies": { "@babel/cli": "*", "@babel/core": "*", - "@babel/plugin-syntax-dynamic-import": "*", "@babel/plugin-transform-modules-commonjs": "*", "@babel/preset-env": "*", "@chiragrupani/karma-chromium-edge-launcher": "*", From 428eb63a2d78a81f92fadee6d9e390cb0b587705 Mon Sep 17 00:00:00 2001 From: El Date: Mon, 15 Jun 2026 01:11:13 +0000 Subject: [PATCH 6/9] VNC-393 Remove unused native touch handling logic --- core/rfb.js | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 4f186cde2..f5fabd9ad 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -226,13 +226,6 @@ export default class RFB extends EventTargetMixin { this._gestureLastMagnitudeX = 0; this._gestureLastMagnitudeY = 0; - // Native (XInput2) touch state. When the server confirms touch support - // we stop synthesising gestures and instead forward raw touch points, - // letting touch-aware remote applications handle multitouch directly. - this._serverSupportsTouch = false; - this._nativeTouchActive = false; - this._activeTouchIds = new Set(); - // Secondary Displays this._supportsMultiMonitor = (typeof BroadcastChannel !== "undefined" && typeof SharedWorker !== "undefined"); if (this._supportsMultiMonitor) { @@ -1399,9 +1392,6 @@ export default class RFB extends EventTargetMixin { this._canvas.addEventListener("gesturemove", this._eventHandlers.handleGesture); this._canvas.addEventListener("gestureend", this._eventHandlers.handleGesture); - // Native touch forwarding. Registered in the capture phase so that, - // once active, we can swallow the raw touch events before the gesture - // handler (attached in the bubble phase) ever sees them. this._canvas.addEventListener("touchstart", this._eventHandlers.handleNativeTouch, true); this._canvas.addEventListener("touchmove", this._eventHandlers.handleNativeTouch, true); this._canvas.addEventListener("touchend", this._eventHandlers.handleNativeTouch, true); @@ -2559,7 +2549,6 @@ export default class RFB extends EventTargetMixin { } _handleNativeTouch(ev) { - // || !this._nativeTouchActive if (!this.isConnected || this._viewOnly || !this._isPrimaryDisplay) return; @@ -2578,13 +2567,11 @@ export default class RFB extends EventTargetMixin { let state; if (ev.type === 'touchstart') { state = 0; // begin - this._activeTouchIds.add(id); } else if (ev.type === 'touchmove') { state = 1; // update } else { // touchend / touchcancel state = 2; // end - this._activeTouchIds.delete(id); } this._sendTouch(id, state, x, y); @@ -4142,16 +4129,11 @@ export default class RFB extends EventTargetMixin { } _handleTouchSupported() { - // No payload — the server confirms touch support. We keep the gesture - // handler attached but switch it into a phone-style model: one finger - // scrolls, long-press drags. Scrolling is delivered as ordinary wheel - // events, so it works in every remote app, including ones that don't - // understand touch. - this._serverSupportsTouch = true; - if (isTouchDevice) { + const result = isTouchDevice; + if (result) Log.Info("Server supports touch; using phone-style touch gestures."); - } - return true; + + return result; } _handleDisconnectNotify() { From bfbe37c63d8074298f78fdd11725ba516d19400b Mon Sep 17 00:00:00 2001 From: El Date: Thu, 18 Jun 2026 11:12:52 +0000 Subject: [PATCH 7/9] VNC-393 Update Babel devDependencies to use specific version ranges --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ea970e029..e31ef67cf 100644 --- a/package.json +++ b/package.json @@ -38,10 +38,10 @@ }, "homepage": "https://github.com/kasmtech/noVNC", "devDependencies": { - "@babel/cli": "*", - "@babel/core": "*", - "@babel/plugin-transform-modules-commonjs": "*", - "@babel/preset-env": "*", + "@babel/cli": "^7.0.0", + "@babel/core": "^7.0.0", + "@babel/plugin-transform-modules-commonjs": "^7.0.0", + "@babel/preset-env": "^7.0.0", "@chiragrupani/karma-chromium-edge-launcher": "*", "@interactjs/actions": "^1.10.27", "@interactjs/interact": "^1.10.27", From c7c62daba6057a9be3d194fcd9d4d3a693a8da85 Mon Sep 17 00:00:00 2001 From: El Date: Thu, 18 Jun 2026 12:18:52 +0000 Subject: [PATCH 8/9] VNC-393 Add configurable touch mode to support native and gesture handling --- app/ui.js | 9 +++++++++ core/rfb.js | 16 +++++++++++++++- index.html | 7 +++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/ui.js b/app/ui.js index 1ba0b19f1..dcb389a54 100644 --- a/app/ui.js +++ b/app/ui.js @@ -326,6 +326,7 @@ const UI = { UI.initSetting('enable_threading', true); UI.initSetting('virtual_keyboard_visible', false); UI.initSetting('enable_ime', false); + UI.initSetting('touch_mode', 'native'); UI.initSetting('enable_webrtc', false); UI.initSetting('enable_hidpi', false); UI.initSetting('fallback_image_mode', false); @@ -693,6 +694,8 @@ const UI = { UI.addSettingChangeHandler('virtual_keyboard_visible', UI.toggleKeyboardControls); UI.addSettingChangeHandler('enable_ime'); UI.addSettingChangeHandler('enable_ime', UI.toggleIMEMode); + UI.addSettingChangeHandler('touch_mode'); + UI.addSettingChangeHandler('touch_mode', UI.updateTouchMode); UI.addSettingChangeHandler('enable_webrtc'); UI.addSettingChangeHandler('enable_webrtc', UI.toggleWebRTC); UI.addSettingChangeHandler('enable_hidpi'); @@ -3132,6 +3135,12 @@ const UI = { } }, + updateTouchMode() { + if (UI.rfb) { + UI.rfb.touchMode = UI.getSetting('touch_mode'); + } + }, + toggleWebRTC() { if (UI.rfb) { if (typeof RTCPeerConnection === 'undefined') { diff --git a/core/rfb.js b/core/rfb.js index f5fabd9ad..7a20c43e6 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -175,6 +175,7 @@ export default class RFB extends EventTargetMixin { this._clipboardServerCapabilitiesFormats = {}; this._threading = true; + this._touchMode = 'native'; // Internal objects this._sock = null; // Websock object @@ -769,6 +770,13 @@ export default class RFB extends EventTargetMixin { } } + get touchMode() { return this._touchMode; } + set touchMode(value) { + if (value !== this._touchMode) { + this._touchMode = value; + } + } + get hwEncoderProfile() { return this._hwEncoderProfile; } set hwEncoderProfile(value) { if (value !== this._hwEncoderProfile) { @@ -2549,6 +2557,10 @@ export default class RFB extends EventTargetMixin { } _handleNativeTouch(ev) { + if (this._touchMode !== 'native') { + return; + } + if (!this.isConnected || this._viewOnly || !this._isPrimaryDisplay) return; @@ -2637,7 +2649,9 @@ export default class RFB extends EventTargetMixin { } _handleGesture(ev) { - return; // native touch active — gestures disabled + if (this._touchMode === 'native') { + return; + } let magnitude; diff --git a/index.html b/index.html index 53398e8a0..c9510f204 100644 --- a/index.html +++ b/index.html @@ -299,6 +299,13 @@

IME Input Mode +
  • + + +