Pointer lock api (#29)
Add pointer lock and relative cursor position support. Game mode enables both pointer lock and relative cursor positions.
This commit is contained in:
parent
6c708d8254
commit
1753425874
|
@ -26,6 +26,12 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
// Skip allowed errors
|
||||
let allowedErrors = [ "The user has exited the lock before this request was completed." ];
|
||||
if (event.message && allowedErrors.includes(event.message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let div = document.createElement("div");
|
||||
div.classList.add('noVNC_message');
|
||||
div.appendChild(document.createTextNode(event.message));
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
|
@ -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 |
206
app/ui.js
206
app/ui.js
|
@ -34,7 +34,7 @@ import "core-js/stable";
|
|||
import "regenerator-runtime/runtime";
|
||||
import * as Log from '../core/util/logging.js';
|
||||
import _, { l10n } from './localization.js';
|
||||
import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold, supportsBinaryClipboard, isFirefox, isWindows, isIOS }
|
||||
import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold, supportsBinaryClipboard, isFirefox, isWindows, isIOS, supportsPointerLock }
|
||||
from '../core/util/browser.js';
|
||||
import { setCapture, getPointerEvent } from '../core/util/events.js';
|
||||
import KeyTable from "../core/input/keysym.js";
|
||||
|
@ -47,6 +47,7 @@ const PAGE_TITLE = "KasmVNC";
|
|||
|
||||
var delta = 500;
|
||||
var lastKeypressTime = 0;
|
||||
var lastKeypressCode = -1;
|
||||
var currentEventCount = -1;
|
||||
var idleCounter = 0;
|
||||
|
||||
|
@ -350,6 +351,10 @@ const UI = {
|
|||
document.getElementById("noVNC_view_drag_button")
|
||||
.addEventListener('click', UI.toggleViewDrag);
|
||||
|
||||
document
|
||||
.getElementById("noVNC_setting_pointer_lock")
|
||||
.addEventListener("click", UI.togglePointerLock);
|
||||
|
||||
document.getElementById("noVNC_control_bar_handle")
|
||||
.addEventListener('mousedown', UI.controlbarHandleMouseDown);
|
||||
document.getElementById("noVNC_control_bar_handle")
|
||||
|
@ -415,6 +420,8 @@ const UI = {
|
|||
.addEventListener('click', UI.sendEsc);
|
||||
document.getElementById("noVNC_send_ctrl_alt_del_button")
|
||||
.addEventListener('click', UI.sendCtrlAltDel);
|
||||
document.getElementById("noVNC_game_mode_button")
|
||||
.addEventListener("click", UI.toggleRelativePointer)
|
||||
},
|
||||
|
||||
addMachineHandlers() {
|
||||
|
@ -527,6 +534,7 @@ const UI = {
|
|||
UI.addSettingChangeHandler('clipboard_seamless');
|
||||
UI.addSettingChangeHandler('clipboard_up');
|
||||
UI.addSettingChangeHandler('clipboard_down');
|
||||
UI.addSettingChangeHandler('toggle_control_panel');
|
||||
UI.addSettingChangeHandler('virtual_keyboard_visible');
|
||||
UI.addSettingChangeHandler('virtual_keyboard_visible', UI.toggleKeyboardControls);
|
||||
UI.addSettingChangeHandler('enable_ime');
|
||||
|
@ -611,6 +619,7 @@ const UI = {
|
|||
UI.updatePowerButton();
|
||||
UI.keepControlbar();
|
||||
}
|
||||
//UI.updatePointerLockButton();
|
||||
|
||||
// State change closes dialogs as they may not be relevant
|
||||
// anymore
|
||||
|
@ -637,7 +646,12 @@ const UI = {
|
|||
|
||||
},
|
||||
|
||||
showStatus(text, statusType, time) {
|
||||
showStatus(text, statusType, time, kasm = false) {
|
||||
// If inside the full Kasm CDI framework, don't show messages unless explicitly told to
|
||||
if (WebUtil.isInsideKasmVDI() && !kasm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statusElem = document.getElementById('noVNC_status');
|
||||
|
||||
if (typeof statusType === 'undefined') {
|
||||
|
@ -1307,6 +1321,8 @@ const UI = {
|
|||
UI.rfb.addEventListener("bottleneck_stats", UI.bottleneckStatsRecieve);
|
||||
UI.rfb.addEventListener("bell", UI.bell);
|
||||
UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
|
||||
UI.rfb.addEventListener("inputlock", UI.inputLockChanged);
|
||||
UI.rfb.addEventListener("inputlockerror", UI.inputLockError);
|
||||
UI.rfb.translateShortcuts = UI.getSetting('translate_shortcuts');
|
||||
UI.rfb.clipViewport = UI.getSetting('view_clip');
|
||||
UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
|
||||
|
@ -1327,6 +1343,7 @@ const UI = {
|
|||
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
|
||||
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
||||
UI.rfb.idleDisconnect = UI.getSetting('idle_disconnect');
|
||||
UI.rfb.pointerRelative = UI.getSetting('pointer_relative');
|
||||
UI.rfb.videoQuality = parseInt(UI.getSetting('video_quality'));
|
||||
UI.rfb.antiAliasing = UI.getSetting('anti_aliasing');
|
||||
UI.rfb.clipboardUp = UI.getSetting('clipboard_up');
|
||||
|
@ -1394,23 +1411,23 @@ const UI = {
|
|||
document.getElementById('noVNC_status').style.visibility = "visible";
|
||||
}
|
||||
|
||||
// Send an event to the parent document (kasm app) to toggle the control panel when ctl is double clicked
|
||||
if (UI.getSetting('toggle_control_panel', false)) {
|
||||
|
||||
document.addEventListener('keyup', function (event) {
|
||||
// CTRL and the various implementations of the mac command key
|
||||
if ([17, 224, 91, 93].indexOf(event.keyCode) > -1) {
|
||||
var thisKeypressTime = new Date();
|
||||
|
||||
if (thisKeypressTime - lastKeypressTime <= delta) {
|
||||
UI.toggleNav();
|
||||
thisKeypressTime = 0;
|
||||
//key events for KasmVNC control
|
||||
document.addEventListener('keyup', function (event) {
|
||||
if (event.ctrlKey && event.shiftKey) {
|
||||
switch(event.keyCode) {
|
||||
case 49:
|
||||
UI.toggleNav();
|
||||
break;
|
||||
case 50:
|
||||
UI.toggleRelativePointer();
|
||||
break;
|
||||
case 51:
|
||||
UI.togglePointerLock();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lastKeypressTime = thisKeypressTime;
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
}, true);
|
||||
},
|
||||
|
||||
disconnect() {
|
||||
|
@ -1523,18 +1540,19 @@ const UI = {
|
|||
UI.showStatus(msg, 'error');
|
||||
},
|
||||
|
||||
/*
|
||||
Menu.js Additions
|
||||
*/
|
||||
receiveMessage(event) {
|
||||
//TODO: UNCOMMENT FOR PRODUCTION
|
||||
//if (event.origin !== "https://kasmweb.com")
|
||||
// return;
|
||||
//send message to parent window
|
||||
sendMessage(name, value) {
|
||||
if (WebUtil.isInsideKasmVDI()) {
|
||||
parent.postMessage({ action: name, value: value }, '*' );
|
||||
}
|
||||
},
|
||||
|
||||
//receive message from parent window
|
||||
receiveMessage(event) {
|
||||
if (event.data && event.data.action) {
|
||||
switch (event.data.action) {
|
||||
case 'clipboardsnd':
|
||||
if (UI.rfb.clipboardUp) {
|
||||
if (UI.rfb && UI.rfb.clipboardUp) {
|
||||
UI.rfb.clipboardPasteFrom(event.data.value);
|
||||
}
|
||||
break;
|
||||
|
@ -1542,6 +1560,26 @@ const UI = {
|
|||
UI.forceSetting('video_quality', parseInt(event.data.value), false);
|
||||
UI.updateQuality();
|
||||
break;
|
||||
case 'enable_game_mode':
|
||||
if (UI.rfb && !UI.rfb.pointerRelative) {
|
||||
UI.toggleRelativePointer();
|
||||
}
|
||||
break;
|
||||
case 'disable_game_mode':
|
||||
if (UI.rfb && UI.rfb.pointerRelative) {
|
||||
UI.toggleRelativePointer();
|
||||
}
|
||||
break;
|
||||
case 'enable_pointer_lock':
|
||||
if (UI.rfb && !UI.rfb.pointerLock) {
|
||||
UI.togglePointerLock();
|
||||
}
|
||||
break;
|
||||
case 'disable_pointer_lock':
|
||||
if (UI.rfb && UI.rfb.pointerLock) {
|
||||
UI.togglePointerLock();
|
||||
}
|
||||
break;
|
||||
case 'show_keyboard_controls':
|
||||
if (!UI.getSetting('virtual_keyboard_visible')) {
|
||||
UI.forceSetting('virtual_keyboard_visible', true, false);
|
||||
|
@ -1575,7 +1613,15 @@ const UI = {
|
|||
},
|
||||
|
||||
toggleNav(){
|
||||
parent.postMessage({ action: 'togglenav', value: null}, '*' );
|
||||
if (WebUtil.isInsideKasmVDI()) {
|
||||
parent.postMessage({ action: 'togglenav', value: null}, '*' );
|
||||
} else {
|
||||
UI.toggleControlbar();
|
||||
UI.keepControlbar();
|
||||
UI.activateControlbar();
|
||||
UI.controlbarGrabbed = false;
|
||||
UI.showControlbarHint(false);
|
||||
}
|
||||
},
|
||||
|
||||
clipboardRx(event) {
|
||||
|
@ -1678,6 +1724,7 @@ const UI = {
|
|||
document.getElementById('noVNC_fullscreen_button')
|
||||
.classList.remove("noVNC_selected");
|
||||
}
|
||||
UI.updatePointerLockButton();
|
||||
},
|
||||
|
||||
/* ------^-------
|
||||
|
@ -1730,6 +1777,76 @@ const UI = {
|
|||
UI.updateViewDrag();
|
||||
},
|
||||
|
||||
/* ------^-------
|
||||
* /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.pointerLockElement !== undefined ||
|
||||
document.mozPointerLockElement !== undefined)
|
||||
) {
|
||||
document
|
||||
.getElementById("noVNC_setting_pointer_lock")
|
||||
.classList.remove("noVNC_hidden");
|
||||
document
|
||||
.getElementById("noVNC_game_mode_button")
|
||||
.classList.remove("noVNC_hidden");
|
||||
} else {
|
||||
document
|
||||
.getElementById("noVNC_setting_pointer_lock")
|
||||
.classList.add("noVNC_hidden");
|
||||
document
|
||||
.getElementById("noVNC_game_mode_button")
|
||||
.classList.add("noVNC_hidden");
|
||||
}
|
||||
},
|
||||
|
||||
togglePointerLock() {
|
||||
if (!supportsPointerLock()) {
|
||||
UI.showStatus('Your browser does not support pointer lock.', 'info', 1500, true);
|
||||
//force pointer lock in UI to false and disable control
|
||||
UI.forceSetting('pointer_lock', false, true);
|
||||
} else {
|
||||
UI.rfb.pointerLock = !UI.rfb.pointerLock;
|
||||
if (UI.getSetting('pointer_lock') !== UI.rfb.pointerLock) {
|
||||
UI.forceSetting('pointer_lock', UI.rfb.pointerLock, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
toggleRelativePointer(event=null, forcedToggleValue=null) {
|
||||
if (!supportsPointerLock()) {
|
||||
UI.showStatus('Your browser does not support pointer lock.', 'info', 1500, true);
|
||||
return;
|
||||
}
|
||||
|
||||
var togglePosition = !UI.rfb.pointerRelative;
|
||||
|
||||
if (UI.rfb.pointerLock !== togglePosition) {
|
||||
UI.rfb.pointerLock = togglePosition;
|
||||
}
|
||||
if (UI.rfb.pointerRelative !== togglePosition) {
|
||||
UI.rfb.pointerRelative = togglePosition;
|
||||
}
|
||||
|
||||
if (togglePosition) {
|
||||
document.getElementById('noVNC_game_mode_button').classList.add("noVNC_selected");
|
||||
} else {
|
||||
document.getElementById('noVNC_game_mode_button').classList.remove("noVNC_selected");
|
||||
UI.forceSetting('pointer_lock', false, false);
|
||||
}
|
||||
|
||||
UI.sendMessage('enable_game_mode', togglePosition);
|
||||
UI.sendMessage('enable_pointer_lock', togglePosition);
|
||||
|
||||
},
|
||||
|
||||
/* ------^-------
|
||||
* /VIEW CLIPPING
|
||||
* ==============
|
||||
|
@ -2161,6 +2278,8 @@ const UI = {
|
|||
.classList.add('noVNC_hidden');
|
||||
document.getElementById('noVNC_clipboard_button')
|
||||
.classList.add('noVNC_hidden');
|
||||
document.getElementById('noVNC_game_mode_button')
|
||||
.classList.add('noVNC_hidden');
|
||||
} else {
|
||||
document.getElementById('noVNC_keyboard_button')
|
||||
.classList.remove('noVNC_hidden');
|
||||
|
@ -2168,6 +2287,8 @@ const UI = {
|
|||
.classList.remove('noVNC_hidden');
|
||||
document.getElementById('noVNC_clipboard_button')
|
||||
.classList.remove('noVNC_hidden');
|
||||
document.getElementById('noVNC_game_mode_button')
|
||||
.classList.remove('noVNC_hidden');
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -2186,6 +2307,39 @@ const UI = {
|
|||
document.title = e.detail.name + " - " + PAGE_TITLE;
|
||||
},
|
||||
|
||||
inputLockChanged(e) {
|
||||
var pointer_lock_el = document.getElementById("noVNC_setting_pointer_lock");
|
||||
var pointer_rel_el = document.getElementById("noVNC_game_mode_button");
|
||||
|
||||
if (e.detail.pointer) {
|
||||
pointer_lock_el.checked = true;
|
||||
UI.sendMessage('enable_pointer_lock', true);
|
||||
UI.closeControlbar();
|
||||
UI.showStatus('Press Esc Key to Exit Pointer Lock Mode', 'warn', 5000, true);
|
||||
} else {
|
||||
//If in game mode
|
||||
if (UI.rfb.pointerRelative) {
|
||||
UI.showStatus('Game Mode paused, click on screen to resume Game Mode.', 'warn', 5000, true);
|
||||
} else {
|
||||
UI.forceSetting('pointer_lock', false, false);
|
||||
document.getElementById('noVNC_game_mode_button')
|
||||
.classList.remove("noVNC_selected");
|
||||
UI.sendMessage('enable_pointer_lock', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
inputLockError(e) {
|
||||
UI.showStatus('Unable to enter pointer lock mode.', 'warn', 5000, true);
|
||||
UI.rfb.pointerRelative = false;
|
||||
|
||||
document.getElementById('noVNC_game_mode_button').classList.remove("noVNC_selected");
|
||||
UI.forceSetting('pointer_lock', false, false);
|
||||
|
||||
UI.sendMessage('enable_game_mode', false);
|
||||
UI.sendMessage('enable_pointer_lock', false);
|
||||
},
|
||||
|
||||
bell(e) {
|
||||
if (WebUtil.getConfigVar('bell', 'on') === 'on') {
|
||||
const promise = document.getElementById('noVNC_bell').play();
|
||||
|
|
|
@ -54,6 +54,7 @@ export const encodings = {
|
|||
pseudoEncodingVideoOutTimeLevel100: -1887,
|
||||
|
||||
pseudoEncodingVMwareCursor: 0x574d5664,
|
||||
pseudoEncodingVMwareCursorPosition: 0x574d5666,
|
||||
pseudoEncodingExtendedClipboard: 0xc0a1e5ce
|
||||
};
|
||||
|
||||
|
|
170
core/rfb.js
170
core/rfb.js
|
@ -33,6 +33,7 @@ import RREDecoder from "./decoders/rre.js";
|
|||
import HextileDecoder from "./decoders/hextile.js";
|
||||
import TightDecoder from "./decoders/tight.js";
|
||||
import TightPNGDecoder from "./decoders/tightpng.js";
|
||||
import { toSignedRelative16bit } from './util/int.js';
|
||||
|
||||
// How many seconds to wait for a disconnect to finish
|
||||
const DISCONNECT_TIMEOUT = 3;
|
||||
|
@ -42,7 +43,7 @@ var _videoQuality = 2;
|
|||
var _enableWebP = false;
|
||||
|
||||
// Minimum wait (ms) between two mouse moves
|
||||
const MOUSE_MOVE_DELAY = 17;
|
||||
const MOUSE_MOVE_DELAY = 17;
|
||||
|
||||
// Wheel thresholds
|
||||
let WHEEL_LINE_HEIGHT = 19; // Pixels for one line step (on Windows)
|
||||
|
@ -170,6 +171,9 @@ export default class RFB extends EventTargetMixin {
|
|||
this._mousePos = {};
|
||||
this._mouseButtonMask = 0;
|
||||
this._mouseLastMoveTime = 0;
|
||||
this._pointerLock = false;
|
||||
this._pointerLockPos = { x: 0, y: 0 };
|
||||
this._pointerRelativeEnabled = false;
|
||||
this._mouseLastPinchAndZoomTime = 0;
|
||||
this._viewportDragging = false;
|
||||
this._viewportDragPos = {};
|
||||
|
@ -189,6 +193,8 @@ export default class RFB extends EventTargetMixin {
|
|||
focusCanvas: this._focusCanvas.bind(this),
|
||||
windowResize: this._windowResize.bind(this),
|
||||
handleMouse: this._handleMouse.bind(this),
|
||||
handlePointerLockChange: this._handlePointerLockChange.bind(this),
|
||||
handlePointerLockError: this._handlePointerLockError.bind(this),
|
||||
handleWheel: this._handleWheel.bind(this),
|
||||
handleGesture: this._handleGesture.bind(this),
|
||||
};
|
||||
|
@ -333,6 +339,43 @@ export default class RFB extends EventTargetMixin {
|
|||
|
||||
// ===== PROPERTIES =====
|
||||
|
||||
get pointerLock() { return this._pointerLock; }
|
||||
set pointerLock(value) {
|
||||
if (!this._pointerLock) {
|
||||
if (this._canvas.requestPointerLock) {
|
||||
this._canvas.requestPointerLock();
|
||||
this._pointerLockChanging = true;
|
||||
} else if (this._canvas.mozRequestPointerLock) {
|
||||
this._canvas.mozRequestPointerLock();
|
||||
this._pointerLockChanging = true;
|
||||
}
|
||||
} else {
|
||||
if (window.document.exitPointerLock) {
|
||||
window.document.exitPointerLock();
|
||||
this._pointerLockChanging = true;
|
||||
} else if (window.document.mozExitPointerLock) {
|
||||
window.document.mozExitPointerLock();
|
||||
this._pointerLockChanging = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get pointerRelative() { return this._pointerRelativeEnabled; }
|
||||
set pointerRelative(value)
|
||||
{
|
||||
this._pointerRelativeEnabled = value;
|
||||
if (value) {
|
||||
let max_w = ((this._display.scale === 1) ? this._fbWidth : (this._fbWidth * this._display.scale));
|
||||
let max_h = ((this._display.scale === 1) ? this._fbHeight : (this._fbHeight * this._display.scale));
|
||||
this._pointerLockPos.x = Math.floor(max_w / 2);
|
||||
this._pointerLockPos.y = Math.floor(max_h / 2);
|
||||
|
||||
// reset the cursor position to center
|
||||
this._mousePos = { x: this._pointerLockPos.x , y: this._pointerLockPos.y };
|
||||
this._cursor.move(this._pointerLockPos.x, this._pointerLockPos.y);
|
||||
}
|
||||
}
|
||||
|
||||
get keyboard() { return this._keyboard; }
|
||||
|
||||
get clipboardBinary() { return this._clipboardMode; }
|
||||
|
@ -748,12 +791,10 @@ export default class RFB extends EventTargetMixin {
|
|||
|
||||
focus() {
|
||||
this._keyboard.focus();
|
||||
//this._canvas.focus();
|
||||
}
|
||||
|
||||
blur() {
|
||||
this._keyboard.blur();
|
||||
//this._canvas.blur();
|
||||
}
|
||||
|
||||
clipboardPasteFrom(text) {
|
||||
|
@ -915,6 +956,15 @@ export default class RFB extends EventTargetMixin {
|
|||
// reason so we have to explicitly block it
|
||||
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
|
||||
this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);
|
||||
|
||||
|
@ -938,6 +988,13 @@ export default class RFB extends EventTargetMixin {
|
|||
this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);
|
||||
this._canvas.removeEventListener('click', 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("touchstart", this._eventHandlers.focusCanvas);
|
||||
window.removeEventListener('resize', this._eventHandlers.windowResize);
|
||||
|
@ -976,11 +1033,18 @@ export default class RFB extends EventTargetMixin {
|
|||
value: null
|
||||
}, "*");
|
||||
|
||||
// Re-enable pointerLock if relative cursor is enabled
|
||||
// pointerLock must come from user initiated event
|
||||
if (!this._pointerLock && this._pointerRelativeEnabled) {
|
||||
this.pointerLock = true;
|
||||
}
|
||||
|
||||
if (!this.focusOnClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.focus();
|
||||
|
||||
}
|
||||
|
||||
_setDesktopName(name) {
|
||||
|
@ -1311,8 +1375,34 @@ export default class RFB extends EventTargetMixin {
|
|||
return;
|
||||
}
|
||||
|
||||
let pos = clientToElement(ev.clientX, ev.clientY,
|
||||
let pos;
|
||||
if (this._pointerLock && !this._pointerRelativeEnabled) {
|
||||
let max_w = ((this._display.scale === 1) ? this._fbWidth : (this._fbWidth * this._display.scale));
|
||||
let max_h = ((this._display.scale === 1) ? this._fbHeight : (this._fbHeight * this._display.scale));
|
||||
pos = {
|
||||
x: this._mousePos.x + ev.movementX,
|
||||
y: this._mousePos.y + ev.movementY,
|
||||
};
|
||||
if (pos.x < 0) {
|
||||
pos.x = 0;
|
||||
} else if (pos.x > max_w) {
|
||||
pos.x = max_w;
|
||||
}
|
||||
if (pos.y < 0) {
|
||||
pos.y = 0;
|
||||
} else if (pos.y > max_h) {
|
||||
pos.y = max_h;
|
||||
}
|
||||
this._cursor.move(pos.x, pos.y);
|
||||
} else if (this._pointerLock && this._pointerRelativeEnabled) {
|
||||
pos = {
|
||||
x: this._mousePos.x + ev.movementX,
|
||||
y: this._mousePos.y + ev.movementY,
|
||||
};
|
||||
} else {
|
||||
pos = clientToElement(ev.clientX, ev.clientY,
|
||||
this._canvas);
|
||||
}
|
||||
|
||||
switch (ev.type) {
|
||||
case 'mousedown':
|
||||
|
@ -1428,12 +1518,54 @@ export default class RFB extends EventTargetMixin {
|
|||
this._mouseLastMoveTime = Date.now();
|
||||
}
|
||||
|
||||
_handlePointerLockChange(env) {
|
||||
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._pointerLockChanging = false;
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
"inputlockerror",
|
||||
{ detail: { pointer: this._pointerLock }, }));
|
||||
}
|
||||
|
||||
_sendMouse(x, y, mask) {
|
||||
if (this._rfbConnectionState !== 'connected') { return; }
|
||||
if (this._viewOnly) { return; } // View only, skip mouse events
|
||||
|
||||
RFB.messages.pointerEvent(this._sock, this._display.absX(x),
|
||||
if (this._pointerLock && this._pointerRelativeEnabled) {
|
||||
|
||||
// Use releative cursor position
|
||||
var rel_16_x = toSignedRelative16bit(x - this._pointerLockPos.x);
|
||||
var rel_16_y = toSignedRelative16bit(y - this._pointerLockPos.y);
|
||||
|
||||
//console.log("new_pos x" + x + ", y" + y);
|
||||
//console.log("lock x " + this._pointerLockPos.x + ", y " + this._pointerLockPos.y);
|
||||
//console.log("rel x " + rel_16_x + ", y " + rel_16_y);
|
||||
|
||||
RFB.messages.pointerEvent(this._sock, rel_16_x,
|
||||
rel_16_y, mask);
|
||||
|
||||
// reset the cursor position to center
|
||||
this._mousePos = { x: this._pointerLockPos.x , y: this._pointerLockPos.y };
|
||||
this._cursor.move(this._pointerLockPos.x, this._pointerLockPos.y);
|
||||
} else {
|
||||
RFB.messages.pointerEvent(this._sock, this._display.absX(x),
|
||||
this._display.absY(y), mask);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_sendScroll(x, y, dX, dY) {
|
||||
|
@ -2254,16 +2386,16 @@ export default class RFB extends EventTargetMixin {
|
|||
encs.push(encodings.pseudoEncodingVideoScalingLevel0 + this.videoScaling);
|
||||
encs.push(encodings.pseudoEncodingFrameRateLevel10 + this.frameRate - 10);
|
||||
encs.push(encodings.pseudoEncodingMaxVideoResolution);
|
||||
// preferBandwidth choses preset settings. Since we expose all the settings, lets not pass this
|
||||
|
||||
// preferBandwidth choses preset settings. Since we expose all the settings, lets not pass this
|
||||
if (this.preferBandwidth) // must be last - server processes in reverse order
|
||||
encs.push(encodings.pseudoEncodingPreferBandwidth);
|
||||
|
||||
if (supportsCursorURIs && this._fbDepth == 24) {
|
||||
if (this.preferLocalCursor || !isTouchDevice) {
|
||||
encs.push(encodings.pseudoEncodingVMwareCursor);
|
||||
encs.push(encodings.pseudoEncodingCursor);
|
||||
}
|
||||
if (this._fbDepth == 24) {
|
||||
encs.push(encodings.pseudoEncodingVMwareCursor);
|
||||
encs.push(encodings.pseudoEncodingCursor);
|
||||
}
|
||||
encs.push(encodings.pseudoEncodingVMwareCursorPosition);
|
||||
|
||||
RFB.messages.clientEncodings(this._sock, encs);
|
||||
}
|
||||
|
@ -2795,6 +2927,9 @@ export default class RFB extends EventTargetMixin {
|
|||
case encodings.pseudoEncodingVMwareCursor:
|
||||
return this._handleVMwareCursor();
|
||||
|
||||
case encodings.pseudoEncodingVMwareCursorPosition:
|
||||
return this._handleVMwareCursorPosition();
|
||||
|
||||
case encodings.pseudoEncodingCursor:
|
||||
return this._handleCursor();
|
||||
|
||||
|
@ -2933,6 +3068,19 @@ export default class RFB extends EventTargetMixin {
|
|||
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() {
|
||||
const hotx = this._FBU.x; // hotspot-x
|
||||
const hoty = this._FBU.y; // hotspot-y
|
||||
|
|
|
@ -97,6 +97,47 @@ export function isSafari() {
|
|||
navigator.userAgent.indexOf('Chrome') === -1);
|
||||
}
|
||||
|
||||
// Returns IE version number if IE or older Edge browser
|
||||
export function isIE() {
|
||||
var ua = window.navigator.userAgent;
|
||||
|
||||
// Test values; Uncomment to check result &
|
||||
|
||||
// IE 10
|
||||
// ua = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)';
|
||||
|
||||
// IE 11
|
||||
// ua = 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko';
|
||||
|
||||
// Edge 12 (Spartan)
|
||||
// ua = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36 Edge/12.0';
|
||||
|
||||
// Edge 13
|
||||
// ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586';
|
||||
|
||||
var msie = ua.indexOf('MSIE ');
|
||||
var ie_ver = false;
|
||||
if (msie > 0) {
|
||||
// IE 10 or older => return version number
|
||||
ie_ver = parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
|
||||
}
|
||||
|
||||
var trident = ua.indexOf('Trident/');
|
||||
if (trident > 0) {
|
||||
// IE 11 => return version number
|
||||
var rv = ua.indexOf('rv:');
|
||||
ie_ver = parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
|
||||
}
|
||||
|
||||
var edge = ua.indexOf('Edge/');
|
||||
if (edge > 0) {
|
||||
// Edge (IE 12+) => return version number
|
||||
ie_ver = parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
|
||||
}
|
||||
|
||||
return ie_ver;
|
||||
}
|
||||
|
||||
export function isChromiumBased() {
|
||||
return (!!window.chrome);
|
||||
}
|
||||
|
@ -111,3 +152,10 @@ export function supportsBinaryClipboard() {
|
|||
return (navigator.clipboard && typeof navigator.clipboard.read === "function");
|
||||
}
|
||||
|
||||
export function supportsPointerLock() {
|
||||
//Older versions of edge do support browser lock, but seems to not behave as expected
|
||||
//Disable on browsers that don't fully support or work as expected
|
||||
if (isIOS() || isIE()) { return false; }
|
||||
return (document.exitPointerLock);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,21 +6,19 @@
|
|||
|
||||
import { supportsCursorURIs, isTouchDevice } from './browser.js';
|
||||
|
||||
const useFallback = !supportsCursorURIs || isTouchDevice;
|
||||
const needsFallback = !supportsCursorURIs || isTouchDevice;
|
||||
|
||||
export default class Cursor {
|
||||
constructor() {
|
||||
this._target = null;
|
||||
|
||||
this._canvas = document.createElement('canvas');
|
||||
|
||||
if (useFallback) {
|
||||
this._canvas.style.position = 'fixed';
|
||||
this._canvas.style.zIndex = '65535';
|
||||
this._canvas.style.pointerEvents = 'none';
|
||||
// Can't use "display" because of Firefox bug #1445997
|
||||
this._canvas.style.visibility = 'hidden';
|
||||
}
|
||||
this._canvas.style.position = 'fixed';
|
||||
this._canvas.style.zIndex = '65535';
|
||||
this._canvas.style.pointerEvents = 'none';
|
||||
// Can't use "display" because of Firefox bug #1445997
|
||||
this._canvas.style.visibility = 'hidden';
|
||||
this._useFallback = needsFallback;
|
||||
|
||||
this._position = { x: 0, y: 0 };
|
||||
this._hotSpot = { x: 0, y: 0 };
|
||||
|
@ -40,9 +38,15 @@ export default class Cursor {
|
|||
|
||||
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 };
|
||||
this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
|
||||
this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
|
||||
|
@ -58,16 +62,16 @@ export default class Cursor {
|
|||
return;
|
||||
}
|
||||
|
||||
if (useFallback) {
|
||||
if (needsFallback) {
|
||||
const options = { capture: true, passive: true };
|
||||
this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
|
||||
this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
|
||||
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
|
||||
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
|
||||
|
||||
document.body.removeChild(this._canvas);
|
||||
}
|
||||
|
||||
document.body.removeChild(this._canvas);
|
||||
|
||||
this._target = null;
|
||||
}
|
||||
|
||||
|
@ -91,9 +95,10 @@ export default class Cursor {
|
|||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.putImageData(img, 0, 0);
|
||||
|
||||
if (useFallback) {
|
||||
if (this._useFallback || needsFallback) {
|
||||
this._updatePosition();
|
||||
} else {
|
||||
}
|
||||
if (!needsFallback) {
|
||||
let url = this._canvas.toDataURL();
|
||||
this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
|
||||
}
|
||||
|
@ -112,7 +117,7 @@ export default class Cursor {
|
|||
// Mouse events might be emulated, this allows
|
||||
// moving the cursor in such cases
|
||||
move(clientX, clientY) {
|
||||
if (!useFallback) {
|
||||
if (!this._useFallback) {
|
||||
return;
|
||||
}
|
||||
// clientX/clientY are relative the _visual viewport_,
|
||||
|
@ -130,6 +135,22 @@ export default class Cursor {
|
|||
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) {
|
||||
// This event could be because we're entering the target, or
|
||||
// moving around amongst its sub elements. Let the move handler
|
||||
|
|
|
@ -15,12 +15,40 @@ export function toSigned32bit(toConvert) {
|
|||
}
|
||||
|
||||
/*
|
||||
* Fast hashing function with low entropy, not for security uses.
|
||||
* Converts a signed 32bit integer to a signed 16bit int
|
||||
* Uses second most significant bit to represent it is relative
|
||||
*/
|
||||
export function toSignedRelative16bit(toConvert) {
|
||||
// TODO: move these so they are not computed with every func call
|
||||
var negmask16 = 1 << 15;
|
||||
var negmask32 = 1 << 31;
|
||||
var relmask16 = 1 << 14;
|
||||
|
||||
var converted16 = toConvert | 0;
|
||||
|
||||
// number is negative
|
||||
if ((toConvert & negmask32) != 0) {
|
||||
// clear the 32bit negative bit
|
||||
// not neccessary because the last 16bits will get dropped anyway
|
||||
converted16 *= -1;
|
||||
|
||||
// set the 16bit negative bit
|
||||
converted16 |= negmask16;
|
||||
// set the relative bit
|
||||
converted16 |= relmask16;
|
||||
} else {
|
||||
// set the relative bit
|
||||
converted16 |= relmask16;
|
||||
}
|
||||
|
||||
return converted16;
|
||||
}
|
||||
|
||||
/* Fast hashing function with low entropy */
|
||||
export function hashUInt8Array(data) {
|
||||
let h;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
h = Math.imul(31, h) + data[i] | 0;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
|
38
docs/API.md
38
docs/API.md
|
@ -113,6 +113,10 @@ protocol stream.
|
|||
- The `capabilities` event is fired when `RFB.capabilities` is
|
||||
updated.
|
||||
|
||||
[`inputlock`](#inputlock)
|
||||
- The `inputlock` event is fired when an input lock is acquired (or released)
|
||||
by the canvas.
|
||||
|
||||
### Methods
|
||||
|
||||
[`RFB.disconnect()`](#rfbdisconnect)
|
||||
|
@ -146,6 +150,10 @@ protocol stream.
|
|||
[`RFB.clipboardPasteFrom()`](#rfbclipboardPasteFrom)
|
||||
- Send clipboard contents to server.
|
||||
|
||||
[`inputlock`](#inputlock)
|
||||
- The `inputlock` event is fired when an input lock is acquired (or released)
|
||||
by the canvas.
|
||||
|
||||
### Details
|
||||
|
||||
#### RFB()
|
||||
|
@ -262,6 +270,15 @@ The `capabilities` event is fired whenever an entry is added or removed
|
|||
from `RFB.capabilities`. The `detail` property is an `Object` with the
|
||||
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()
|
||||
|
||||
The `RFB.disconnect()` method is used to disconnect from the currently
|
||||
|
@ -383,3 +400,24 @@ to the remote server.
|
|||
|
||||
**`text`**
|
||||
- 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.
|
|
@ -2610,6 +2610,27 @@ describe('Remote Frame Buffer Protocol Client', function () {
|
|||
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) {
|
||||
let pos = elementToClient(x, y);
|
||||
let ev;
|
||||
|
@ -2723,6 +2744,62 @@ describe('Remote Frame Buffer Protocol Client', function () {
|
|||
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 () {
|
||||
it('should send a single pointer event on mouse movement', function () {
|
||||
sendMouseMoveEvent(50, 70);
|
||||
|
|
27
vnc.html
27
vnc.html
|
@ -168,6 +168,11 @@
|
|||
id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden"
|
||||
title="Fullscreen">
|
||||
|
||||
<!-- Toggle game mode -->
|
||||
<input type="image" alt="Game Mode" src="app/images/gamepad.png"
|
||||
id="noVNC_game_mode_button" class="noVNC_button noVNC_hidden"
|
||||
title="Game Pointer Mode">
|
||||
|
||||
<!-- Settings -->
|
||||
<input type="image" alt="Settings" src="app/images/settings.svg"
|
||||
id="noVNC_settings_button" class="noVNC_button"
|
||||
|
@ -197,12 +202,15 @@
|
|||
<input id="noVNC_setting_translate_shortcuts" type="checkbox" />Translate keyboard shurtcuts
|
||||
</label>
|
||||
</li>
|
||||
</li>
|
||||
<li>
|
||||
<label><input id="noVNC_setting_enable_webp" type="checkbox" /> Enable WebP Compression</label></li>
|
||||
<li>
|
||||
<label><input id="noVNC_setting_enable_perf_stats" type="checkbox" /> Enable Performance Stats</label></li>
|
||||
<li>
|
||||
<label>
|
||||
<input type="checkbox" id="noVNC_setting_pointer_lock" /> Enable Pointer Lock
|
||||
</label>
|
||||
</li>
|
||||
<label><input id="noVNC_setting_enable_ime" type="checkbox" /> IME Input Mode</label></li>
|
||||
<li>
|
||||
<label><input id="noVNC_setting_virtual_keyboard_visible" type="checkbox" /> Show Virtual Keyboard Control</label></li>
|
||||
|
@ -227,6 +235,23 @@
|
|||
</select>
|
||||
</li>
|
||||
<li><hr></li>
|
||||
<li>
|
||||
<div class="noVNC_expander">Keyboard Shortcuts</div>
|
||||
<div>
|
||||
<ul>
|
||||
<li>
|
||||
<label>
|
||||
<input id="noVNC_setting_toggle_control_panel" type="checkbox" /> Enable KasmVNC Keyboard Shortcuts
|
||||
</label>
|
||||
</li>
|
||||
<li>Ctrl+Shift+</li>
|
||||
<li>1 - Toggle Control Panel</li>
|
||||
<li>2 - Toggle Game Pointer Mode</li>
|
||||
<li>3 - Toggle Pointer Lock</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li><hr></li>
|
||||
<li>
|
||||
<div class="noVNC_expander">Stream Quality</div>
|
||||
<div><ul>
|
||||
|
|
Loading…
Reference in New Issue