Merge pull request #1119 from patrakov/master

Show dot when there otherwise would be no visible cursor
This commit is contained in:
Samuel Mannehed 2018-09-16 11:20:34 +02:00 committed by GitHub
commit 772c686776
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 123 additions and 23 deletions

View File

@ -161,6 +161,7 @@ const UI = {
UI.initSetting('resize', 'off'); UI.initSetting('resize', 'off');
UI.initSetting('shared', true); UI.initSetting('shared', true);
UI.initSetting('view_only', false); UI.initSetting('view_only', false);
UI.initSetting('show_dot', false);
UI.initSetting('path', 'websockify'); UI.initSetting('path', 'websockify');
UI.initSetting('repeaterID', ''); UI.initSetting('repeaterID', '');
UI.initSetting('reconnect', false); UI.initSetting('reconnect', false);
@ -347,6 +348,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_dot');
UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
UI.addSettingChangeHandler('host'); UI.addSettingChangeHandler('host');
UI.addSettingChangeHandler('port'); UI.addSettingChangeHandler('port');
UI.addSettingChangeHandler('path'); UI.addSettingChangeHandler('path');
@ -1015,6 +1018,7 @@ const UI = {
UI.rfb = new RFB(document.getElementById('noVNC_container'), url, UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
{ shared: UI.getSetting('shared'), { shared: UI.getSetting('shared'),
showDotCursor: UI.getSetting('show_dot'),
repeaterID: UI.getSetting('repeaterID'), repeaterID: UI.getSetting('repeaterID'),
credentials: { password: password } }); credentials: { password: password } });
UI.rfb.addEventListener("connect", UI.connectFinished); UI.rfb.addEventListener("connect", UI.connectFinished);
@ -1583,6 +1587,11 @@ const UI = {
UI.setMouseButton(1); //has it's own logic for hiding/showing UI.setMouseButton(1); //has it's own logic for hiding/showing
}, },
updateShowDotCursor() {
if (!UI.rfb) return;
UI.rfb.showDotCursor = UI.getSetting('show_dot');
},
updateLogging() { updateLogging() {
WebUtil.init_logging(UI.getSetting('logging')); WebUtil.init_logging(UI.getSetting('logging'));
}, },

View File

@ -48,6 +48,7 @@ export default class RFB extends EventTargetMixin {
this._rfb_credentials = options.credentials || {}; this._rfb_credentials = options.credentials || {};
this._shared = 'shared' in options ? !!options.shared : true; this._shared = 'shared' in options ? !!options.shared : true;
this._repeaterID = options.repeaterID || ''; this._repeaterID = options.repeaterID || '';
this._showDotCursor = options.showDotCursor || false;
// Internal state // Internal state
this._rfb_connection_state = ''; this._rfb_connection_state = '';
@ -166,7 +167,19 @@ export default class RFB extends EventTargetMixin {
this._canvas.tabIndex = -1; this._canvas.tabIndex = -1;
this._screen.appendChild(this._canvas); 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 // populate encHandlers with bound versions
this._encHandlers[encodings.encodingRaw] = RFB.encodingHandlers.RAW.bind(this); 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 ===== // ===== PUBLIC METHODS =====
disconnect() { disconnect() {
@ -418,6 +437,7 @@ export default class RFB extends EventTargetMixin {
this._target.appendChild(this._screen); this._target.appendChild(this._screen);
this._cursor.attach(this._canvas); this._cursor.attach(this._canvas);
this._refreshCursor();
// Monitor size changes of the screen // Monitor size changes of the screen
// FIXME: Use ResizeObserver, or hidden overflow // FIXME: Use ResizeObserver, or hidden overflow
@ -1601,6 +1621,44 @@ export default class RFB extends EventTargetMixin {
RFB.messages.xvpOp(this._sock, ver, op); 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) { static genDES(password, challenge) {
const passwd = []; const passwd = [];
for (let i = 0; i < password.length; i++) { for (let i = 0; i < password.length; i++) {
@ -2521,20 +2579,36 @@ RFB.encodingHandlers = {
Cursor() { Cursor() {
Log.Debug(">> set_cursor"); Log.Debug(">> set_cursor");
const x = this._FBU.x; // hotspot-x const hotx = this._FBU.x; // hotspot-x
const y = this._FBU.y; // hotspot-y const hoty = this._FBU.y; // hotspot-y
const w = this._FBU.width; const w = this._FBU.width;
const h = this._FBU.height; const h = this._FBU.height;
const pixelslength = w * h * 4; 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; this._FBU.bytes = pixelslength + masklength;
if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; } if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; }
this._cursor.change(this._sock.rQshiftBytes(pixelslength), // Decode from BGRX pixels + bit mask to RGBA
this._sock.rQshiftBytes(masklength), const pixels = this._sock.rQshiftBytes(pixelslength);
x, y, w, h); 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.bytes = 0;
this._FBU.rects--; 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,
}
};

View File

@ -79,25 +79,12 @@ export default class Cursor {
this._target = null; this._target = null;
} }
change(pixels, mask, hotx, hoty, w, h) { change(rgba, hotx, hoty, w, h) {
if ((w === 0) || (h === 0)) { if ((w === 0) || (h === 0)) {
this.clear(); this.clear();
return; 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.x = this._position.x + this._hotSpot.x - hotx;
this._position.y = this._position.y + this._hotSpot.y - hoty; this._position.y = this._position.y + this._hotSpot.y - hoty;
this._hotSpot.x = hotx; this._hotSpot.x = hotx;
@ -111,10 +98,10 @@ export default class Cursor {
let img; let img;
try { try {
// IE doesn't support this // IE doesn't support this
img = new ImageData(new Uint8ClampedArray(cur), w, h); img = new ImageData(new Uint8ClampedArray(rgba), w, h);
} catch (ex) { } catch (ex) {
img = ctx.createImageData(w, h); img = ctx.createImageData(w, h);
img.data.set(new Uint8ClampedArray(cur)); img.data.set(new Uint8ClampedArray(rgba));
} }
ctx.clearRect(0, 0, w, h); ctx.clearRect(0, 0, w, h);
ctx.putImageData(img, 0, 0); ctx.putImageData(img, 0, 0);

View File

@ -53,6 +53,11 @@ protocol stream.
should be sent whenever the container changes dimensions. Disabled should be sent whenever the container changes dimensions. Disabled
by default. 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* `capabilities` *Read only*
- Is an `Object` indicating which optional extensions are available - Is an `Object` indicating which optional extensions are available
on the server. Some methods may only be called if the corresponding on the server. Some methods may only be called if the corresponding

View File

@ -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 * `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`. 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 * `logging` - The console log level. Can be one of `error`, `warn`, `info` or
`debug`. `debug`.

View File

@ -250,6 +250,10 @@
<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_dot" type="checkbox" /> Show Dot when No Cursor</label>
</li>
<li><hr></li>
<!-- Logging selection dropdown --> <!-- Logging selection dropdown -->
<li> <li>
<label>Logging: <label>Logging: