Implement TouchpadMode gesture handling.
This commit is contained in:
parent
810d294b18
commit
6175985967
58
app/ui.js
58
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);
|
||||
}
|
||||
|
|
@ -464,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');
|
||||
|
||||
|
|
@ -1064,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() {
|
||||
|
|
@ -1119,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) {
|
||||
|
|
@ -1348,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 {
|
||||
|
|
@ -1372,6 +1386,7 @@ const UI = {
|
|||
|
||||
UI.rfb.dragViewport = !UI.rfb.dragViewport;
|
||||
UI.updateViewDrag();
|
||||
UI.updateTouchpadMode();
|
||||
},
|
||||
|
||||
updateViewDrag() {
|
||||
|
|
@ -1379,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
|
||||
|
|
@ -1432,7 +1451,7 @@ const UI = {
|
|||
* ------v------*/
|
||||
|
||||
showVirtualKeyboard() {
|
||||
if (!isTouchDevice) return;
|
||||
if (!isTouchDevice()) return;
|
||||
|
||||
const input = document.getElementById('noVNC_keyboardinput');
|
||||
|
||||
|
|
@ -1450,7 +1469,7 @@ const UI = {
|
|||
},
|
||||
|
||||
hideVirtualKeyboard() {
|
||||
if (!isTouchDevice) return;
|
||||
if (!isTouchDevice()) return;
|
||||
|
||||
const input = document.getElementById('noVNC_keyboardinput');
|
||||
|
||||
|
|
@ -1599,12 +1618,33 @@ const UI = {
|
|||
if (!UI.rfb) return;
|
||||
|
||||
UI.rfb.touchpadMode = !UI.rfb.touchpadMode;
|
||||
UI.updateTouchpadButton();
|
||||
WebUtil.writeSetting('touchpad_mode', UI.rfb.touchpadMode);
|
||||
UI.updateTouchpadMode();
|
||||
UI.updateViewDrag();
|
||||
},
|
||||
|
||||
updateTouchpadButton() {
|
||||
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');
|
||||
}
|
||||
|
||||
UI.updateViewDrag
|
||||
|
||||
const touchpadButton = document.getElementById('noVNC_touchpad_button');
|
||||
|
||||
if (UI.rfb.touchpadMode) {
|
||||
touchpadButton.classList.add("noVNC_selected");
|
||||
} else {
|
||||
|
|
@ -1730,12 +1770,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');
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,9 +175,11 @@ export default class GestureHandler {
|
|||
firstY: y,
|
||||
lastX: x,
|
||||
lastY: y,
|
||||
angle: 0
|
||||
movementX: 0,
|
||||
movementY: 0,
|
||||
angle: 0,
|
||||
});
|
||||
|
||||
|
||||
switch (this._tracked.length) {
|
||||
case 1:
|
||||
this._startLongpressTimeout();
|
||||
|
|
@ -164,7 +199,7 @@ export default class GestureHandler {
|
|||
}
|
||||
}
|
||||
|
||||
_touchMove(id, x, y) {
|
||||
_touchMove(id, x, y) {
|
||||
let touch = this._tracked.find(t => t.id === id);
|
||||
|
||||
// If this is an update for a touch we're not tracking, ignore it
|
||||
|
|
@ -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
|
||||
|
|
|
|||
302
core/rfb.js
302
core/rfb.js
|
|
@ -188,12 +188,16 @@ export default class RFB extends EventTargetMixin {
|
|||
this._viewportHasMoved = false;
|
||||
this._accumulatedWheelDeltaX = 0;
|
||||
this._accumulatedWheelDeltaY = 0;
|
||||
|
||||
|
||||
// Gesture state
|
||||
this._gestureLastTapTime = null;
|
||||
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,7 +294,7 @@ export default class RFB extends EventTargetMixin {
|
|||
this._clippingViewport = false;
|
||||
this._scaleViewport = false;
|
||||
this._resizeSession = false;
|
||||
this.touchpadMode = false;
|
||||
this._touchpadMode = false;
|
||||
|
||||
this._showDotCursor = false;
|
||||
if (options.showDotCursor !== undefined) {
|
||||
|
|
@ -409,6 +413,24 @@ export default class RFB extends EventTargetMixin {
|
|||
this._sendEncodings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 =====
|
||||
|
||||
|
|
@ -530,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();
|
||||
}
|
||||
|
|
@ -1264,21 +1297,57 @@ 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':
|
||||
|
|
@ -1287,8 +1356,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;
|
||||
}
|
||||
|
|
@ -1302,13 +1378,22 @@ 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);
|
||||
|
|
@ -1331,11 +1416,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) {
|
||||
|
|
@ -1363,18 +1455,204 @@ 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);
|
||||
this._sendMouse(posFromCanvas.x, posFromCanvas.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 = this._cursor.position;
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,13 @@ export default class Cursor {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ x: number, y: number }}
|
||||
*/
|
||||
get position() {
|
||||
return this._position;
|
||||
}
|
||||
|
||||
attach(target) {
|
||||
if (this._target) {
|
||||
this.detach();
|
||||
|
|
@ -44,7 +51,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 +69,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 +102,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 +123,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_,
|
||||
|
|
|
|||
Loading…
Reference in New Issue