Remote cursor support (tested against UltraVNC and TightVNC so far)
This commit is contained in:
parent
4cb5aa45ae
commit
bce2a57bf0
12
app/ui.js
12
app/ui.js
|
@ -184,6 +184,7 @@ const UI = {
|
||||||
UI.initSetting('shared', true);
|
UI.initSetting('shared', true);
|
||||||
UI.initSetting('bell', 'on');
|
UI.initSetting('bell', 'on');
|
||||||
UI.initSetting('view_only', false);
|
UI.initSetting('view_only', false);
|
||||||
|
UI.initSetting('show_remote_cursor', true);
|
||||||
UI.initSetting('show_dot', false);
|
UI.initSetting('show_dot', false);
|
||||||
UI.initSetting('path', 'websockify');
|
UI.initSetting('path', 'websockify');
|
||||||
UI.initSetting('repeaterID', '');
|
UI.initSetting('repeaterID', '');
|
||||||
|
@ -369,6 +370,8 @@ const UI = {
|
||||||
UI.addSettingChangeHandler('shared');
|
UI.addSettingChangeHandler('shared');
|
||||||
UI.addSettingChangeHandler('view_only');
|
UI.addSettingChangeHandler('view_only');
|
||||||
UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
|
UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
|
||||||
|
UI.addSettingChangeHandler('show_remote_cursor');
|
||||||
|
UI.addSettingChangeHandler('show_remote_cursor', UI.updateShowRemoteCursor);
|
||||||
UI.addSettingChangeHandler('show_dot');
|
UI.addSettingChangeHandler('show_dot');
|
||||||
UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
|
UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
|
||||||
UI.addSettingChangeHandler('host');
|
UI.addSettingChangeHandler('host');
|
||||||
|
@ -441,6 +444,7 @@ const UI = {
|
||||||
UI.disableSetting('port');
|
UI.disableSetting('port');
|
||||||
UI.disableSetting('path');
|
UI.disableSetting('path');
|
||||||
UI.disableSetting('repeaterID');
|
UI.disableSetting('repeaterID');
|
||||||
|
UI.disableSetting('show_remote_cursor');
|
||||||
|
|
||||||
// Hide the controlbar after 2 seconds
|
// Hide the controlbar after 2 seconds
|
||||||
UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
|
UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
|
||||||
|
@ -451,6 +455,7 @@ const UI = {
|
||||||
UI.enableSetting('port');
|
UI.enableSetting('port');
|
||||||
UI.enableSetting('path');
|
UI.enableSetting('path');
|
||||||
UI.enableSetting('repeaterID');
|
UI.enableSetting('repeaterID');
|
||||||
|
UI.enableSetting('show_remote_cursor');
|
||||||
UI.updatePowerButton();
|
UI.updatePowerButton();
|
||||||
UI.keepControlbar();
|
UI.keepControlbar();
|
||||||
}
|
}
|
||||||
|
@ -887,6 +892,7 @@ const UI = {
|
||||||
UI.updateSetting('compression');
|
UI.updateSetting('compression');
|
||||||
UI.updateSetting('shared');
|
UI.updateSetting('shared');
|
||||||
UI.updateSetting('view_only');
|
UI.updateSetting('view_only');
|
||||||
|
UI.updateSetting('show_remote_cursor');
|
||||||
UI.updateSetting('path');
|
UI.updateSetting('path');
|
||||||
UI.updateSetting('repeaterID');
|
UI.updateSetting('repeaterID');
|
||||||
UI.updateSetting('logging');
|
UI.updateSetting('logging');
|
||||||
|
@ -1100,6 +1106,7 @@ const UI = {
|
||||||
UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
|
UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
|
||||||
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
|
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
|
||||||
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
|
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
|
||||||
|
UI.rfb.showRemoteCursor = UI.getSetting('show_remote_cursor');
|
||||||
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
||||||
|
|
||||||
UI.updateViewOnly(); // requires UI.rfb
|
UI.updateViewOnly(); // requires UI.rfb
|
||||||
|
@ -1754,6 +1761,11 @@ const UI = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateShowRemoteCursor () {
|
||||||
|
if (!UI.rfb) return;
|
||||||
|
UI.rfb.showRemoteCursor = UI.getSetting('show_remote_cursor');
|
||||||
|
},
|
||||||
|
|
||||||
updateShowDotCursor() {
|
updateShowDotCursor() {
|
||||||
if (!UI.rfb) return;
|
if (!UI.rfb) return;
|
||||||
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
||||||
|
|
|
@ -34,7 +34,10 @@ export const encodings = {
|
||||||
pseudoEncodingCompressLevel9: -247,
|
pseudoEncodingCompressLevel9: -247,
|
||||||
pseudoEncodingCompressLevel0: -256,
|
pseudoEncodingCompressLevel0: -256,
|
||||||
pseudoEncodingVMwareCursor: 0x574d5664,
|
pseudoEncodingVMwareCursor: 0x574d5664,
|
||||||
pseudoEncodingExtendedClipboard: 0xc0a1e5ce
|
pseudoEncodingExtendedClipboard: 0xc0a1e5ce,
|
||||||
|
pseudoEncodingRichCursor: 0xffffff11,
|
||||||
|
pseudoEncodingPointerPos: 0xffffff18,
|
||||||
|
pseudoEncodingTightPointerPos: -232
|
||||||
};
|
};
|
||||||
|
|
||||||
export function encodingName(num) {
|
export function encodingName(num) {
|
||||||
|
|
27
core/rfb.js
27
core/rfb.js
|
@ -309,7 +309,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
get viewOnly() { return this._viewOnly; }
|
get viewOnly() { return this._viewOnly; }
|
||||||
set viewOnly(viewOnly) {
|
set viewOnly(viewOnly) {
|
||||||
this._viewOnly = viewOnly;
|
this._viewOnly = this._cursor.viewOnly = viewOnly;
|
||||||
|
|
||||||
if (this._rfbConnectionState === "connecting" ||
|
if (this._rfbConnectionState === "connecting" ||
|
||||||
this._rfbConnectionState === "connected") {
|
this._rfbConnectionState === "connected") {
|
||||||
|
@ -413,6 +413,11 @@ export default class RFB extends EventTargetMixin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get showRemoteCursor() { return this._showRemoteCursor; }
|
||||||
|
set showRemoteCursor(show) {
|
||||||
|
this._showRemoteCursor = show;
|
||||||
|
}
|
||||||
|
|
||||||
// ===== PUBLIC METHODS =====
|
// ===== PUBLIC METHODS =====
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
@ -2264,6 +2269,12 @@ export default class RFB extends EventTargetMixin {
|
||||||
if (this._fbDepth == 24) {
|
if (this._fbDepth == 24) {
|
||||||
encs.push(encodings.pseudoEncodingVMwareCursor);
|
encs.push(encodings.pseudoEncodingVMwareCursor);
|
||||||
encs.push(encodings.pseudoEncodingCursor);
|
encs.push(encodings.pseudoEncodingCursor);
|
||||||
|
encs.push(encodings.pseudoEncodingRichCursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._showRemoteCursor) {
|
||||||
|
encs.push(encodings.pseudoEncodingPointerPos);
|
||||||
|
encs.push(encodings.pseudoEncodingTightPointerPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
RFB.messages.clientEncodings(this._sock, encs);
|
RFB.messages.clientEncodings(this._sock, encs);
|
||||||
|
@ -2673,6 +2684,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
return this._handleVMwareCursor();
|
return this._handleVMwareCursor();
|
||||||
|
|
||||||
case encodings.pseudoEncodingCursor:
|
case encodings.pseudoEncodingCursor:
|
||||||
|
case encodings.pseudoEncodingRichCursor:
|
||||||
return this._handleCursor();
|
return this._handleCursor();
|
||||||
|
|
||||||
case encodings.pseudoEncodingQEMUExtendedKeyEvent:
|
case encodings.pseudoEncodingQEMUExtendedKeyEvent:
|
||||||
|
@ -2696,6 +2708,10 @@ export default class RFB extends EventTargetMixin {
|
||||||
case encodings.pseudoEncodingQEMULedEvent:
|
case encodings.pseudoEncodingQEMULedEvent:
|
||||||
return this._handleLedEvent();
|
return this._handleLedEvent();
|
||||||
|
|
||||||
|
case encodings.pseudoEncodingPointerPos:
|
||||||
|
case encodings.pseudoEncodingTightPointerPos:
|
||||||
|
return this._handlePointerPos();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return this._handleDataRect();
|
return this._handleDataRect();
|
||||||
}
|
}
|
||||||
|
@ -2888,6 +2904,15 @@ export default class RFB extends EventTargetMixin {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handlePointerPos() {
|
||||||
|
const x = this._FBU.x;
|
||||||
|
const y = this._FBU.y;
|
||||||
|
|
||||||
|
this._cursor.moveRemote(x, y, this._display.scale);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
_handleExtendedDesktopSize() {
|
_handleExtendedDesktopSize() {
|
||||||
if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
|
if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -14,7 +14,6 @@ export default class Cursor {
|
||||||
|
|
||||||
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';
|
||||||
|
@ -24,7 +23,6 @@ export default class Cursor {
|
||||||
this._canvas.style.WebkitUserSelect = 'none';
|
this._canvas.style.WebkitUserSelect = '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._position = { x: 0, y: 0 };
|
this._position = { x: 0, y: 0 };
|
||||||
this._hotSpot = { x: 0, y: 0 };
|
this._hotSpot = { x: 0, y: 0 };
|
||||||
|
@ -35,6 +33,20 @@ export default class Cursor {
|
||||||
'mousemove': this._handleMouseMove.bind(this),
|
'mousemove': this._handleMouseMove.bind(this),
|
||||||
'mouseup': this._handleMouseUp.bind(this),
|
'mouseup': this._handleMouseUp.bind(this),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._mouseOver = false;
|
||||||
|
this._viewOnly = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get viewOnly() { return this._viewOnly; }
|
||||||
|
set viewOnly(viewOnly) {
|
||||||
|
if (viewOnly !== this._viewOnly) {
|
||||||
|
this._viewOnly = viewOnly;
|
||||||
|
this._resetNativeCursorStyle();
|
||||||
|
if (this._viewOnly) {
|
||||||
|
this._showCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attach(target) {
|
attach(target) {
|
||||||
|
@ -44,12 +56,13 @@ export default class Cursor {
|
||||||
|
|
||||||
this._target = target;
|
this._target = target;
|
||||||
|
|
||||||
if (useFallback) {
|
|
||||||
document.body.appendChild(this._canvas);
|
document.body.appendChild(this._canvas);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
if (useFallback) {
|
||||||
this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
|
this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
|
||||||
this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
|
this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
|
||||||
}
|
}
|
||||||
|
@ -62,17 +75,18 @@ export default class Cursor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useFallback) {
|
|
||||||
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);
|
||||||
|
|
||||||
|
if (useFallback) {
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
if (document.contains(this._canvas)) {
|
if (document.contains(this._canvas)) {
|
||||||
document.body.removeChild(this._canvas);
|
document.body.removeChild(this._canvas);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this._target = null;
|
this._target = null;
|
||||||
}
|
}
|
||||||
|
@ -97,16 +111,17 @@ 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 (useFallback || this._viewOnly || !this._mouseOver) {
|
||||||
this._updatePosition();
|
this._updatePosition();
|
||||||
} else {
|
}
|
||||||
|
if (!useFallback && !this._viewOnly) {
|
||||||
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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this._target.style.cursor = 'none';
|
this._resetNativeCursorStyle();
|
||||||
this._canvas.width = 0;
|
this._canvas.width = 0;
|
||||||
this._canvas.height = 0;
|
this._canvas.height = 0;
|
||||||
this._position.x = this._position.x + this._hotSpot.x;
|
this._position.x = this._position.x + this._hotSpot.x;
|
||||||
|
@ -115,6 +130,12 @@ export default class Cursor {
|
||||||
this._hotSpot.y = 0;
|
this._hotSpot.y = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resetNativeCursorStyle() {
|
||||||
|
if (this._target) {
|
||||||
|
this._target.style.cursor = this._viewOnly ? 'not-allowed' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
|
@ -136,19 +157,58 @@ export default class Cursor {
|
||||||
this._updateVisibility(target);
|
this._updateVisibility(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveRemote(remoteX, remoteY, scale) {
|
||||||
|
if (this._mouseOver && !this._viewOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetBounds = this._target.getBoundingClientRect();
|
||||||
|
this._position.x = targetBounds.left + remoteX * scale - this._hotSpot.x;
|
||||||
|
this._position.y = targetBounds.top + remoteY * scale - this._hotSpot.y;
|
||||||
|
|
||||||
|
this._updatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
_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
|
||||||
// sort things out.
|
// sort things out.
|
||||||
|
this._mouseOver = true;
|
||||||
this._handleMouseMove(event);
|
this._handleMouseMove(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleMouseLeave(event) {
|
_handleMouseLeave(event) {
|
||||||
|
if (this._viewOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetBounds = this._getVisibleBoundingRect(this._target);
|
||||||
|
this._mouseOver = event.clientX >= targetBounds.left && event.clientX < targetBounds.right &&
|
||||||
|
event.clientY >= targetBounds.top && event.clientY < targetBounds.bottom;
|
||||||
// Check if we should show the cursor on the element we are leaving to
|
// Check if we should show the cursor on the element we are leaving to
|
||||||
this._updateVisibility(event.relatedTarget);
|
this._updateVisibility(event.relatedTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getVisibleBoundingRect(element) {
|
||||||
|
let rect = element.getBoundingClientRect();
|
||||||
|
let bounds = { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom };
|
||||||
|
if (element.parentElement) {
|
||||||
|
let parentBounds = element.parentElement.getBoundingClientRect();
|
||||||
|
bounds = {
|
||||||
|
left: Math.max(bounds.left, parentBounds.left),
|
||||||
|
top: Math.max(bounds.top, parentBounds.top),
|
||||||
|
right: Math.min(bounds.right, parentBounds.right),
|
||||||
|
bottom: Math.min(bounds.bottom, parentBounds.bottom)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
_handleMouseMove(event) {
|
_handleMouseMove(event) {
|
||||||
|
if (this._viewOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._updateVisibility(event.target);
|
this._updateVisibility(event.target);
|
||||||
|
|
||||||
this._position.x = event.clientX - this._hotSpot.x;
|
this._position.x = event.clientX - this._hotSpot.x;
|
||||||
|
@ -158,6 +218,10 @@ export default class Cursor {
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleMouseUp(event) {
|
_handleMouseUp(event) {
|
||||||
|
if (this._viewOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We might get this event because of a drag operation that
|
// We might get this event because of a drag operation that
|
||||||
// moved outside of the target. Check what's under the cursor
|
// moved outside of the target. Check what's under the cursor
|
||||||
// now and adjust visibility based on that.
|
// now and adjust visibility based on that.
|
||||||
|
@ -230,7 +294,7 @@ export default class Cursor {
|
||||||
if (this._captureIsActive()) {
|
if (this._captureIsActive()) {
|
||||||
target = document.captureElement;
|
target = document.captureElement;
|
||||||
}
|
}
|
||||||
if (this._shouldShowCursor(target)) {
|
if (!this._mouseOver || (useFallback && this._shouldShowCursor(target))) {
|
||||||
this._showCursor();
|
this._showCursor();
|
||||||
} else {
|
} else {
|
||||||
this._hideCursor();
|
this._hideCursor();
|
||||||
|
|
|
@ -77,6 +77,10 @@ protocol stream.
|
||||||
if the remote session is smaller than its container, or handled
|
if the remote session is smaller than its container, or handled
|
||||||
according to `clipViewport` if it is larger. Disabled by default.
|
according to `clipViewport` if it is larger. Disabled by default.
|
||||||
|
|
||||||
|
`showRemoteCursor`
|
||||||
|
- Is a `boolean` indicating whether the remote cursor position should
|
||||||
|
be tracked. The server must support the respective pseudo encoding.
|
||||||
|
|
||||||
`showDotCursor`
|
`showDotCursor`
|
||||||
- Is a `boolean` indicating whether a dot cursor should be shown
|
- Is a `boolean` indicating whether a dot cursor should be shown
|
||||||
instead of a zero-sized or fully-transparent cursor if the server
|
instead of a zero-sized or fully-transparent cursor if the server
|
||||||
|
|
7
vnc.html
7
vnc.html
|
@ -289,6 +289,13 @@
|
||||||
<input id="noVNC_setting_reconnect_delay" type="number">
|
<input id="noVNC_setting_reconnect_delay" type="number">
|
||||||
</li>
|
</li>
|
||||||
<li><hr></li>
|
<li><hr></li>
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input id="noVNC_setting_show_remote_cursor" type="checkbox"
|
||||||
|
class="toggle">
|
||||||
|
Show remote cursor
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label>
|
<label>
|
||||||
<input id="noVNC_setting_show_dot" type="checkbox"
|
<input id="noVNC_setting_show_dot" type="checkbox"
|
||||||
|
|
Loading…
Reference in New Issue