summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSamuel Mannehed <samuel@cendio.se>2018-09-16 11:20:34 +0200
committerGitHub <noreply@github.com>2018-09-16 11:20:34 +0200
commit772c6867764044624f75bc76a76ec351caefacb7 (patch)
treed08a3281e0c72623607ab76e652e2dbb389f2cf2
parente15950a8efeca1b15adff3290135f10228961a1e (diff)
parent4c38179d15ee5011a9144843fc655194409dd506 (diff)
downloadnovnc-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.js9
-rw-r--r--core/rfb.js106
-rw-r--r--core/util/cursor.js19
-rw-r--r--docs/API.md5
-rw-r--r--docs/EMBEDDING.md3
-rw-r--r--vnc.html4
6 files changed, 123 insertions, 23 deletions
diff --git a/app/ui.js b/app/ui.js
index c03f8d3..8486d53 100644
--- a/app/ui.js
+++ b/app/ui.js
@@ -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`.
diff --git a/vnc.html b/vnc.html
index 14b8558..d00eec3 100644
--- a/vnc.html
+++ b/vnc.html
@@ -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: