diff options
author | Samuel Mannehed <samuel@cendio.se> | 2018-09-16 11:20:34 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-09-16 11:20:34 +0200 |
commit | 772c6867764044624f75bc76a76ec351caefacb7 (patch) | |
tree | d08a3281e0c72623607ab76e652e2dbb389f2cf2 | |
parent | e15950a8efeca1b15adff3290135f10228961a1e (diff) | |
parent | 4c38179d15ee5011a9144843fc655194409dd506 (diff) | |
download | novnc-772c6867764044624f75bc76a76ec351caefacb7.tar.gz |
Merge pull request #1119 from patrakov/master
Show dot when there otherwise would be no visible cursor
-rw-r--r-- | app/ui.js | 9 | ||||
-rw-r--r-- | core/rfb.js | 106 | ||||
-rw-r--r-- | core/util/cursor.js | 19 | ||||
-rw-r--r-- | docs/API.md | 5 | ||||
-rw-r--r-- | docs/EMBEDDING.md | 3 | ||||
-rw-r--r-- | vnc.html | 4 |
6 files changed, 123 insertions, 23 deletions
@@ -161,6 +161,7 @@ const UI = { UI.initSetting('resize', 'off'); UI.initSetting('shared', true); UI.initSetting('view_only', false); + UI.initSetting('show_dot', false); UI.initSetting('path', 'websockify'); UI.initSetting('repeaterID', ''); UI.initSetting('reconnect', false); @@ -347,6 +348,8 @@ const UI = { UI.addSettingChangeHandler('shared'); UI.addSettingChangeHandler('view_only'); UI.addSettingChangeHandler('view_only', UI.updateViewOnly); + UI.addSettingChangeHandler('show_dot'); + UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor); UI.addSettingChangeHandler('host'); UI.addSettingChangeHandler('port'); UI.addSettingChangeHandler('path'); @@ -1015,6 +1018,7 @@ const UI = { UI.rfb = new RFB(document.getElementById('noVNC_container'), url, { shared: UI.getSetting('shared'), + showDotCursor: UI.getSetting('show_dot'), repeaterID: UI.getSetting('repeaterID'), credentials: { password: password } }); UI.rfb.addEventListener("connect", UI.connectFinished); @@ -1583,6 +1587,11 @@ const UI = { UI.setMouseButton(1); //has it's own logic for hiding/showing }, + updateShowDotCursor() { + if (!UI.rfb) return; + UI.rfb.showDotCursor = UI.getSetting('show_dot'); + }, + updateLogging() { WebUtil.init_logging(UI.getSetting('logging')); }, diff --git a/core/rfb.js b/core/rfb.js index a52c00d..9b59c89 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -48,6 +48,7 @@ export default class RFB extends EventTargetMixin { this._rfb_credentials = options.credentials || {}; this._shared = 'shared' in options ? !!options.shared : true; this._repeaterID = options.repeaterID || ''; + this._showDotCursor = options.showDotCursor || false; // Internal state this._rfb_connection_state = ''; @@ -166,7 +167,19 @@ export default class RFB extends EventTargetMixin { this._canvas.tabIndex = -1; this._screen.appendChild(this._canvas); - this._cursor = new Cursor(); + // Cursor + this._cursor = new Cursor(); + + // 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 + // is hit blindly. But there are also VNC servers that draw the cursor + // in the framebuffer and don't send the empty local cursor. There is + // no way to satisfy both sides. + // + // The spec is unclear on this "initial cursor" issue. Many other + // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the + // initial cursor instead. + this._cursorImage = RFB.cursors.none; // populate encHandlers with bound versions this._encHandlers[encodings.encodingRaw] = RFB.encodingHandlers.RAW.bind(this); @@ -316,6 +329,12 @@ export default class RFB extends EventTargetMixin { } } + get showDotCursor() { return this._showDotCursor; } + set showDotCursor(show) { + this._showDotCursor = show; + this._refreshCursor(); + } + // ===== PUBLIC METHODS ===== disconnect() { @@ -418,6 +437,7 @@ export default class RFB extends EventTargetMixin { this._target.appendChild(this._screen); this._cursor.attach(this._canvas); + this._refreshCursor(); // Monitor size changes of the screen // FIXME: Use ResizeObserver, or hidden overflow @@ -1601,6 +1621,44 @@ export default class RFB extends EventTargetMixin { RFB.messages.xvpOp(this._sock, ver, op); } + _updateCursor(rgba, hotx, hoty, w, h) { + this._cursorImage = { + rgbaPixels: rgba, + hotx: hotx, hoty: hoty, w: w, h: h, + }; + this._refreshCursor(); + } + + _shouldShowDotCursor() { + // Called when this._cursorImage is updated + if (!this._showDotCursor) { + // User does not want to see the dot, so... + return false; + } + + // The dot should not be shown if the cursor is already visible, + // i.e. contains at least one not-fully-transparent pixel. + // So iterate through all alpha bytes in rgba and stop at the + // first non-zero. + for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) { + if (this._cursorImage.rgbaPixels[i]) { + return false; + } + } + + // At this point, we know that the cursor is fully transparent, and + // the user wants to see the dot instead of this. + return true; + } + + _refreshCursor() { + const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage; + this._cursor.change(image.rgbaPixels, + image.hotx, image.hoty, + image.w, image.h + ); + } + static genDES(password, challenge) { const passwd = []; for (let i = 0; i < password.length; i++) { @@ -2521,20 +2579,36 @@ RFB.encodingHandlers = { Cursor() { Log.Debug(">> set_cursor"); - const x = this._FBU.x; // hotspot-x - const y = this._FBU.y; // hotspot-y + const hotx = this._FBU.x; // hotspot-x + const hoty = this._FBU.y; // hotspot-y const w = this._FBU.width; const h = this._FBU.height; const pixelslength = w * h * 4; - const masklength = Math.floor((w + 7) / 8) * h; + const masklength = Math.ceil(w / 8) * h; this._FBU.bytes = pixelslength + masklength; if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; } - this._cursor.change(this._sock.rQshiftBytes(pixelslength), - this._sock.rQshiftBytes(masklength), - x, y, w, h); + // Decode from BGRX pixels + bit mask to RGBA + const pixels = this._sock.rQshiftBytes(pixelslength); + const mask = this._sock.rQshiftBytes(masklength); + let rgba = new Uint8Array(w * h * 4); + + let pix_idx = 0; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + let mask_idx = y * Math.ceil(w / 8) + Math.floor(x / 8); + let alpha = (mask[mask_idx] << (x % 8)) & 0x80 ? 255 : 0; + rgba[pix_idx ] = pixels[pix_idx + 2]; + rgba[pix_idx + 1] = pixels[pix_idx + 1]; + rgba[pix_idx + 2] = pixels[pix_idx]; + rgba[pix_idx + 3] = alpha; + pix_idx += 4; + } + } + + this._updateCursor(rgba, hotx, hoty, w, h); this._FBU.bytes = 0; this._FBU.rects--; @@ -2557,3 +2631,21 @@ RFB.encodingHandlers = { } } } + +RFB.cursors = { + none: { + rgbaPixels: new Uint8Array(), + w: 0, h: 0, + hotx: 0, hoty: 0, + }, + + dot: { + rgbaPixels: new Uint8Array([ + 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, + 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, + 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, + ]), + w: 3, h: 3, + hotx: 1, hoty: 1, + } +}; diff --git a/core/util/cursor.js b/core/util/cursor.js index 18aa7be..7997194 100644 --- a/core/util/cursor.js +++ b/core/util/cursor.js @@ -79,25 +79,12 @@ export default class Cursor { this._target = null; } - change(pixels, mask, hotx, hoty, w, h) { + change(rgba, hotx, hoty, w, h) { if ((w === 0) || (h === 0)) { this.clear(); return; } - let cur = [] - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - let idx = y * Math.ceil(w / 8) + Math.floor(x / 8); - let alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; - idx = ((w * y) + x) * 4; - cur.push(pixels[idx + 2]); // red - cur.push(pixels[idx + 1]); // green - cur.push(pixels[idx]); // blue - cur.push(alpha); // alpha - } - } - this._position.x = this._position.x + this._hotSpot.x - hotx; this._position.y = this._position.y + this._hotSpot.y - hoty; this._hotSpot.x = hotx; @@ -111,10 +98,10 @@ export default class Cursor { let img; try { // IE doesn't support this - img = new ImageData(new Uint8ClampedArray(cur), w, h); + img = new ImageData(new Uint8ClampedArray(rgba), w, h); } catch (ex) { img = ctx.createImageData(w, h); - img.data.set(new Uint8ClampedArray(cur)); + img.data.set(new Uint8ClampedArray(rgba)); } ctx.clearRect(0, 0, w, h); ctx.putImageData(img, 0, 0); diff --git a/docs/API.md b/docs/API.md index a81da5c..ae7fb66 100644 --- a/docs/API.md +++ b/docs/API.md @@ -53,6 +53,11 @@ protocol stream. should be sent whenever the container changes dimensions. Disabled by default. +`showDotCursor` + - Is a `boolean` indicating whether a dot cursor should be shown + instead of a zero-sized or fully-transparent cursor if the server + sets such invisible cursor. Disabled by default. + `capabilities` *Read only* - Is an `Object` indicating which optional extensions are available on the server. Some methods may only be called if the corresponding diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index cad80ef..5399b48 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -61,6 +61,9 @@ query string. Currently the following options are available: * `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`. +* `show_dot` - If a dot cursor should be shown when the remote server provides + no local cursor, or provides a fully-transparent (invisible) cursor. + * `logging` - The console log level. Can be one of `error`, `warn`, `info` or `debug`. @@ -250,6 +250,10 @@ <input id="noVNC_setting_reconnect_delay" type="number" /> </li> <li><hr></li> + <li> + <label><input id="noVNC_setting_show_dot" type="checkbox" /> Show Dot when No Cursor</label> + </li> + <li><hr></li> <!-- Logging selection dropdown --> <li> <label>Logging: |