This commit is contained in:
jkanefendt 2025-05-28 12:57:16 +00:00 committed by GitHub
commit 87a00370c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 143 additions and 28 deletions

View File

@ -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');

View File

@ -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) {

View File

@ -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;

View File

@ -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();

View File

@ -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

View File

@ -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"