Merge a7f4d708ca into 2f1e11b54a
This commit is contained in:
commit
c0dc4d0bce
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="25"
|
||||||
|
height="25"
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
id="svg2"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||||
|
sodipodi:docname="pointer.svg"
|
||||||
|
inkscape:export-filename="/home/ossman/devel/noVNC/images/keyboard.png"
|
||||||
|
inkscape:export-xdpi="90"
|
||||||
|
inkscape:export-ydpi="90">
|
||||||
|
<defs
|
||||||
|
id="defs4" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#717171"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="22.627417"
|
||||||
|
inkscape:cx="6.9841519"
|
||||||
|
inkscape:cy="18.584699"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:snap-bbox="true"
|
||||||
|
inkscape:bbox-paths="true"
|
||||||
|
inkscape:bbox-nodes="true"
|
||||||
|
inkscape:snap-bbox-edge-midpoints="true"
|
||||||
|
inkscape:snap-bbox-midpoints="false"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1403"
|
||||||
|
inkscape:window-x="2560"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:object-paths="true"
|
||||||
|
inkscape:snap-intersection-paths="true"
|
||||||
|
inkscape:object-nodes="true"
|
||||||
|
inkscape:snap-midpoints="true"
|
||||||
|
inkscape:snap-smooth-nodes="true"
|
||||||
|
inkscape:document-rotation="0">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid4136" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata7">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-1027.3622)">
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 6.3910823,1030.3965 v 17.497 l 3.5465954,-2.6671 1.5862113,4.1015 4.336661,-1.5752 -1.59331,-4.2624 4.341678,-0.3562 z"
|
||||||
|
id="path879" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
51
app/ui.js
51
app/ui.js
|
|
@ -232,6 +232,10 @@ const UI = {
|
||||||
document.getElementById("noVNC_view_drag_button")
|
document.getElementById("noVNC_view_drag_button")
|
||||||
.addEventListener('click', UI.toggleViewDrag);
|
.addEventListener('click', UI.toggleViewDrag);
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById("noVNC_pointer_lock_button")
|
||||||
|
.addEventListener("click", UI.requestPointerLock);
|
||||||
|
|
||||||
document.getElementById("noVNC_control_bar_handle")
|
document.getElementById("noVNC_control_bar_handle")
|
||||||
.addEventListener('mousedown', UI.controlbarHandleMouseDown);
|
.addEventListener('mousedown', UI.controlbarHandleMouseDown);
|
||||||
document.getElementById("noVNC_control_bar_handle")
|
document.getElementById("noVNC_control_bar_handle")
|
||||||
|
|
@ -453,6 +457,7 @@ const UI = {
|
||||||
UI.updatePowerButton();
|
UI.updatePowerButton();
|
||||||
UI.keepControlbar();
|
UI.keepControlbar();
|
||||||
}
|
}
|
||||||
|
UI.updatePointerLockButton();
|
||||||
|
|
||||||
// State change closes dialogs as they may not be relevant
|
// State change closes dialogs as they may not be relevant
|
||||||
// anymore
|
// anymore
|
||||||
|
|
@ -1051,6 +1056,7 @@ const UI = {
|
||||||
UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
|
UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
|
||||||
UI.rfb.addEventListener("bell", UI.bell);
|
UI.rfb.addEventListener("bell", UI.bell);
|
||||||
UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
|
UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
|
||||||
|
UI.rfb.addEventListener("inputlock", UI.inputLockChanged);
|
||||||
UI.rfb.clipViewport = UI.getSetting('view_clip');
|
UI.rfb.clipViewport = UI.getSetting('view_clip');
|
||||||
UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
|
UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
|
||||||
UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
|
UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
|
||||||
|
|
@ -1293,6 +1299,7 @@ const UI = {
|
||||||
document.getElementById('noVNC_fullscreen_button')
|
document.getElementById('noVNC_fullscreen_button')
|
||||||
.classList.remove("noVNC_selected");
|
.classList.remove("noVNC_selected");
|
||||||
}
|
}
|
||||||
|
UI.updatePointerLockButton();
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ------^-------
|
/* ------^-------
|
||||||
|
|
@ -1345,6 +1352,38 @@ const UI = {
|
||||||
/* ------^-------
|
/* ------^-------
|
||||||
* /VIEW CLIPPING
|
* /VIEW CLIPPING
|
||||||
* ==============
|
* ==============
|
||||||
|
* POINTER LOCK
|
||||||
|
* ------v------*/
|
||||||
|
|
||||||
|
updatePointerLockButton() {
|
||||||
|
// Only show the button if the pointer lock API is properly supported
|
||||||
|
// AND in fullscreen.
|
||||||
|
if (
|
||||||
|
UI.connected &&
|
||||||
|
(document.fullscreenElement || // alternative standard method
|
||||||
|
document.mozFullScreenElement || // currently working methods
|
||||||
|
document.webkitFullscreenElement ||
|
||||||
|
document.msFullscreenElement) &&
|
||||||
|
(document.pointerLockElement !== undefined ||
|
||||||
|
document.mozPointerLockElement !== undefined)
|
||||||
|
) {
|
||||||
|
document
|
||||||
|
.getElementById("noVNC_pointer_lock_button")
|
||||||
|
.classList.remove("noVNC_hidden");
|
||||||
|
} else {
|
||||||
|
document
|
||||||
|
.getElementById("noVNC_pointer_lock_button")
|
||||||
|
.classList.add("noVNC_hidden");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
requestPointerLock() {
|
||||||
|
UI.rfb.requestInputLock({ pointer: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ------^-------
|
||||||
|
* /POINTER LOCK
|
||||||
|
* ==============
|
||||||
* VIEWDRAG
|
* VIEWDRAG
|
||||||
* ------v------*/
|
* ------v------*/
|
||||||
|
|
||||||
|
|
@ -1710,6 +1749,18 @@ const UI = {
|
||||||
document.title = e.detail.name + " - " + PAGE_TITLE;
|
document.title = e.detail.name + " - " + PAGE_TITLE;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
inputLockChanged(e) {
|
||||||
|
if (e.detail.pointer) {
|
||||||
|
document
|
||||||
|
.getElementById("noVNC_pointer_lock_button")
|
||||||
|
.classList.add("noVNC_selected");
|
||||||
|
} else {
|
||||||
|
document
|
||||||
|
.getElementById("noVNC_pointer_lock_button")
|
||||||
|
.classList.remove("noVNC_selected");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
bell(e) {
|
bell(e) {
|
||||||
if (WebUtil.getConfigVar('bell', 'on') === 'on') {
|
if (WebUtil.getConfigVar('bell', 'on') === 'on') {
|
||||||
const promise = document.getElementById('noVNC_bell').play();
|
const promise = document.getElementById('noVNC_bell').play();
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export const encodings = {
|
||||||
pseudoEncodingCompressLevel9: -247,
|
pseudoEncodingCompressLevel9: -247,
|
||||||
pseudoEncodingCompressLevel0: -256,
|
pseudoEncodingCompressLevel0: -256,
|
||||||
pseudoEncodingVMwareCursor: 0x574d5664,
|
pseudoEncodingVMwareCursor: 0x574d5664,
|
||||||
|
pseudoEncodingVMwareCursorPosition: 0x574d5666,
|
||||||
pseudoEncodingExtendedClipboard: 0xc0a1e5ce
|
pseudoEncodingExtendedClipboard: 0xc0a1e5ce
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
97
core/rfb.js
97
core/rfb.js
|
|
@ -183,6 +183,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._mousePos = {};
|
this._mousePos = {};
|
||||||
this._mouseButtonMask = 0;
|
this._mouseButtonMask = 0;
|
||||||
this._mouseLastMoveTime = 0;
|
this._mouseLastMoveTime = 0;
|
||||||
|
this._pointerLock = false;
|
||||||
this._viewportDragging = false;
|
this._viewportDragging = false;
|
||||||
this._viewportDragPos = {};
|
this._viewportDragPos = {};
|
||||||
this._viewportHasMoved = false;
|
this._viewportHasMoved = false;
|
||||||
|
|
@ -200,6 +201,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
focusCanvas: this._focusCanvas.bind(this),
|
focusCanvas: this._focusCanvas.bind(this),
|
||||||
handleResize: this._handleResize.bind(this),
|
handleResize: this._handleResize.bind(this),
|
||||||
handleMouse: this._handleMouse.bind(this),
|
handleMouse: this._handleMouse.bind(this),
|
||||||
|
handlePointerLockChange: this._handlePointerLockChange.bind(this),
|
||||||
|
handlePointerLockError: this._handlePointerLockError.bind(this),
|
||||||
handleWheel: this._handleWheel.bind(this),
|
handleWheel: this._handleWheel.bind(this),
|
||||||
handleGesture: this._handleGesture.bind(this),
|
handleGesture: this._handleGesture.bind(this),
|
||||||
handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this),
|
handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this),
|
||||||
|
|
@ -481,6 +484,24 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._canvas.blur();
|
this._canvas.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestInputLock(locks) {
|
||||||
|
if (locks.pointer) {
|
||||||
|
if (this._canvas.requestPointerLock) {
|
||||||
|
this._canvas.requestPointerLock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._canvas.mozRequestPointerLock) {
|
||||||
|
this._canvas.mozRequestPointerLock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we were not able to request any lock, still let the user know
|
||||||
|
// about the result.
|
||||||
|
this.dispatchEvent(new CustomEvent(
|
||||||
|
"inputlock",
|
||||||
|
{ detail: { pointer: this._pointerLock }, }));
|
||||||
|
}
|
||||||
|
|
||||||
clipboardPasteFrom(text) {
|
clipboardPasteFrom(text) {
|
||||||
if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
|
if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
|
||||||
|
|
||||||
|
|
@ -548,6 +569,14 @@ export default class RFB extends EventTargetMixin {
|
||||||
// preventDefault() on mousedown doesn't stop this event for some
|
// preventDefault() on mousedown doesn't stop this event for some
|
||||||
// reason so we have to explicitly block it
|
// reason so we have to explicitly block it
|
||||||
this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);
|
this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);
|
||||||
|
// Pointer Lock listeners need to be installed in document instead of the canvas.
|
||||||
|
if (document.onpointerlockchange !== undefined) {
|
||||||
|
document.addEventListener('pointerlockchange', this._eventHandlers.handlePointerLockChange, false);
|
||||||
|
document.addEventListener('pointerlockerror', this._eventHandlers.handlePointerLockError, false);
|
||||||
|
} else if (document.onmozpointerlockchange !== undefined) {
|
||||||
|
document.addEventListener('mozpointerlockchange', this._eventHandlers.handlePointerLockChange, false);
|
||||||
|
document.addEventListener('mozpointerlockerror', this._eventHandlers.handlePointerLockError, false);
|
||||||
|
}
|
||||||
|
|
||||||
// Wheel events
|
// Wheel events
|
||||||
this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);
|
this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);
|
||||||
|
|
@ -572,6 +601,13 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);
|
this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);
|
||||||
this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);
|
this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);
|
||||||
this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);
|
this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);
|
||||||
|
if (document.onpointerlockchange !== undefined) {
|
||||||
|
document.removeEventListener('pointerlockchange', this._eventHandlers.handlePointerLockChange);
|
||||||
|
document.removeEventListener('pointerlockerror', this._eventHandlers.handlePointerLockError);
|
||||||
|
} else if (document.onmozpointerlockchange !== undefined) {
|
||||||
|
document.removeEventListener('mozpointerlockchange', this._eventHandlers.handlePointerLockChange);
|
||||||
|
document.removeEventListener('mozpointerlockerror', this._eventHandlers.handlePointerLockError);
|
||||||
|
}
|
||||||
this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
|
this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
|
||||||
this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
|
this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
|
||||||
this._resizeObserver.disconnect();
|
this._resizeObserver.disconnect();
|
||||||
|
|
@ -980,8 +1016,27 @@ export default class RFB extends EventTargetMixin {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pos = clientToElement(ev.clientX, ev.clientY,
|
let pos;
|
||||||
|
if (this._pointerLock) {
|
||||||
|
pos = {
|
||||||
|
x: this._mousePos.x + ev.movementX,
|
||||||
|
y: this._mousePos.y + ev.movementY,
|
||||||
|
};
|
||||||
|
if (pos.x < 0) {
|
||||||
|
pos.x = 0;
|
||||||
|
} else if (pos.x > this._fbWidth) {
|
||||||
|
pos.x = this._fbWidth;
|
||||||
|
}
|
||||||
|
if (pos.y < 0) {
|
||||||
|
pos.y = 0;
|
||||||
|
} else if (pos.y > this._fbHeight) {
|
||||||
|
pos.y = this._fbHeight;
|
||||||
|
}
|
||||||
|
this._cursor.move(pos.x, pos.y);
|
||||||
|
} else {
|
||||||
|
pos = clientToElement(ev.clientX, ev.clientY,
|
||||||
this._canvas);
|
this._canvas);
|
||||||
|
}
|
||||||
|
|
||||||
switch (ev.type) {
|
switch (ev.type) {
|
||||||
case 'mousedown':
|
case 'mousedown':
|
||||||
|
|
@ -1082,6 +1137,28 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._mouseLastMoveTime = Date.now();
|
this._mouseLastMoveTime = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handlePointerLockChange() {
|
||||||
|
if (
|
||||||
|
document.pointerLockElement === this._canvas ||
|
||||||
|
document.mozPointerLockElement === this._canvas
|
||||||
|
) {
|
||||||
|
this._pointerLock = true;
|
||||||
|
this._cursor.setEmulateCursor(true);
|
||||||
|
} else {
|
||||||
|
this._pointerLock = false;
|
||||||
|
this._cursor.setEmulateCursor(false);
|
||||||
|
}
|
||||||
|
this.dispatchEvent(new CustomEvent(
|
||||||
|
"inputlock",
|
||||||
|
{ detail: { pointer: this._pointerLock }, }));
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlePointerLockError() {
|
||||||
|
this.dispatchEvent(new CustomEvent(
|
||||||
|
"inputlock",
|
||||||
|
{ detail: { pointer: this._pointerLock }, }));
|
||||||
|
}
|
||||||
|
|
||||||
_sendMouse(x, y, mask) {
|
_sendMouse(x, y, mask) {
|
||||||
if (this._rfbConnectionState !== 'connected') { return; }
|
if (this._rfbConnectionState !== 'connected') { return; }
|
||||||
if (this._viewOnly) { return; } // View only, skip mouse events
|
if (this._viewOnly) { return; } // View only, skip mouse events
|
||||||
|
|
@ -2065,6 +2142,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
encs.push(encodings.pseudoEncodingCursor);
|
encs.push(encodings.pseudoEncodingCursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
encs.push(encodings.pseudoEncodingVMwareCursorPosition);
|
||||||
|
|
||||||
RFB.messages.clientEncodings(this._sock, encs);
|
RFB.messages.clientEncodings(this._sock, encs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2471,6 +2550,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
case encodings.pseudoEncodingVMwareCursor:
|
case encodings.pseudoEncodingVMwareCursor:
|
||||||
return this._handleVMwareCursor();
|
return this._handleVMwareCursor();
|
||||||
|
|
||||||
|
case encodings.pseudoEncodingVMwareCursorPosition:
|
||||||
|
return this._handleVMwareCursorPosition();
|
||||||
|
|
||||||
case encodings.pseudoEncodingCursor:
|
case encodings.pseudoEncodingCursor:
|
||||||
return this._handleCursor();
|
return this._handleCursor();
|
||||||
|
|
||||||
|
|
@ -2609,6 +2691,19 @@ export default class RFB extends EventTargetMixin {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleVMwareCursorPosition() {
|
||||||
|
const x = this._FBU.x;
|
||||||
|
const y = this._FBU.y;
|
||||||
|
|
||||||
|
if (this._pointerLock) {
|
||||||
|
// Only attempt to match the server's pointer position if we are in
|
||||||
|
// pointer lock mode.
|
||||||
|
this._mousePos = { x: x, y: y };
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
_handleCursor() {
|
_handleCursor() {
|
||||||
const hotx = this._FBU.x; // hotspot-x
|
const hotx = this._FBU.x; // hotspot-x
|
||||||
const hoty = this._FBU.y; // hotspot-y
|
const hoty = this._FBU.y; // hotspot-y
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,20 @@
|
||||||
|
|
||||||
import { supportsCursorURIs, isTouchDevice } from './browser.js';
|
import { supportsCursorURIs, isTouchDevice } from './browser.js';
|
||||||
|
|
||||||
const useFallback = !supportsCursorURIs || isTouchDevice;
|
const needsFallback = !supportsCursorURIs || isTouchDevice;
|
||||||
|
|
||||||
export default class Cursor {
|
export default class Cursor {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._target = null;
|
this._target = null;
|
||||||
|
|
||||||
this._canvas = document.createElement('canvas');
|
this._canvas = document.createElement('canvas');
|
||||||
|
|
||||||
if (useFallback) {
|
|
||||||
this._canvas.style.position = 'fixed';
|
this._canvas.style.position = 'fixed';
|
||||||
this._canvas.style.zIndex = '65535';
|
this._canvas.style.zIndex = '65535';
|
||||||
this._canvas.style.pointerEvents = 'none';
|
this._canvas.style.pointerEvents = 'none';
|
||||||
// Can't use "display" because of Firefox bug #1445997
|
// Can't use "display" because of Firefox bug #1445997
|
||||||
this._canvas.style.visibility = 'hidden';
|
this._canvas.style.visibility = 'hidden';
|
||||||
}
|
|
||||||
|
this._useFallback = needsFallback;
|
||||||
|
|
||||||
this._position = { x: 0, y: 0 };
|
this._position = { x: 0, y: 0 };
|
||||||
this._hotSpot = { x: 0, y: 0 };
|
this._hotSpot = { x: 0, y: 0 };
|
||||||
|
|
@ -40,9 +39,14 @@ export default class Cursor {
|
||||||
|
|
||||||
this._target = target;
|
this._target = target;
|
||||||
|
|
||||||
if (useFallback) {
|
|
||||||
document.body.appendChild(this._canvas);
|
document.body.appendChild(this._canvas);
|
||||||
|
|
||||||
|
if (needsFallback) {
|
||||||
|
// Only add the event listeners if this will be responsible for
|
||||||
|
// rendering the cursor all the time. Otherwise, the cursor will
|
||||||
|
// only be rendered then the forced emulation is turned on, and
|
||||||
|
// that doesn't require this class to be adjusting the cursor
|
||||||
|
// position.
|
||||||
const options = { capture: true, passive: true };
|
const options = { capture: true, passive: true };
|
||||||
this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
|
this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
|
||||||
this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
|
this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
|
||||||
|
|
@ -58,15 +62,15 @@ export default class Cursor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useFallback) {
|
if (needsFallback) {
|
||||||
const options = { capture: true, passive: true };
|
const options = { capture: true, passive: true };
|
||||||
this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
|
this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
|
||||||
this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
|
this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
|
||||||
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
|
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
|
||||||
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
|
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
|
||||||
|
}
|
||||||
|
|
||||||
document.body.removeChild(this._canvas);
|
document.body.removeChild(this._canvas);
|
||||||
}
|
|
||||||
|
|
||||||
this._target = null;
|
this._target = null;
|
||||||
}
|
}
|
||||||
|
|
@ -91,9 +95,10 @@ export default class Cursor {
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
ctx.putImageData(img, 0, 0);
|
ctx.putImageData(img, 0, 0);
|
||||||
|
|
||||||
if (useFallback) {
|
if (this._useFallback || needsFallback) {
|
||||||
this._updatePosition();
|
this._updatePosition();
|
||||||
} else {
|
}
|
||||||
|
if (!needsFallback) {
|
||||||
let url = this._canvas.toDataURL();
|
let url = this._canvas.toDataURL();
|
||||||
this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
|
this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +117,7 @@ export default class Cursor {
|
||||||
// Mouse events might be emulated, this allows
|
// Mouse events might be emulated, this allows
|
||||||
// moving the cursor in such cases
|
// moving the cursor in such cases
|
||||||
move(clientX, clientY) {
|
move(clientX, clientY) {
|
||||||
if (!useFallback) {
|
if (!this._useFallback) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// clientX/clientY are relative the _visual viewport_,
|
// clientX/clientY are relative the _visual viewport_,
|
||||||
|
|
@ -130,6 +135,22 @@ export default class Cursor {
|
||||||
this._updateVisibility(target);
|
this._updateVisibility(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force the use of cursor emulation. This is needed when the pointer lock
|
||||||
|
// is in use, since the browser will not render the cursor.
|
||||||
|
setEmulateCursor(emulate) {
|
||||||
|
if (needsFallback) {
|
||||||
|
// We need to use the fallback all the time, so we shouldn't update
|
||||||
|
// the fallback flag.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._useFallback = emulate;
|
||||||
|
if (this._useFallback) {
|
||||||
|
this._showCursor();
|
||||||
|
} else {
|
||||||
|
this._hideCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_handleMouseOver(event) {
|
_handleMouseOver(event) {
|
||||||
// This event could be because we're entering the target, or
|
// This event could be because we're entering the target, or
|
||||||
// moving around amongst its sub elements. Let the move handler
|
// moving around amongst its sub elements. Let the move handler
|
||||||
|
|
|
||||||
37
docs/API.md
37
docs/API.md
|
|
@ -117,6 +117,10 @@ protocol stream.
|
||||||
- The `capabilities` event is fired when `RFB.capabilities` is
|
- The `capabilities` event is fired when `RFB.capabilities` is
|
||||||
updated.
|
updated.
|
||||||
|
|
||||||
|
[`inputlock`](#inputlock)
|
||||||
|
- The `inputlock` event is fired when an input lock is acquired (or released)
|
||||||
|
by the canvas.
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
|
|
||||||
[`RFB.disconnect()`](#rfbdisconnect)
|
[`RFB.disconnect()`](#rfbdisconnect)
|
||||||
|
|
@ -155,6 +159,9 @@ protocol stream.
|
||||||
[`RFB.clipboardPasteFrom()`](#rfbclipboardpastefrom)
|
[`RFB.clipboardPasteFrom()`](#rfbclipboardpastefrom)
|
||||||
- Send clipboard contents to server.
|
- Send clipboard contents to server.
|
||||||
|
|
||||||
|
[`RFB.requestInputLock()`](#rfbrequestInputLock)
|
||||||
|
- Requests that the RFB canvas acquire an input lock.
|
||||||
|
|
||||||
### Details
|
### Details
|
||||||
|
|
||||||
#### RFB()
|
#### RFB()
|
||||||
|
|
@ -285,6 +292,15 @@ The `capabilities` event is fired whenever an entry is added or removed
|
||||||
from `RFB.capabilities`. The `detail` property is an `Object` with the
|
from `RFB.capabilities`. The `detail` property is an `Object` with the
|
||||||
property `capabilities` containing the new value of `RFB.capabilities`.
|
property `capabilities` containing the new value of `RFB.capabilities`.
|
||||||
|
|
||||||
|
#### inputlock
|
||||||
|
|
||||||
|
The `inputlock` event is fired after a request to acquire an input lock or
|
||||||
|
whenever the state of the canvas' input lock has changed, the latter typically
|
||||||
|
occurs because the lock was released by the user pressing the ESC key or
|
||||||
|
performing a browser-specific gesture. The `detail` property is an `Object`
|
||||||
|
with the property `pointer` containing whether the Pointer Lock is currently
|
||||||
|
held or not.
|
||||||
|
|
||||||
#### RFB.disconnect()
|
#### RFB.disconnect()
|
||||||
|
|
||||||
The `RFB.disconnect()` method is used to disconnect from the currently
|
The `RFB.disconnect()` method is used to disconnect from the currently
|
||||||
|
|
@ -423,3 +439,24 @@ to the remote server.
|
||||||
|
|
||||||
**`text`**
|
**`text`**
|
||||||
- A `DOMString` specifying the clipboard data to send.
|
- A `DOMString` specifying the clipboard data to send.
|
||||||
|
|
||||||
|
#### RFB.requestInputLock()
|
||||||
|
|
||||||
|
The `RFB.requestInputLock()` method is used to request that the RFB canvas hold
|
||||||
|
an input lock. An `inputlock` event will be fired with the result of the
|
||||||
|
acquisition of the requested locks.
|
||||||
|
|
||||||
|
##### Syntax
|
||||||
|
|
||||||
|
RFB.requestInputLock( { pointer: true } );
|
||||||
|
|
||||||
|
###### Parameters
|
||||||
|
|
||||||
|
**`pointer`**
|
||||||
|
- Requests to acquire a [Pointer
|
||||||
|
Lock](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API),
|
||||||
|
which hides the local mouse cursor and provides relative motion events.
|
||||||
|
This must be called directly from an event handler where a user has
|
||||||
|
directly interacted with an element through an [engagement
|
||||||
|
gesture](https://w3c.github.io/pointerlock/#dfn-engagement-gesture) (e.g. a
|
||||||
|
click or touch event) for the browser to allow this.
|
||||||
|
|
|
||||||
|
|
@ -2725,6 +2725,27 @@ describe('Remote Frame Buffer Protocol Client', function () {
|
||||||
client._canvas.dispatchEvent(ev);
|
client._canvas.dispatchEvent(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function supportsSendMouseMovementEvent() {
|
||||||
|
// Some browsers (like Safari) support the movementX /
|
||||||
|
// movementY properties of MouseEvent, but do not allow creation
|
||||||
|
// of non-trusted events with those properties.
|
||||||
|
let ev;
|
||||||
|
|
||||||
|
ev = new MouseEvent('mousemove',
|
||||||
|
{ 'movementX': 100,
|
||||||
|
'movementY': 100 });
|
||||||
|
return ev.movementX === 100 && ev.movementY === 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMouseMovementEvent(dx, dy) {
|
||||||
|
let ev;
|
||||||
|
|
||||||
|
ev = new MouseEvent('mousemove',
|
||||||
|
{ 'movementX': dx,
|
||||||
|
'movementY': dy });
|
||||||
|
client._canvas.dispatchEvent(ev);
|
||||||
|
}
|
||||||
|
|
||||||
function sendMouseButtonEvent(x, y, down, button) {
|
function sendMouseButtonEvent(x, y, down, button) {
|
||||||
let pos = elementToClient(x, y);
|
let pos = elementToClient(x, y);
|
||||||
let ev;
|
let ev;
|
||||||
|
|
@ -2838,6 +2859,62 @@ describe('Remote Frame Buffer Protocol Client', function () {
|
||||||
50, 70, 0x0);
|
50, 70, 0x0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should ignore remote cursor position updates', function () {
|
||||||
|
if (!supportsSendMouseMovementEvent()) {
|
||||||
|
this.skip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Simple VMware Cursor Position FBU message with pointer coordinates
|
||||||
|
// (50, 50).
|
||||||
|
const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x32, 0x00, 0x32,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x57, 0x4d, 0x56, 0x66 ];
|
||||||
|
client._resize(100, 100);
|
||||||
|
|
||||||
|
const cursorSpy = sinon.spy(client, '_handleVMwareCursorPosition');
|
||||||
|
client._sock._websocket._receiveData(new Uint8Array(incoming));
|
||||||
|
expect(cursorSpy).to.have.been.calledOnceWith();
|
||||||
|
cursorSpy.restore();
|
||||||
|
|
||||||
|
expect(client._mousePos).to.deep.equal({ });
|
||||||
|
sendMouseMoveEvent(10, 10);
|
||||||
|
clock.tick(100);
|
||||||
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
||||||
|
10, 10, 0x0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle remote mouse position updates in pointer lock mode', function () {
|
||||||
|
if (!supportsSendMouseMovementEvent()) {
|
||||||
|
this.skip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Simple VMware Cursor Position FBU message with pointer coordinates
|
||||||
|
// (50, 50).
|
||||||
|
const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x32, 0x00, 0x32,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x57, 0x4d, 0x56, 0x66 ];
|
||||||
|
client._resize(100, 100);
|
||||||
|
|
||||||
|
const spy = sinon.spy();
|
||||||
|
client.addEventListener("inputlock", spy);
|
||||||
|
let stub = sinon.stub(document, 'pointerLockElement');
|
||||||
|
stub.get(function () { return client._canvas; });
|
||||||
|
client._handlePointerLockChange();
|
||||||
|
stub.restore();
|
||||||
|
client._sock._websocket._receiveData(new Uint8Array([0x02, 0x02]));
|
||||||
|
expect(spy).to.have.been.calledOnce;
|
||||||
|
expect(spy.args[0][0].detail.pointer).to.be.true;
|
||||||
|
|
||||||
|
const cursorSpy = sinon.spy(client, '_handleVMwareCursorPosition');
|
||||||
|
client._sock._websocket._receiveData(new Uint8Array(incoming));
|
||||||
|
expect(cursorSpy).to.have.been.calledOnceWith();
|
||||||
|
cursorSpy.restore();
|
||||||
|
|
||||||
|
expect(client._mousePos).to.deep.equal({ x: 50, y: 50 });
|
||||||
|
sendMouseMovementEvent(10, 10);
|
||||||
|
clock.tick(100);
|
||||||
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
||||||
|
60, 60, 0x0);
|
||||||
|
});
|
||||||
|
|
||||||
describe('Event Aggregation', function () {
|
describe('Event Aggregation', function () {
|
||||||
it('should send a single pointer event on mouse movement', function () {
|
it('should send a single pointer event on mouse movement', function () {
|
||||||
sendMouseMoveEvent(50, 70);
|
sendMouseMoveEvent(50, 70);
|
||||||
|
|
|
||||||
5
vnc.html
5
vnc.html
|
|
@ -84,6 +84,11 @@
|
||||||
id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden"
|
id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden"
|
||||||
title="Move/Drag Viewport">
|
title="Move/Drag Viewport">
|
||||||
|
|
||||||
|
<!-- Lock pointer events -->
|
||||||
|
<input type="image" alt="Lock pointer" src="app/images/pointer.svg"
|
||||||
|
id="noVNC_pointer_lock_button" class="noVNC_button noVNC_hidden"
|
||||||
|
title="Lock pointer">
|
||||||
|
|
||||||
<!--noVNC Touch Device only buttons-->
|
<!--noVNC Touch Device only buttons-->
|
||||||
<div id="noVNC_mobile_buttons">
|
<div id="noVNC_mobile_buttons">
|
||||||
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
|
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue