diff --git a/app/images/touchpad.svg b/app/images/touchpad.svg new file mode 100644 index 000000000..0bdd7a1fe --- /dev/null +++ b/app/images/touchpad.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + diff --git a/app/styles/base.css b/app/styles/base.css index f83ad4b93..7a74ff7de 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -345,7 +345,10 @@ html { padding: 0 10px; } -#noVNC_control_bar > .noVNC_scroll > * { +/* Do not apply margin to #noVNC_mobilebuttons div. There is now + more than one button inside it, so we'll apply margins directly + to the buttons in another rule. */ +#noVNC_control_bar > .noVNC_scroll > *:not(#noVNC_mobile_buttons) { display: block; margin: 10px auto; } @@ -553,6 +556,12 @@ html { :root:not(.noVNC_connected) #noVNC_mobile_buttons { display: none; } + +#noVNC_mobile_buttons > .noVNC_button { + display: block; + margin: 10px auto; +} + @media not all and (any-pointer: coarse) { /* FIXME: The button for the virtual keyboard is the only button in this group of "mobile buttons". It is bad to assume that no touch diff --git a/app/ui.js b/app/ui.js index 85695ca2e..a7d2848ea 100644 --- a/app/ui.js +++ b/app/ui.js @@ -88,7 +88,7 @@ const UI = { }); // Adapt the interface for touch screen devices - if (isTouchDevice) { + if (isTouchDevice()) { // Remove the address bar setTimeout(() => window.scrollTo(0, 1), 100); } @@ -232,6 +232,9 @@ const UI = { document.getElementById("noVNC_view_drag_button") .addEventListener('click', UI.toggleViewDrag); + document.getElementById("noVNC_touchpad_button") + .addEventListener('click', UI.toggleTouchpadMode); + document.getElementById("noVNC_control_bar_handle") .addEventListener('mousedown', UI.controlbarHandleMouseDown); document.getElementById("noVNC_control_bar_handle") @@ -461,6 +464,12 @@ const UI = { .classList.remove('noVNC_open'); }, + /** + * @param {string} text + * @param { "normal" | "info" | "warn" | "warning" | "error" } statusType + * @param {number} time + * @returns + */ showStatus(text, statusType, time) { const statusElem = document.getElementById('noVNC_status'); @@ -1061,8 +1070,10 @@ const UI = { UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); UI.rfb.compressionLevel = parseInt(UI.getSetting('compression')); UI.rfb.showDotCursor = UI.getSetting('show_dot'); + UI.rfb.touchpadMode = WebUtil.readSetting('touchpad_mode', 'false') === 'true'; UI.updateViewOnly(); // requires UI.rfb + UI.updateTouchpadMode(); }, disconnect() { @@ -1116,6 +1127,12 @@ const UI = { // Do this last because it can only be used on rendered elements UI.rfb.focus(); + + // In touchpad mode, we want the cursor centered in the + // viewport at the start so we can see it. + if (UI.rfb.touchpadMode) { + UI.rfb.centerCursorInViewport(); + } }, disconnectFinished(e) { @@ -1345,7 +1362,7 @@ const UI = { // Can't be clipping if viewport is scaled to fit UI.forceSetting('view_clip', false); UI.rfb.clipViewport = false; - } else if (brokenScrollbars) { + } else if (brokenScrollbars || UI.rfb.touchpadMode) { UI.forceSetting('view_clip', true); UI.rfb.clipViewport = true; } else { @@ -1369,6 +1386,7 @@ const UI = { UI.rfb.dragViewport = !UI.rfb.dragViewport; UI.updateViewDrag(); + UI.updateTouchpadMode(); }, updateViewDrag() { @@ -1376,6 +1394,10 @@ const UI = { const viewDragButton = document.getElementById('noVNC_view_drag_button'); + if (UI.rfb.dragViewport) { + UI.rfb.touchpadMode = false; + } + if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) && UI.rfb.dragViewport) { // We are no longer clipping the viewport. Make sure @@ -1429,7 +1451,7 @@ const UI = { * ------v------*/ showVirtualKeyboard() { - if (!isTouchDevice) return; + if (!isTouchDevice()) return; const input = document.getElementById('noVNC_keyboardinput'); @@ -1447,7 +1469,7 @@ const UI = { }, hideVirtualKeyboard() { - if (!isTouchDevice) return; + if (!isTouchDevice()) return; const input = document.getElementById('noVNC_keyboardinput'); @@ -1586,11 +1608,52 @@ const UI = { } }, -/* ------^------- - * /KEYBOARD - * ============== - * EXTRA KEYS - * ------v------*/ + /* ------^------- + * /KEYBOARD + * ============== + * TOUCHPAD + * ------v------*/ + + toggleTouchpadMode() { + if (!UI.rfb) return; + + UI.rfb.touchpadMode = !UI.rfb.touchpadMode; + WebUtil.writeSetting('touchpad_mode', UI.rfb.touchpadMode); + UI.updateTouchpadMode(); + UI.updateViewDrag(); + }, + + updateTouchpadMode() { + if (UI.rfb.touchpadMode) { + UI.rfb.dragViewport = false; + + UI.forceSetting('resize', 'off'); + UI.forceSetting('view_clip', true); + UI.forceSetting('show_dot', true); + + UI.rfb.clipViewport = true; + UI.rfb.scaleViewport = false; + UI.rfb.resizeSession = false; + UI.rfb.showDotCursor = true; + } else { + UI.enableSetting('resize'); + UI.enableSetting('view_clip'); + UI.enableSetting('show_dot'); + } + + const touchpadButton = document.getElementById('noVNC_touchpad_button'); + if (UI.rfb.touchpadMode) { + touchpadButton.classList.add("noVNC_selected"); + } else { + touchpadButton.classList.remove("noVNC_selected"); + } + }, + + /* ------^------- + * /TOUCHPAD + * ============== + * EXTRA KEYS + * ------v------*/ openExtraKeys() { UI.closeAllPanels(); @@ -1704,12 +1767,14 @@ const UI = { .classList.add('noVNC_hidden'); document.getElementById('noVNC_clipboard_button') .classList.add('noVNC_hidden'); + document.getElementById('noVNC_clipboard_button') + .classList.add('noVNC_hidden'); } else { document.getElementById('noVNC_keyboard_button') .classList.remove('noVNC_hidden'); document.getElementById('noVNC_toggle_extra_keys_button') .classList.remove('noVNC_hidden'); - document.getElementById('noVNC_clipboard_button') + document.getElementById('noVNC_touchpad_button') .classList.remove('noVNC_hidden'); } }, diff --git a/core/display.js b/core/display.js index fcd626999..da1e3153e 100644 --- a/core/display.js +++ b/core/display.js @@ -87,8 +87,38 @@ export default class Display { return this._fbHeight; } + get viewportLocation() { + return this._viewportLoc; + } + // ===== PUBLIC METHODS ===== + /** + * Attempt to move the viewport by the specified amounts + * and returns the amount of actual position change. + * @param {number} moveByX + * @param {number} moveByY + * @return {{ x: number, y: number }} + */ + viewportTryMoveBy(moveByX, moveByY) { + if (moveByX === 0 && moveByY === 0) { + return { + x: 0, + y: 0 + }; + } + + const vpX = this._viewportLoc.x; + const vpY = this._viewportLoc.y; + + this.viewportChangePos(moveByX, moveByY); + + return { + x: this._viewportLoc.x - vpX, + y: this._viewportLoc.y - vpY + }; + } + viewportChangePos(deltaX, deltaY) { const vp = this._viewportLoc; deltaX = Math.floor(deltaX); @@ -433,6 +463,10 @@ export default class Display { this._rescale(scaleRatio); } + rescale(factor) { + this._rescale(factor); + } + // ===== PRIVATE METHODS ===== _rescale(factor) { diff --git a/core/input/gesturehandler.js b/core/input/gesturehandler.js index 6fa72d2aa..86eb6420f 100644 --- a/core/input/gesturehandler.js +++ b/core/input/gesturehandler.js @@ -18,7 +18,6 @@ const GH_PINCH = 64; const GH_INITSTATE = 127; -const GH_MOVE_THRESHOLD = 50; const GH_ANGLE_THRESHOLD = 90; // Degrees // Timeout when waiting for gestures (ms) @@ -38,6 +37,7 @@ export default class GestureHandler { this._target = null; this._state = GH_INITSTATE; + this._touchpadMode = false; this._tracked = []; this._ignored = []; @@ -51,6 +51,37 @@ export default class GestureHandler { this._boundEventHandler = this._eventHandler.bind(this); } + // ===== PROPERTIES ===== + + /** + * @returns {boolean} + */ + get touchpadMode() { + return this._touchpadMode; + } + + /** + * @param {boolean} enabled + */ + set touchpadMode(enabled) { + this._touchpadMode = enabled; + } + + /** + * @returns {number} + */ + get _ghMoveThreshold() { + // In TouchpadMode, we want movements to be very precise, + // so we'll reduce the movement threshold. + if (this._touchpadMode) { + return 5; + } + + return 50; + } + + // ===== PUBLIC METHODS ===== + attach(target) { this.detach(); @@ -64,7 +95,6 @@ export default class GestureHandler { this._target.addEventListener('touchcancel', this._boundEventHandler); } - detach() { if (!this._target) { return; @@ -84,6 +114,10 @@ export default class GestureHandler { this._target = null; } + /** + * + * @param {TouchEvent} e + */ _eventHandler(e) { let fn; @@ -102,7 +136,6 @@ export default class GestureHandler { fn = this._touchEnd; break; } - for (let i = 0; i < e.changedTouches.length; i++) { let touch = e.changedTouches[i]; fn.call(this, touch.identifier, touch.clientX, touch.clientY); @@ -142,7 +175,9 @@ export default class GestureHandler { firstY: y, lastX: x, lastY: y, - angle: 0 + movementX: 0, + movementY: 0, + angle: 0, }); switch (this._tracked.length) { @@ -173,6 +208,8 @@ export default class GestureHandler { } // Update the touches last position with the event coordinates + touch.movementX = x - touch.lastX; + touch.movementY = y - touch.lastY; touch.lastX = x; touch.lastY = y; @@ -187,7 +224,7 @@ export default class GestureHandler { if (!this._hasDetectedGesture()) { // Ignore moves smaller than the minimum threshold - if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) { + if (Math.hypot(deltaX, deltaY) < this._ghMoveThreshold) { return; } @@ -216,7 +253,7 @@ export default class GestureHandler { // We know that the current touch moved far enough, // but unless both touches moved further than their // threshold we don't want to disqualify any gestures - if (prevDeltaMove > GH_MOVE_THRESHOLD) { + if (prevDeltaMove > this._ghMoveThreshold) { // The angle difference between the direction of the touch points let deltaAngle = Math.abs(touch.angle - prevTouch.angle); @@ -458,6 +495,15 @@ export default class GestureHandler { detail['clientX'] = pos.x; detail['clientY'] = pos.y; + if (this._touchpadMode && + this._tracked.length === 1) { + + const touch = this._tracked[0]; + + detail['movementX'] = touch.movementX; + detail['movementY'] = touch.movementY; + } + // FIXME: other coordinates? // Some gestures also have a magnitude diff --git a/core/rfb.js b/core/rfb.js index c71d6b88f..93e1a5bbb 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -194,6 +194,10 @@ export default class RFB extends EventTargetMixin { this._gestureFirstDoubleTapEv = null; this._gestureLastMagnitudeX = 0; this._gestureLastMagnitudeY = 0; + this._isTouchpadDragging = false; + this._touchpadTapTimeoutId = null; + this._lastTouchpadPinchMagnitude = 0; + this._currentPinchScale = 1; // Bound event handlers this._eventHandlers = { @@ -290,6 +294,7 @@ export default class RFB extends EventTargetMixin { this._clippingViewport = false; this._scaleViewport = false; this._resizeSession = false; + this._touchpadMode = false; this._showDotCursor = false; if (options.showDotCursor !== undefined) { @@ -409,6 +414,24 @@ export default class RFB extends EventTargetMixin { } } + /** + * @returns {boolean} + */ + get touchpadMode() { + return this._touchpadMode; + } + + /** + * @param {boolean} enabled + */ + set touchpadMode(enabled) { + if (!this._gestures) { + return; + } + this._touchpadMode = enabled; + this._gestures.touchpadMode = enabled; + } + // ===== PUBLIC METHODS ===== disconnect() { @@ -529,6 +552,17 @@ export default class RFB extends EventTargetMixin { } } + centerCursorInViewport() { + const container = document.getElementById('noVNC_container'); + const containerBounds = container.getBoundingClientRect(); + const x = containerBounds.left + (containerBounds.width * .5); + const y = containerBounds.top + (containerBounds.height * .5); + this._cursor.move(x, y); + + const elementPos = clientToElement(x, y, this._canvas); + this._handleMouseMove(elementPos.x, elementPos.y); + } + getImageData() { return this._display.getImageData(); } @@ -1263,21 +1297,55 @@ export default class RFB extends EventTargetMixin { case 'gesturestart': switch (ev.detail.type) { case 'onetap': - this._handleTapEvent(ev, 0x1); + if (this._touchpadMode) { + this._handleTouchpadOneTapEvent(); + } else { + this._handleTapEvent(ev, 0x1); + } break; case 'twotap': + if (this._touchpadMode) { + this._sendTouchpadTwoTap(); + break; + } this._handleTapEvent(ev, 0x4); break; case 'threetap': + if (this._touchpadMode) { + this._sendTouchpadThreeTap(); + break; + } this._handleTapEvent(ev, 0x2); break; case 'drag': + // In TouchpadMode, we don't want to move the cursor + // at the start of dragging. It should remain at its + // current location. We'll only press the left mouse + // button if this is the second tap in a double-tap + // sequence. + if (this._touchpadMode) { + if (this._touchpadTapTimeoutId > 0) { + this._clearTouchpadTapTimeoutId(); + this._isTouchpadDragging = true; + this._mouseButtonMask = 0x1; + const cursorPos = this._getCursorPositionToCanvas(); + this._sendMouse(cursorPos.x, cursorPos.y, 0x1); + } + break; + } this._fakeMouseMove(ev, pos.x, pos.y); this._handleMouseButton(pos.x, pos.y, true, 0x1); break; case 'longpress': - this._fakeMouseMove(ev, pos.x, pos.y); - this._handleMouseButton(pos.x, pos.y, true, 0x4); + // In TouchpadMode, we want to start the right-click at the + // current cursor location. + if (this._touchpadMode) { + const cursorPos = this._getCursorPositionToCanvas(); + this._handleMouseButton(cursorPos.x, cursorPos.y, true, 0x4); + } else { + this._fakeMouseMove(ev, pos.x, pos.y); + this._handleMouseButton(pos.x, pos.y, true, 0x4); + } break; case 'twodrag': @@ -1286,8 +1354,15 @@ export default class RFB extends EventTargetMixin { this._fakeMouseMove(ev, pos.x, pos.y); break; case 'pinch': - this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX, - ev.detail.magnitudeY); + magnitude = Math.hypot( + ev.detail.magnitudeX, + ev.detail.magnitudeY); + + if (this._touchpadMode) { + this._lastTouchpadPinchMagnitude = magnitude; + break; + } + this._gestureLastMagnitudeX = magnitude; this._fakeMouseMove(ev, pos.x, pos.y); break; } @@ -1301,13 +1376,21 @@ export default class RFB extends EventTargetMixin { break; case 'drag': case 'longpress': - this._fakeMouseMove(ev, pos.x, pos.y); + // In TouchpadMode, we want to move the cursor from its + // current position, not to where the touch currently is. + if (this._touchpadMode) { + this._handleTouchpadMove(ev.detail.movementX, ev.detail.movementY); + } else { + this._fakeMouseMove(ev, pos.x, pos.y); + } break; case 'twodrag': // Always scroll in the same position. // We don't know if the mouse was moved so we need to move it // every update. - this._fakeMouseMove(ev, pos.x, pos.y); + if (!this._touchpadMode) { + this._fakeMouseMove(ev, pos.x, pos.y); + } while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) { this._handleMouseButton(pos.x, pos.y, true, 0x8); this._handleMouseButton(pos.x, pos.y, false, 0x8); @@ -1330,11 +1413,18 @@ export default class RFB extends EventTargetMixin { } break; case 'pinch': + magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY); + + if (this._touchpadMode) { + this._handleTouchpadPinchZoom(magnitude); + break; + } + // Always scroll in the same position. // We don't know if the mouse was moved so we need to move it // every update. this._fakeMouseMove(ev, pos.x, pos.y); - magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY); + if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) { this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) { @@ -1362,18 +1452,208 @@ export default class RFB extends EventTargetMixin { case 'twodrag': break; case 'drag': + if (this._touchpadMode) { + if (this._isTouchpadDragging) { + this._mouseButtonMask = 0; + const cursorPos = this._getCursorPositionToCanvas(); + this._sendMouse(cursorPos.x, cursorPos.y, 0); + this._isTouchpadDragging = false; + } + break; + } this._fakeMouseMove(ev, pos.x, pos.y); this._handleMouseButton(pos.x, pos.y, false, 0x1); break; case 'longpress': - this._fakeMouseMove(ev, pos.x, pos.y); - this._handleMouseButton(pos.x, pos.y, false, 0x4); + // In TouchPad mode, we want to finish at the current cursor location. + if (this._touchpadMode) { + const cursorPos = this._getCursorPositionToCanvas(); + this._handleMouseButton(cursorPos.x, cursorPos.y, false, 0x4); + } else { + this._fakeMouseMove(ev, pos.x, pos.y); + this._handleMouseButton(pos.x, pos.y, false, 0x4); + } break; } break; } } + // TouchpadMode Private Methods + + /** + * @param {number} movementX + * @param {number} movementY + */ + _handleTouchpadMove(movementX, movementY) { + + // Add a multiplier to higher-velocity movements to + // traverse the screen quicker. + const xMultiplier = Math.max(5, Math.abs(movementX)) / 5; + movementX *= Math.min(xMultiplier, 4); + + const yMultiplier = Math.max(5, Math.abs(movementY)) / 5; + movementY *= Math.min(yMultiplier, 4); + + // Get the desired new location for the cursor. + let cursorPos = this._cursor.position; + let targetX = cursorPos.x + movementX; + let targetY = cursorPos.y + movementY; + + // Constrain the location to the canvas bounds. + const canvasBounds = this._canvas.getBoundingClientRect(); + const safeX = Math.max(canvasBounds.left, Math.min(targetX, canvasBounds.right)); + const safeY = Math.max(canvasBounds.top, Math.min(targetY, canvasBounds.bottom)); + + // See if the cursor has moved outside the center deadzone. + const deadzone = this._getTouchpadCursorDeadZone(); + const moveViewportX = Math.min(safeX - deadzone.left, 0) + + Math.max(safeX - deadzone.right, 0); + + const moveViewportY = Math.min(safeY - deadzone.top, 0) + + Math.max(safeY - deadzone.bottom, 0); + + // Try moving the viewport, getting the actual amount it moved. + const viewportChange = this._display.viewportTryMoveBy(moveViewportX, moveViewportY); + + // Subtract the viewport position change from the target + // cursor position. This will cause it to stay at the + // edge of the deadzone if we're pushing against it, or + // move past it to the edge of the screen if the viewport + // can pan no further. + this._cursor.move(safeX - viewportChange.x, safeY - viewportChange.y); + + // Finally, translate the coordinates to those relative to the + // canvas and send the pointer move event to the remote machine. + const posFromCanvas = clientToElement(safeX, safeY, this._canvas); + const hotspot = this._cursor.hotspot; + this._sendMouse( + posFromCanvas.x + hotspot.x, + posFromCanvas.y + hotspot.y, + this._mouseButtonMask); + } + + _handleTouchpadOneTapEvent() { + if (this._touchpadTapTimeoutId > 0) { + // A double-tap occurred. + this._clearTouchpadTapTimeoutId(); + this._sendTouchpadTap(); + this._sendTouchpadTap(); + return; + } + + this._touchpadTapTimeoutId = window.setTimeout(() => { + this._clearTouchpadTapTimeoutId(); + this._sendTouchpadTap(); + }, 250); + + } + + /** + * + * @param {number} magnitude + */ + _handleTouchpadPinchZoom(magnitude) { + if (this._lastTouchpadPinchMagnitude > 0) { + // Calculate the new pinch scale. + const container = document.getElementById('noVNC_container'); + const magnitudeChange = this._lastTouchpadPinchMagnitude / magnitude; + const newScale = this._currentPinchScale * magnitudeChange; + this._currentPinchScale = Math.max(.25, Math.min(4, newScale)); + + // Capture the current viewport size. + const originalVpW = this._display.viewportLocation.w; + const originalVpH = this._display.viewportLocation.h; + + // Change viewport size based on new scale. + const newWidth = container.clientWidth * this._currentPinchScale; + const newHeight = container.clientHeight * this._currentPinchScale; + this._display.viewportChangeSize(newWidth, newHeight); + + // Apply scaling to CSS. + const visualScale = container.clientWidth / newWidth; + this._display.rescale(visualScale); + + // Adjust viewport location to keep it centered. + const moveX = (originalVpW - this._display.viewportLocation.w) / 2; + const moveY = (originalVpH - this._display.viewportLocation.h) / 2; + this._display.viewportChangePos(moveX, moveY); + } + this._lastTouchpadPinchMagnitude = magnitude; + } + + _clearTouchpadTapTimeoutId() { + window.clearTimeout(this._touchpadTapTimeoutId); + this._touchpadTapTimeoutId = 0; + } + + _sendTouchpadTap() { + const cursorPos = this._getCursorPositionToCanvas(); + this._sendMouse(cursorPos.x, cursorPos.y, 0x1); + this._sendMouse(cursorPos.x, cursorPos.y, 0); + this._mouseButtonMask = 0; + } + _sendTouchpadTwoTap() { + this._clearTouchpadTapTimeoutId(); + const cursorPos = this._getCursorPositionToCanvas(); + this._sendMouse(cursorPos.x, cursorPos.y, 0x4); + this._sendMouse(cursorPos.x, cursorPos.y, 0); + this._mouseButtonMask = 0; + } + + _sendTouchpadThreeTap() { + this._clearTouchpadTapTimeoutId(); + const cursorPos = this._getCursorPositionToCanvas(); + this._sendMouse(cursorPos.x, cursorPos.y, 0x2); + this._sendMouse(cursorPos.x, cursorPos.y, 0); + this._mouseButtonMask = 0; + } + + /** + * Gets the current cursor position, offset by the canvas client bounds. + * @returns {{x: number, y: number}} + */ + _getCursorPositionToCanvas() { + const cursorPos = { + x: this._cursor.position.x + this._cursor.hotspot.x, + y: this._cursor.position.y + this._cursor.hotspot.y + }; + return clientToElement(cursorPos.x, cursorPos.y, this._canvas); + } + + /** + * Returns the center area within the canvas bounds where + * cursor movement won't trigger viewport movement. + * @returns {{ + * top: number, + * bottom: number, + * left: number, + * right: number, + * width: number, + * height: number + * }} + */ + _getTouchpadCursorDeadZone() { + const canvasBounds = this._canvas.getBoundingClientRect(); + const canvasCenter = { + x: canvasBounds.width * .5, + y: canvasBounds.height * .5 + }; + const xFromCenter = canvasBounds.width * .1; + const yFromCenter = canvasBounds.height * .1; + const innerWidth = xFromCenter * 2; + const innerHeight = yFromCenter * 2; + + return { + top: canvasCenter.y - yFromCenter, + bottom: canvasCenter.y + yFromCenter, + height: innerHeight, + left: canvasCenter.x - xFromCenter, + right: canvasCenter.x + xFromCenter, + width: innerWidth + }; + } + // Message Handlers _negotiateProtocolVersion() { diff --git a/core/util/browser.js b/core/util/browser.js index bbc9f5c1e..6ba667d10 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -11,17 +11,26 @@ import * as Log from './logging.js'; // Touch detection -export let isTouchDevice = ('ontouchstart' in document.documentElement) || - // requried for Chrome debugger - (document.ontouchstart !== undefined) || - // required for MS Surface - (navigator.maxTouchPoints > 0) || - (navigator.msMaxTouchPoints > 0); +let _touchEventOccurred = false; window.addEventListener('touchstart', function onFirstTouch() { - isTouchDevice = true; + _touchEventOccurred = true; window.removeEventListener('touchstart', onFirstTouch, false); }, false); +// This needs to be a function to allow the exported value +// to update if touchstart event fires. Also, the other +// values are dynamic and can change without a page reload +// (e.g. opening the emulator in dev tools), so we don't want +// to assign them to a variable that captures their current value. +export function isTouchDevice() { + return _touchEventOccurred || + ('ontouchstart' in document.documentElement) || + // requried for Chrome debugger + (document.ontouchstart !== undefined) || + // required for MS Surface + (navigator.maxTouchPoints > 0) || + (navigator.msMaxTouchPoints > 0); +} // The goal is to find a certain physical width, the devicePixelRatio // brings us a bit closer but is not optimal. diff --git a/core/util/cursor.js b/core/util/cursor.js index 3000cf0e6..9f24dff10 100644 --- a/core/util/cursor.js +++ b/core/util/cursor.js @@ -6,7 +6,7 @@ import { supportsCursorURIs, isTouchDevice } from './browser.js'; -const useFallback = !supportsCursorURIs || isTouchDevice; +const useFallback = () => !supportsCursorURIs || isTouchDevice(); export default class Cursor { constructor() { @@ -14,7 +14,7 @@ export default class Cursor { this._canvas = document.createElement('canvas'); - if (useFallback) { + if (useFallback()) { this._canvas.style.position = 'fixed'; this._canvas.style.zIndex = '65535'; this._canvas.style.pointerEvents = 'none'; @@ -37,6 +37,20 @@ export default class Cursor { }; } + /** + * @returns {{ x: number, y: number }} + */ + get position() { + return this._position; + } + + /** + * @returns {{ x: number, y: number }} + */ + get hotspot() { + return this._hotSpot; + } + attach(target) { if (this._target) { this.detach(); @@ -44,7 +58,7 @@ export default class Cursor { this._target = target; - if (useFallback) { + if (useFallback()) { document.body.appendChild(this._canvas); const options = { capture: true, passive: true }; @@ -62,7 +76,7 @@ export default class Cursor { return; } - if (useFallback) { + if (useFallback()) { const options = { capture: true, passive: true }; this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options); this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options); @@ -95,7 +109,7 @@ export default class Cursor { ctx.clearRect(0, 0, w, h); ctx.putImageData(img, 0, 0); - if (useFallback) { + if (useFallback()) { this._updatePosition(); } else { let url = this._canvas.toDataURL(); @@ -116,7 +130,7 @@ export default class Cursor { // Mouse events might be emulated, this allows // moving the cursor in such cases move(clientX, clientY) { - if (!useFallback) { + if (!useFallback()) { return; } // clientX/clientY are relative the _visual viewport_, diff --git a/vnc.html b/vnc.html index 24a118dbd..6ac6e63b8 100644 --- a/vnc.html +++ b/vnc.html @@ -79,6 +79,9 @@

no
VNC

+ +