diff --git a/app/ui.js b/app/ui.js index 2542e0591..d0941d81f 100644 --- a/app/ui.js +++ b/app/ui.js @@ -178,6 +178,7 @@ const UI = { UI.initSetting('password'); UI.initSetting('autoconnect', false); UI.initSetting('view_clip', false); + UI.initSetting('view_drag', false); UI.initSetting('resize', 'off'); UI.initSetting('quality', 6); UI.initSetting('compression', 2); @@ -1096,11 +1097,17 @@ const UI = { UI.rfb.addEventListener("bell", UI.bell); UI.rfb.addEventListener("desktopname", UI.updateDesktopName); UI.rfb.clipViewport = UI.getSetting('view_clip'); + UI.rfb.dragViewport = UI.getSetting('view_drag'); UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); UI.rfb.compressionLevel = parseInt(UI.getSetting('compression')); UI.rfb.showDotCursor = UI.getSetting('show_dot'); + UI.rfb.showLocalCursor = { + drag: 'grab', + dragging: 'grabbing', + empty: 'default', + }; UI.updateViewOnly(); // requires UI.rfb }, diff --git a/core/rfb.js b/core/rfb.js index 80011e4a1..99848ce8b 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -233,6 +233,14 @@ export default class RFB extends EventTargetMixin { // Cursor this._cursor = new Cursor(); + this._showLocalCursor = false; + this._localCursors = { + dragging: null, + drag: null, + viewOnly: null, + default: null, + empty: null, + }; // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes // it. Result: no cursor at all until a window border or an edit field @@ -290,12 +298,12 @@ export default class RFB extends EventTargetMixin { // ===== PROPERTIES ===== - this.dragViewport = false; this.focusOnClick = true; this._viewOnly = false; this._clipViewport = false; this._clippingViewport = false; + this._dragViewport = false; this._scaleViewport = false; this._resizeSession = false; @@ -315,8 +323,10 @@ export default class RFB extends EventTargetMixin { this._rfbConnectionState === "connected") { if (viewOnly) { this._keyboard.ungrab(); + this._refreshCursor(); } else { this._keyboard.grab(); + this._refreshCursor(); } } } @@ -342,6 +352,12 @@ export default class RFB extends EventTargetMixin { this._updateClip(); } + get dragViewport() { return this._dragViewport; } + set dragViewport(dragViewport) { + this._dragViewport = dragViewport; + this._refreshCursor(); + } + get scaleViewport() { return this._scaleViewport; } set scaleViewport(scale) { this._scaleViewport = scale; @@ -370,6 +386,29 @@ export default class RFB extends EventTargetMixin { this._refreshCursor(); } + get showLocalCursor() { return this._showLocalCursor; } + set showLocalCursor(cursors) { + cursors ??= false; + this._showLocalCursor = !!cursors; + const { + default: defaultCursor, + viewOnly: viewOnlyCursor, + drag: dragCursor, + dragging: draggingCursor, + empty: emptyCursor, + } = cursors; + defaultCursor && (this._localCursors.default = defaultCursor); + viewOnlyCursor && (this._localCursors.viewOnly = viewOnlyCursor); + dragCursor && (this._localCursors.drag = dragCursor); + draggingCursor && (this._localCursors.dragging = draggingCursor); + emptyCursor && (this._localCursors.empty = emptyCursor); + this._cursor.detach(); + this._cursor.attach(this._canvas, { + showLocalCursor: this._showLocalCursor, + }); + this._refreshCursor(); + } + get background() { return this._screen.style.background; } set background(cssValue) { this._screen.style.background = cssValue; } @@ -574,7 +613,9 @@ export default class RFB extends EventTargetMixin { this._gestures.attach(this._canvas); - this._cursor.attach(this._canvas); + this._cursor.attach(this._canvas, { + showLocalCursor: this._showLocalCursor + }); this._refreshCursor(); // Monitor size changes of the screen element @@ -1111,16 +1152,23 @@ export default class RFB extends EventTargetMixin { let bmask = RFB._convertButtonMask(ev.buttons); - let down = ev.type == 'mousedown'; + let down = false; switch (ev.type) { case 'mousedown': + down = true; + // eslint-disable-next-line no-fallthrough case 'mouseup': - if (this.dragViewport) { + if (this._dragViewport) { if (down && !this._viewportDragging) { this._viewportDragging = true; this._viewportDragPos = {'x': pos.x, 'y': pos.y}; this._viewportHasMoved = false; + if (this._showLocalCursor) { + this._refreshCursor(); + this._cursor.detach(); + } + this._flushMouseMoveTimer(pos.x, pos.y); // Skip sending mouse events, instead save the current @@ -1130,6 +1178,13 @@ export default class RFB extends EventTargetMixin { } else { this._viewportDragging = false; + if (this._showLocalCursor) { + this._cursor.attach(this._canvas, { + showLocalCursor: this._showLocalCursor, + }); + this._refreshCursor(); + } + // If we actually performed a drag then we are done // here and should not send any mouse events if (this._viewportHasMoved) { @@ -1334,7 +1389,7 @@ export default class RFB extends EventTargetMixin { this._handleTapEvent(ev, 0x2); break; case 'drag': - if (this.dragViewport) { + if (this._dragViewport) { this._viewportHasMoved = false; this._viewportDragging = true; this._viewportDragPos = {'x': pos.x, 'y': pos.y}; @@ -1344,7 +1399,7 @@ export default class RFB extends EventTargetMixin { } break; case 'longpress': - if (this.dragViewport) { + if (this._dragViewport) { // If dragViewport is true, we need to wait to see // if we have dragged outside the threshold before // sending any events to the server. @@ -1376,7 +1431,7 @@ export default class RFB extends EventTargetMixin { break; case 'drag': case 'longpress': - if (this.dragViewport) { + if (this._dragViewport) { this._viewportDragging = true; const deltaX = this._viewportDragPos.x - pos.x; const deltaY = this._viewportDragPos.y - pos.y; @@ -1451,7 +1506,7 @@ export default class RFB extends EventTargetMixin { case 'twodrag': break; case 'drag': - if (this.dragViewport) { + if (this._dragViewport) { this._viewportDragging = false; } else { this._fakeMouseMove(ev, pos.x, pos.y); @@ -1465,7 +1520,7 @@ export default class RFB extends EventTargetMixin { break; } - if (this.dragViewport && !this._viewportHasMoved) { + if (this._dragViewport && !this._viewportHasMoved) { this._fakeMouseMove(ev, pos.x, pos.y); // If dragViewport is true, we need to wait to see // if we have dragged outside the threshold before @@ -3032,10 +3087,20 @@ export default class RFB extends EventTargetMixin { _shouldShowDotCursor() { // Called when this._cursorImage is updated - if (!this._showDotCursor) { - // User does not want to see the dot, so... + if (!this._showDotCursor && !(this._showLocalCursor && this._localCursors.empty)) { + // User does not want to see the dot or has no local cursor, so... return false; } + if (this._showLocalCursor) { + // Do not show the dot in states with a local cursor + if (this._viewportDragging) { + if (this._localCursors.dragging) { return false; } + } else if (this._dragViewport) { + if (this._localCursors.drag) { return false; } + } else if (this._viewOnly) { + if (this._localCursors.viewOnly) { return false; } + } + } // The dot should not be shown if the cursor is already visible, // i.e. contains at least one not-fully-transparent pixel. @@ -3057,6 +3122,10 @@ export default class RFB extends EventTargetMixin { this._rfbConnectionState !== "connected") { return; } + if (this._showLocalCursor) { + this._refreshCursorWithLocalCursors(); + return; + } const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage; this._cursor.change(image.rgbaPixels, image.hotx, image.hoty, @@ -3064,6 +3133,30 @@ export default class RFB extends EventTargetMixin { ); } + _refreshCursorWithLocalCursors() { + let image = this._cursorImage; + let localCursor; // = 'none'; + if (this._viewportDragging) { + localCursor = this._localCursors.dragging; + } else if (this._dragViewport) { + localCursor = this._localCursors.drag; + } else if (this._viewOnly) { + localCursor = this._localCursors.viewOnly; + // clear locally rendered cursor when switching to view-only whilst connected + image = RFB.cursors.none; + } else if (this._shouldShowDotCursor()) { + localCursor = this._localCursors.empty; + image = this._showDotCursor ? RFB.cursors.dot : this._cursorImage; + } else { + localCursor = this._localCursors.default; + } + this._cursor.change(image.rgbaPixels, + image.hotx, image.hoty, + image.w, image.h, + { localCursor } + ); + } + static genDES(password, challenge) { const passwordChars = password.split('').map(c => c.charCodeAt(0)); const key = legacyCrypto.importKey( diff --git a/core/util/cursor.js b/core/util/cursor.js index 6d689e7d5..9670b7a72 100644 --- a/core/util/cursor.js +++ b/core/util/cursor.js @@ -6,7 +6,12 @@ import { supportsCursorURIs, isTouchDevice } from './browser.js'; -const useFallback = !supportsCursorURIs || isTouchDevice; +// Sometimes (at least with Chrome and Firefox on Windows) +// isTouchDevice is true even if there is no touch device, in +// this case useFallback ist also true and cursor URIs are never +// used. +const __FORCE_NO_TOUCH_DEVICE__ = false; +const useFallback = !supportsCursorURIs || (!__FORCE_NO_TOUCH_DEVICE__ && isTouchDevice); export default class Cursor { constructor() { @@ -14,17 +19,16 @@ export default class Cursor { this._canvas = document.createElement('canvas'); - if (useFallback) { - this._canvas.style.position = 'fixed'; - this._canvas.style.zIndex = '65535'; - this._canvas.style.pointerEvents = 'none'; - // Safari on iOS can select the cursor image - // https://bugs.webkit.org/show_bug.cgi?id=249223 - this._canvas.style.userSelect = 'none'; - this._canvas.style.WebkitUserSelect = 'none'; - // Can't use "display" because of Firefox bug #1445997 - this._canvas.style.visibility = 'hidden'; - } + // always initalize canvas.style in case of showing local cursors + this._canvas.style.position = 'fixed'; + this._canvas.style.zIndex = '65535'; + this._canvas.style.pointerEvents = 'none'; + // Safari on iOS can select the cursor image + // https://bugs.webkit.org/show_bug.cgi?id=249223 + this._canvas.style.userSelect = 'none'; + this._canvas.style.WebkitUserSelect = 'none'; + // Can't use "display" because of Firefox bug #1445997 + this._canvas.style.visibility = 'hidden'; this._position = { x: 0, y: 0 }; this._hotSpot = { x: 0, y: 0 }; @@ -37,14 +41,15 @@ export default class Cursor { }; } - attach(target) { + attach(target, { showLocalCursor } = {}) { if (this._target) { this.detach(); } this._target = target; + this._showLocalCursor = !!showLocalCursor; - if (useFallback) { + if (useFallback || this._showLocalCursor) { document.body.appendChild(this._canvas); const options = { capture: true, passive: true }; @@ -54,7 +59,7 @@ export default class Cursor { this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options); } - this.clear(); + this.clear({ localCursor: showLocalCursor }); } detach() { @@ -62,7 +67,7 @@ export default class Cursor { return; } - if (useFallback) { + if (useFallback || this._showLocalCursor) { const options = { capture: true, passive: true }; this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options); this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options); @@ -77,9 +82,9 @@ export default class Cursor { this._target = null; } - change(rgba, hotx, hoty, w, h) { + change(rgba, hotx, hoty, w, h, { localCursor } = {}) { if ((w === 0) || (h === 0)) { - this.clear(); + this.clear({ localCursor }); return; } @@ -97,7 +102,8 @@ export default class Cursor { ctx.clearRect(0, 0, w, h); ctx.putImageData(img, 0, 0); - if (useFallback) { + if (useFallback || this._showLocalCursor) { + this._target.style.cursor = localCursor ?? 'none'; this._updatePosition(); } else { let url = this._canvas.toDataURL(); @@ -105,8 +111,12 @@ export default class Cursor { } } - clear() { - this._target.style.cursor = 'none'; + clear({ localCursor } = {}) { + // whilst dragging the viewport and changes to the remote cursor + // are made the target might be detached + if (this._target) { + this._target.style.cursor = localCursor ?? 'none'; + } this._canvas.width = 0; this._canvas.height = 0; this._position.x = this._position.x + this._hotSpot.x; @@ -118,7 +128,7 @@ export default class Cursor { // Mouse events might be emulated, this allows // moving the cursor in such cases move(clientX, clientY) { - if (!useFallback) { + if (!useFallback && !this._showLocalCursor) { return; } // clientX/clientY are relative the _visual viewport_, diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index 9e927d0d3..32ac049e7 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -82,6 +82,9 @@ Currently, the following options are available: * `view_clip` - If the remote session should be clipped or use scrollbars if it cannot fit in the browser. +* `view_drag` - If the remote session is clipped enable dragging the viewport + when connected. + * `resize` - How to resize the remote session if it is not the same size as the browser window. Can be one of `off`, `scale` and `remote`. diff --git a/vnc.html b/vnc.html index 82cacd580..980ee2854 100644 --- a/vnc.html +++ b/vnc.html @@ -227,6 +227,10 @@

no
VNC

Clip to window +
  • + + +