Merge pull request #1119 from patrakov/master
Show dot when there otherwise would be no visible cursor
This commit is contained in:
commit
772c686776
|
@ -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'));
|
||||||
},
|
},
|
||||||
|
|
106
core/rfb.js
106
core/rfb.js
|
@ -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,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
||||||
|
|
4
vnc.html
4
vnc.html
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue