This commit is contained in:
Pierre Ossman 2017-11-10 14:19:05 +01:00
commit a201bfc5eb
19 changed files with 1675 additions and 1871 deletions

View File

@ -591,17 +591,17 @@ select:active {
padding: 0 10px;
}
/* XVP Shutdown/Reboot */
:root:not(.noVNC_connected) #noVNC_xvp_button {
/* Shutdown/Reboot */
:root:not(.noVNC_connected) #noVNC_power_button {
display: none;
}
#noVNC_xvp {
#noVNC_power {
}
#noVNC_xvp_buttons {
#noVNC_power_buttons {
display: none;
}
#noVNC_xvp input[type=button] {
#noVNC_power input[type=button] {
width: 100%;
}

252
app/ui.js
View File

@ -13,7 +13,7 @@
import * as Log from '../core/util/logging.js';
import _, { l10n } from '../core/util/localization.js';
import { isTouchDevice, browserSupportsCursorURIs as cursorURIsSupported } from '../core/util/browsers.js';
import { isTouchDevice } from '../core/util/browsers.js';
import { setCapture, getPointerEvent } from '../core/util/events.js';
import KeyTable from "../core/input/keysym.js";
import keysyms from "../core/input/keysymdef.js";
@ -91,7 +91,7 @@ var UI = {
UI.addControlbarHandlers();
UI.addTouchSpecificHandlers();
UI.addExtraKeysHandlers();
UI.addXvpHandlers();
UI.addMachineHandlers();
UI.addConnectionControlHandlers();
UI.addClipboardHandlers();
UI.addSettingsHandlers();
@ -167,7 +167,6 @@ var UI = {
UI.initSetting('host', window.location.hostname);
UI.initSetting('port', port);
UI.initSetting('encrypt', (window.location.protocol === "https:"));
UI.initSetting('cursor', !isTouchDevice);
UI.initSetting('view_clip', false);
UI.initSetting('resize', 'off');
UI.initSetting('shared', true);
@ -200,28 +199,6 @@ var UI = {
}
},
initRFB: function() {
try {
UI.rfb = new RFB({'target': document.getElementById('noVNC_canvas'),
'onNotification': UI.notification,
'onUpdateState': UI.updateState,
'onDisconnected': UI.disconnectFinished,
'onPasswordRequired': UI.passwordRequired,
'onXvpInit': UI.updateXvpButton,
'onClipboard': UI.clipboardReceive,
'onBell': UI.bell,
'onFBUComplete': UI.initialResize,
'onFBResize': UI.updateSessionSize,
'onDesktopName': UI.updateDesktopName});
return true;
} catch (exc) {
var msg = "Unable to create RFB client -- " + exc;
Log.Error(msg);
UI.showStatus(msg, 'error');
return false;
}
},
/* ------^-------
* /INIT
* ==============
@ -278,8 +255,8 @@ var UI = {
document.getElementById("noVNC_keyboard_button")
.addEventListener('click', UI.toggleVirtualKeyboard);
UI.touchKeyboard = new Keyboard({target: document.getElementById('noVNC_keyboardinput'),
onKeyEvent: UI.keyEvent});
UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput'));
UI.touchKeyboard.onkeyevent = UI.keyEvent;
UI.touchKeyboard.grab();
document.getElementById("noVNC_keyboardinput")
.addEventListener('input', UI.keyInput);
@ -330,15 +307,15 @@ var UI = {
.addEventListener('click', UI.sendCtrlAltDel);
},
addXvpHandlers: function() {
document.getElementById("noVNC_xvp_shutdown_button")
.addEventListener('click', function() { UI.rfb.xvpShutdown(); });
document.getElementById("noVNC_xvp_reboot_button")
.addEventListener('click', function() { UI.rfb.xvpReboot(); });
document.getElementById("noVNC_xvp_reset_button")
.addEventListener('click', function() { UI.rfb.xvpReset(); });
document.getElementById("noVNC_xvp_button")
.addEventListener('click', UI.toggleXvpPanel);
addMachineHandlers: function() {
document.getElementById("noVNC_shutdown_button")
.addEventListener('click', function() { UI.rfb.machineShutdown(); });
document.getElementById("noVNC_reboot_button")
.addEventListener('click', function() { UI.rfb.machineReboot(); });
document.getElementById("noVNC_reset_button")
.addEventListener('click', function() { UI.rfb.machineReset(); });
document.getElementById("noVNC_power_button")
.addEventListener('click', UI.togglePowerPanel);
},
addConnectionControlHandlers: function() {
@ -377,8 +354,6 @@ var UI = {
.addEventListener('click', UI.toggleSettingsPanel);
UI.addSettingChangeHandler('encrypt');
UI.addSettingChangeHandler('cursor');
UI.addSettingChangeHandler('cursor', UI.updateLocalCursor);
UI.addSettingChangeHandler('resize');
UI.addSettingChangeHandler('resize', UI.enableDisableViewClip);
UI.addSettingChangeHandler('resize', UI.applyResizeMode);
@ -413,7 +388,7 @@ var UI = {
* VISUAL
* ------v------*/
updateState: function(rfb, state, oldstate) {
updateState: function(event) {
var msg;
document.documentElement.classList.remove("noVNC_connecting");
@ -421,7 +396,7 @@ var UI = {
document.documentElement.classList.remove("noVNC_disconnecting");
document.documentElement.classList.remove("noVNC_reconnecting");
switch (state) {
switch (event.detail.state) {
case 'connecting':
document.getElementById("noVNC_transition_text").textContent = _("Connecting...");
document.documentElement.classList.add("noVNC_connecting");
@ -429,8 +404,9 @@ var UI = {
case 'connected':
UI.connected = true;
UI.inhibit_reconnect = false;
UI.doneInitialResize = false;
document.documentElement.classList.add("noVNC_connected");
if (rfb && rfb.get_encrypt()) {
if (UI.getSetting('encrypt')) {
msg = _("Connected (encrypted) to ") + UI.desktopName;
} else {
msg = _("Connected (unencrypted) to ") + UI.desktopName;
@ -462,12 +438,6 @@ var UI = {
UI.enableDisableViewClip();
if (cursorURIsSupported() && !isTouchDevice) {
UI.enableSetting('cursor');
} else {
UI.disableSetting('cursor');
}
if (UI.connected) {
UI.disableSetting('encrypt');
UI.disableSetting('shared');
@ -487,12 +457,12 @@ var UI = {
UI.enableSetting('port');
UI.enableSetting('path');
UI.enableSetting('repeaterID');
UI.updateXvpButton(0);
UI.updatePowerButton();
UI.keepControlbar();
}
// Hide input related buttons in view only mode
if (UI.rfb && UI.rfb.get_view_only()) {
if (UI.rfb && UI.rfb.viewOnly) {
document.getElementById('noVNC_keyboard_button')
.classList.add('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button')
@ -562,8 +532,8 @@ var UI = {
document.getElementById('noVNC_status').classList.remove("noVNC_open");
},
notification: function (rfb, msg, level, options) {
UI.showStatus(msg, level);
notification: function (e) {
UI.showStatus(e.detail.message, e.detail.level);
},
activateControlbar: function(event) {
@ -866,7 +836,7 @@ var UI = {
closeAllPanels: function() {
UI.closeSettingsPanel();
UI.closeXvpPanel();
UI.closePowerPanel();
UI.closeClipboardPanel();
UI.closeExtraKeys();
},
@ -883,12 +853,6 @@ var UI = {
// Refresh UI elements from saved cookies
UI.updateSetting('encrypt');
if (cursorURIsSupported()) {
UI.updateSetting('cursor');
} else {
UI.updateSetting('cursor', !isTouchDevice);
UI.disableSetting('cursor');
}
UI.updateSetting('view_clip');
UI.updateSetting('resize');
UI.updateSetting('shared');
@ -924,50 +888,52 @@ var UI = {
/* ------^-------
* /SETTINGS
* ==============
* XVP
* POWER
* ------v------*/
openXvpPanel: function() {
openPowerPanel: function() {
UI.closeAllPanels();
UI.openControlbar();
document.getElementById('noVNC_xvp')
document.getElementById('noVNC_power')
.classList.add("noVNC_open");
document.getElementById('noVNC_xvp_button')
document.getElementById('noVNC_power_button')
.classList.add("noVNC_selected");
},
closeXvpPanel: function() {
document.getElementById('noVNC_xvp')
closePowerPanel: function() {
document.getElementById('noVNC_power')
.classList.remove("noVNC_open");
document.getElementById('noVNC_xvp_button')
document.getElementById('noVNC_power_button')
.classList.remove("noVNC_selected");
},
toggleXvpPanel: function() {
if (document.getElementById('noVNC_xvp')
togglePowerPanel: function() {
if (document.getElementById('noVNC_power')
.classList.contains("noVNC_open")) {
UI.closeXvpPanel();
UI.closePowerPanel();
} else {
UI.openXvpPanel();
UI.openPowerPanel();
}
},
// Disable/enable XVP button
updateXvpButton: function(ver) {
if (ver >= 1 && !UI.rfb.get_view_only()) {
document.getElementById('noVNC_xvp_button')
// Disable/enable power button
updatePowerButton: function() {
if (UI.connected &&
UI.rfb.capabilities.power &&
!UI.rfb.viewOnly) {
document.getElementById('noVNC_power_button')
.classList.remove("noVNC_hidden");
} else {
document.getElementById('noVNC_xvp_button')
document.getElementById('noVNC_power_button')
.classList.add("noVNC_hidden");
// Close XVP panel if open
UI.closeXvpPanel();
// Close power panel if open
UI.closePowerPanel();
}
},
/* ------^-------
* /XVP
* /POWER
* ==============
* CLIPBOARD
* ------v------*/
@ -998,9 +964,9 @@ var UI = {
}
},
clipboardReceive: function(rfb, text) {
Log.Debug(">> UI.clipboardReceive: " + text.substr(0,40) + "...");
document.getElementById('noVNC_clipboard_text').value = text;
clipboardReceive: function(e) {
Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0,40) + "...");
document.getElementById('noVNC_clipboard_text').value = e.detail.text;
Log.Debug("<< UI.clipboardReceive");
},
@ -1053,19 +1019,34 @@ var UI = {
return;
}
if (!UI.initRFB()) return;
UI.closeAllPanels();
UI.closeConnectPanel();
UI.rfb.set_encrypt(UI.getSetting('encrypt'));
UI.rfb.set_shared(UI.getSetting('shared'));
UI.rfb.set_repeaterID(UI.getSetting('repeaterID'));
UI.updateLocalCursor();
UI.updateViewOnly();
UI.rfb.connect(host, port, password, path);
var url;
url = UI.getSetting('encrypt') ? 'wss' : 'ws';
url += '://' + host;
if(port) {
url += ':' + port;
}
url += '/' + path;
UI.rfb = new RFB(document.getElementById('noVNC_canvas'), url,
{ shared: UI.getSetting('shared'),
repeaterID: UI.getSetting('repeaterID'),
credentials: { password: password } });
UI.rfb.addEventListener("notification", UI.notification);
UI.rfb.addEventListener("updatestate", UI.updateState);
UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
UI.rfb.addEventListener("credentialsrequired", UI.credentials);
UI.rfb.addEventListener("capabilities", function () { UI.updatePowerButton(); UI.initialResize(); });
UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
UI.rfb.addEventListener("bell", UI.bell);
UI.rfb.addEventListener("fbresize", UI.updateSessionSize);
UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
},
disconnect: function() {
@ -1075,9 +1056,6 @@ var UI = {
// Disable automatic reconnecting
UI.inhibit_reconnect = true;
// Restore the callback used for initial resize
UI.rfb.set_onFBUComplete(UI.initialResize);
// Don't display the connection settings until we're actually disconnected
},
@ -1092,9 +1070,9 @@ var UI = {
UI.connect(null, UI.reconnect_password);
},
disconnectFinished: function (rfb, reason) {
if (typeof reason !== 'undefined') {
UI.showStatus(reason, 'error');
disconnectFinished: function (e) {
if (typeof e.detail.reason !== 'undefined') {
UI.showStatus(e.detail.reason, 'error');
} else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) {
document.getElementById("noVNC_transition_text").textContent = _("Reconnecting...");
document.documentElement.classList.add("noVNC_reconnecting");
@ -1125,8 +1103,8 @@ var UI = {
* PASSWORD
* ------v------*/
passwordRequired: function(rfb, msg) {
credentials: function(e) {
// FIXME: handle more types
document.getElementById('noVNC_password_dlg')
.classList.add('noVNC_open');
@ -1134,24 +1112,23 @@ var UI = {
document.getElementById('noVNC_password_input').focus();
}, 100);
if (typeof msg === 'undefined') {
msg = _("Password is required");
}
var msg = _("Password is required");
Log.Warn(msg);
UI.showStatus(msg, "warning");
},
setPassword: function(e) {
// Prevent actually submitting the form
e.preventDefault();
var inputElem = document.getElementById('noVNC_password_input');
var password = inputElem.value;
// Clear the input after reading the password
inputElem.value = "";
UI.rfb.sendPassword(password);
UI.rfb.sendCredentials({ password: password });
UI.reconnect_password = password;
document.getElementById('noVNC_password_dlg')
.classList.remove('noVNC_open');
// Prevent actually submitting the form
e.preventDefault();
},
/* ------^-------
@ -1214,11 +1191,10 @@ var UI = {
var screen = UI.screenSize();
if (screen && UI.connected && UI.rfb.get_display()) {
if (screen && UI.connected) {
var display = UI.rfb.get_display();
var resizeMode = UI.getSetting('resize');
display.set_scale(1);
UI.rfb.viewportScale = 1.0;
// Make sure the viewport is adjusted first
UI.updateViewClip();
@ -1250,19 +1226,17 @@ var UI = {
if (!UI.rfb) return;
var resizeMode = UI.getSetting('resize');
if (resizeMode !== 'scale' && resizeMode !== 'downscale') {
if (resizeMode !== 'scale') {
return;
}
var screen = UI.screenSize();
if (!screen || !UI.connected || !UI.rfb.get_display()) {
if (!screen || !UI.connected) {
return;
}
var display = UI.rfb.get_display();
var downscaleOnly = resizeMode === 'downscale';
display.autoscale(screen.w, screen.h, downscaleOnly);
UI.rfb.autoscale(screen.w, screen.h);
UI.fixScrollbars();
},
@ -1275,13 +1249,14 @@ var UI = {
// Normally we only apply the current resize mode after a window resize
// event. This means that when a new connection is opened, there is no
// resize mode active.
// We have to wait until the first FBU because this is where the client
// will find the supported encodings of the server. Some calls later in
// the chain is dependant on knowing the server-capabilities.
initialResize: function(rfb, fbu) {
// We have to wait until we know the capabilities of the server as
// some calls later in the chain is dependant on knowing the
// server-capabilities.
initialResize: function() {
if (UI.doneInitialResize) return;
UI.applyResizeMode();
// After doing this once, we remove the callback.
UI.rfb.set_onFBUComplete(function() { });
UI.doneInitialResize = true;
},
/* ------^-------
@ -1300,12 +1275,11 @@ var UI = {
updateViewClip: function() {
if (!UI.rfb) return;
var display = UI.rfb.get_display();
var cur_clip = display.get_viewport();
var cur_clip = UI.rfb.clipViewport;
var new_clip = UI.getSetting('view_clip');
var resizeSetting = UI.getSetting('resize');
if (resizeSetting === 'downscale' || resizeSetting === 'scale') {
if (resizeSetting === 'scale') {
// Disable viewport clipping if we are scaling
new_clip = false;
} else if (isTouchDevice) {
@ -1314,7 +1288,7 @@ var UI = {
}
if (cur_clip !== new_clip) {
display.set_viewport(new_clip);
UI.rfb.clipViewport = new_clip;
}
var size = UI.screenSize();
@ -1322,7 +1296,7 @@ var UI = {
if (new_clip && size) {
// When clipping is enabled, the screen is limited to
// the size of the browser window.
display.viewportChangeSize(size.w, size.h);
UI.rfb.viewportChangeSize(size.w, size.h);
UI.fixScrollbars();
}
@ -1335,7 +1309,7 @@ var UI = {
enableDisableViewClip: function() {
var resizeSetting = UI.getSetting('resize');
// Disable clipping if we are scaling, connected or on touch
if (resizeSetting === 'downscale' || resizeSetting === 'scale' ||
if (resizeSetting === 'scale' ||
isTouchDevice) {
UI.disableSetting('view_clip');
} else {
@ -1352,7 +1326,7 @@ var UI = {
toggleViewDrag: function() {
if (!UI.rfb) return;
var drag = UI.rfb.get_viewportDrag();
var drag = UI.rfb.dragViewport;
UI.setViewDrag(!drag);
},
@ -1360,7 +1334,7 @@ var UI = {
setViewDrag: function(drag) {
if (!UI.rfb) return;
UI.rfb.set_viewportDrag(drag);
UI.rfb.dragViewport = drag;
UI.updateViewDrag();
},
@ -1372,22 +1346,21 @@ var UI = {
// Check if viewport drag is possible. It is only possible
// if the remote display is clipping the client display.
if (UI.rfb.get_display().get_viewport() &&
UI.rfb.get_display().clippingDisplay()) {
if (UI.rfb.clipViewport && UI.rfb.isClipped) {
clipping = true;
}
var viewDragButton = document.getElementById('noVNC_view_drag_button');
if (!clipping &&
UI.rfb.get_viewportDrag()) {
UI.rfb.dragViewport) {
// The size of the remote display is the same or smaller
// than the client display. Make sure viewport drag isn't
// active when it can't be used.
UI.rfb.set_viewportDrag(false);
UI.rfb.dragViewport = false;
}
if (UI.rfb.get_viewportDrag()) {
if (UI.rfb.dragViewport) {
viewDragButton.classList.add("noVNC_selected");
} else {
viewDragButton.classList.remove("noVNC_selected");
@ -1655,9 +1628,9 @@ var UI = {
* ------v------*/
setMouseButton: function(num) {
var view_only = UI.rfb.get_view_only();
var view_only = UI.rfb.viewOnly;
if (UI.rfb && !view_only) {
UI.rfb.get_mouse().set_touchButton(num);
UI.rfb.touchButton = num;
}
var blist = [0, 1,2,4];
@ -1672,21 +1645,16 @@ var UI = {
}
},
updateLocalCursor: function() {
if (!UI.rfb) return;
UI.rfb.set_local_cursor(UI.getSetting('cursor'));
},
updateViewOnly: function() {
if (!UI.rfb) return;
UI.rfb.set_view_only(UI.getSetting('view_only'));
UI.rfb.viewOnly = UI.getSetting('view_only');
},
updateLogging: function() {
WebUtil.init_logging(UI.getSetting('logging'));
},
updateSessionSize: function(rfb, width, height) {
updateSessionSize: function(e) {
UI.updateViewClip();
UI.updateScaling();
UI.fixScrollbars();
@ -1704,13 +1672,13 @@ var UI = {
screen.style.overflow = "";
},
updateDesktopName: function(rfb, name) {
UI.desktopName = name;
updateDesktopName: function(e) {
UI.desktopName = e.detail.name;
// Display the desktop name in the document title
document.title = name + " - noVNC";
document.title = e.detail.name + " - noVNC";
},
bell: function(rfb) {
bell: function(e) {
if (WebUtil.getConfigVar('bell', 'on') === 'on') {
var promise = document.getElementById('noVNC_bell').play();
// The standards disagree on the return value here

View File

@ -10,12 +10,10 @@
/*jslint browser: true, white: false */
/*global Util, Base64, changeCursor */
import { browserSupportsCursorURIs as cursorURIsSupported } from './util/browsers.js';
import { set_defaults, make_properties } from './util/properties.js';
import * as Log from './util/logging.js';
import Base64 from "./base64.js";
export default function Display(defaults) {
export default function Display(target) {
this._drawCtx = null;
this._c_forceCanvas = false;
@ -32,16 +30,11 @@ export default function Display(defaults) {
this._tile_x = 0;
this._tile_y = 0;
set_defaults(this, defaults, {
'scale': 1.0,
'viewport': false,
'render_mode': '',
"onFlush": function () {},
});
Log.Debug(">> Display.constructor");
// The visible canvas
this._target = target;
if (!this._target) {
throw new Error("Target must be set");
}
@ -72,23 +65,10 @@ export default function Display(defaults) {
this.clear();
// Check canvas features
if ('createImageData' in this._drawCtx) {
this._render_mode = 'canvas rendering';
} else {
if (!('createImageData' in this._drawCtx)) {
throw new Error("Canvas does not support createImageData");
}
if (this._prefer_js === null) {
Log.Info("Prefering javascript operations");
this._prefer_js = true;
}
// Determine browser support for setting the cursor via data URI scheme
if (this._cursor_uri || this._cursor_uri === null ||
this._cursor_uri === undefined) {
this._cursor_uri = cursorURIsSupported();
}
Log.Debug("<< Display.constructor");
};
@ -101,13 +81,50 @@ try {
}
Display.prototype = {
// Public methods
// ===== PROPERTIES =====
_scale: 1.0,
get scale() { return this._scale; },
set scale(scale) {
this._rescale(scale);
},
_clipViewport: false,
get clipViewport() { return this._clipViewport; },
set clipViewport(viewport) {
this._clipViewport = viewport;
// May need to readjust the viewport dimensions
var vp = this._viewportLoc;
this.viewportChangeSize(vp.w, vp.h);
this.viewportChangePos(0, 0);
},
get width() {
return this._fb_width;
},
get height() {
return this._fb_height;
},
get isClipped() {
var vp = this._viewportLoc;
return this._fb_width > vp.w || this._fb_height > vp.h;
},
logo: null,
// ===== EVENT HANDLERS =====
onflush: function () {}, // A flush request has finished
// ===== PUBLIC METHODS =====
viewportChangePos: function (deltaX, deltaY) {
var vp = this._viewportLoc;
deltaX = Math.floor(deltaX);
deltaY = Math.floor(deltaY);
if (!this._viewport) {
if (!this._clipViewport) {
deltaX = -vp.w; // clamped later of out of bounds
deltaY = -vp.h;
}
@ -146,7 +163,7 @@ Display.prototype = {
viewportChangeSize: function(width, height) {
if (!this._viewport ||
if (!this._clipViewport ||
typeof(width) === "undefined" ||
typeof(height) === "undefined") {
@ -307,7 +324,7 @@ Display.prototype = {
flush: function() {
if (this._renderQ.length === 0) {
this._onFlush();
this.onflush();
} else {
this._flushing = true;
}
@ -382,56 +399,45 @@ Display.prototype = {
this._tile = this._drawCtx.createImageData(width, height);
}
if (this._prefer_js) {
var red = color[2];
var green = color[1];
var blue = color[0];
var red = color[2];
var green = color[1];
var blue = color[0];
var data = this._tile.data;
for (var i = 0; i < width * height * 4; i += 4) {
data[i] = red;
data[i + 1] = green;
data[i + 2] = blue;
data[i + 3] = 255;
}
} else {
this.fillRect(x, y, width, height, color, true);
var data = this._tile.data;
for (var i = 0; i < width * height * 4; i += 4) {
data[i] = red;
data[i + 1] = green;
data[i + 2] = blue;
data[i + 3] = 255;
}
},
// update sub-rectangle of the current tile
subTile: function (x, y, w, h, color) {
if (this._prefer_js) {
var red = color[2];
var green = color[1];
var blue = color[0];
var xend = x + w;
var yend = y + h;
var red = color[2];
var green = color[1];
var blue = color[0];
var xend = x + w;
var yend = y + h;
var data = this._tile.data;
var width = this._tile.width;
for (var j = y; j < yend; j++) {
for (var i = x; i < xend; i++) {
var p = (i + (j * width)) * 4;
data[p] = red;
data[p + 1] = green;
data[p + 2] = blue;
data[p + 3] = 255;
}
var data = this._tile.data;
var width = this._tile.width;
for (var j = y; j < yend; j++) {
for (var i = x; i < xend; i++) {
var p = (i + (j * width)) * 4;
data[p] = red;
data[p + 1] = green;
data[p + 2] = blue;
data[p + 3] = 255;
}
} else {
this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color, true);
}
},
// draw the current tile to the screen
finishTile: function () {
if (this._prefer_js) {
this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y);
this._damage(this._tile_x, this._tile_y,
this._tile.width, this._tile.height);
}
// else: No-op -- already done by setSubTile
this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y);
this._damage(this._tile_x, this._tile_y,
this._tile.width, this._tile.height);
},
blitImage: function (x, y, width, height, arr, offset, from_queue) {
@ -500,11 +506,6 @@ Display.prototype = {
},
changeCursor: function (pixels, mask, hotx, hoty, w, h) {
if (this._cursor_uri === false) {
Log.Warn("changeCursor called but no cursor data URI support");
return;
}
Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
},
@ -516,32 +517,7 @@ Display.prototype = {
this._target.style.cursor = "none";
},
clippingDisplay: function () {
var vp = this._viewportLoc;
return this._fb_width > vp.w || this._fb_height > vp.h;
},
// Overridden getters/setters
set_scale: function (scale) {
this._rescale(scale);
},
set_viewport: function (viewport) {
this._viewport = viewport;
// May need to readjust the viewport dimensions
var vp = this._viewportLoc;
this.viewportChangeSize(vp.w, vp.h);
this.viewportChangePos(0, 0);
},
get_width: function () {
return this._fb_width;
},
get_height: function () {
return this._fb_height;
},
autoscale: function (containerWidth, containerHeight, downscaleOnly) {
autoscale: function (containerWidth, containerHeight) {
var vp = this._viewportLoc;
var targetAspectRatio = containerWidth / containerHeight;
var fbAspectRatio = vp.w / vp.h;
@ -553,14 +529,11 @@ Display.prototype = {
scaleRatio = containerHeight / vp.h;
}
if (scaleRatio > 1.0 && downscaleOnly) {
scaleRatio = 1.0;
}
this._rescale(scaleRatio);
},
// Private Methods
// ===== PRIVATE METHODS =====
_rescale: function (factor) {
this._scale = factor;
var vp = this._viewportLoc;
@ -685,28 +658,11 @@ Display.prototype = {
if (this._renderQ.length === 0 && this._flushing) {
this._flushing = false;
this._onFlush();
this.onflush();
}
},
};
make_properties(Display, [
['target', 'wo', 'dom'], // Canvas element for rendering
['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only)
['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "type": mime-type, "data": data}
['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0
['viewport', 'rw', 'bool'], // Use viewport clipping
['width', 'ro', 'int'], // Display area width
['height', 'ro', 'int'], // Display area height
['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only)
['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods
['cursor_uri', 'rw', 'raw'], // Can we render cursor using data URI
['onFlush', 'rw', 'func'], // onFlush(): A flush request has finished
]);
// Class Methods
Display.changeCursor = function (target, pixels, mask, hotx, hoty, w, h) {
if ((w === 0) || (h === 0)) {

View File

@ -10,7 +10,6 @@
import * as Log from '../util/logging.js';
import { stopEvent } from '../util/events.js';
import { set_defaults, make_properties } from '../util/properties.js';
import * as KeyboardUtil from "./util.js";
import KeyTable from "./keysym.js";
@ -18,15 +17,13 @@ import KeyTable from "./keysym.js";
// Keyboard event handler
//
export default function Keyboard(defaults) {
export default function Keyboard(target) {
this._target = target || null;
this._keyDownList = {}; // List of depressed keys
// (even if they are happy)
this._pendingKey = null; // Key waiting for keypress
set_defaults(this, defaults, {
'target': null,
});
// keep these here so we can refer to them later
this._eventHandlers = {
'keyup': this._handleKeyUp.bind(this),
@ -56,14 +53,14 @@ function isEdge() {
}
Keyboard.prototype = {
// private methods
// ===== EVENT HANDLERS =====
onkeyevent: function () {}, // Handler for key press/release
// ===== PRIVATE METHODS =====
_sendKeyEvent: function (keysym, code, down) {
if (!this._onKeyEvent) {
return;
}
Log.Debug("onKeyEvent " + (down ? "down" : "up") +
Log.Debug("onkeyevent " + (down ? "down" : "up") +
", keysym: " + keysym, ", code: " + code);
// Windows sends CtrlLeft+AltRight when you press
@ -77,19 +74,19 @@ Keyboard.prototype = {
('ControlLeft' in this._keyDownList) &&
('AltRight' in this._keyDownList)) {
fakeAltGraph = true;
this._onKeyEvent(this._keyDownList['AltRight'],
this.onkeyevent(this._keyDownList['AltRight'],
'AltRight', false);
this._onKeyEvent(this._keyDownList['ControlLeft'],
this.onkeyevent(this._keyDownList['ControlLeft'],
'ControlLeft', false);
}
}
this._onKeyEvent(keysym, code, down);
this.onkeyevent(keysym, code, down);
if (fakeAltGraph) {
this._onKeyEvent(this._keyDownList['ControlLeft'],
this.onkeyevent(this._keyDownList['ControlLeft'],
'ControlLeft', true);
this._onKeyEvent(this._keyDownList['AltRight'],
this.onkeyevent(this._keyDownList['AltRight'],
'AltRight', true);
}
},
@ -305,7 +302,7 @@ Keyboard.prototype = {
Log.Debug("<< Keyboard.allKeysUp");
},
// Public methods
// ===== PUBLIC METHODS =====
grab: function () {
//Log.Debug(">> Keyboard.grab");
@ -336,9 +333,3 @@ Keyboard.prototype = {
//Log.Debug(">> Keyboard.ungrab");
},
};
make_properties(Keyboard, [
['target', 'wo', 'dom'], // DOM element that captures keyboard input
['onKeyEvent', 'rw', 'func'] // Handler for key press/release
]);

View File

@ -11,13 +11,13 @@
import * as Log from '../util/logging.js';
import { isTouchDevice } from '../util/browsers.js';
import { setCapture, stopEvent, getPointerEvent } from '../util/events.js';
import { set_defaults, make_properties } from '../util/properties.js';
var WHEEL_STEP = 10; // Delta threshold for a mouse wheel step
var WHEEL_STEP_TIMEOUT = 50; // ms
var WHEEL_LINE_HEIGHT = 19;
export default function Mouse(defaults) {
export default function Mouse(target) {
this._target = target || document;
this._doubleClickTimer = null;
this._lastTouchPos = null;
@ -28,12 +28,6 @@ export default function Mouse(defaults) {
this._accumulatedWheelDeltaX = 0;
this._accumulatedWheelDeltaY = 0;
// Configuration attributes
set_defaults(this, defaults, {
'target': document,
'touchButton': 1
});
this._eventHandlers = {
'mousedown': this._handleMouseDown.bind(this),
'mouseup': this._handleMouseUp.bind(this),
@ -44,7 +38,16 @@ export default function Mouse(defaults) {
};
Mouse.prototype = {
// private methods
// ===== PROPERTIES =====
touchButton: 1, // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
// ===== EVENT HANDLERS =====
onmousebutton: function () {}, // Handler for mouse button click/release
onmousemove: function () {}, // Handler for mouse movement
// ===== PRIVATE METHODS =====
_resetDoubleClickTimer: function () {
this._doubleClickTimer = null;
@ -83,7 +86,7 @@ Mouse.prototype = {
}
this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500);
}
bmask = this._touchButton;
bmask = this.touchButton;
// If bmask is set
} else if (e.which) {
/* everything except IE */
@ -95,11 +98,10 @@ Mouse.prototype = {
(e.button & 0x4) / 2; // Middle
}
if (this._onMouseButton) {
Log.Debug("onMouseButton " + (down ? "down" : "up") +
", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
this._onMouseButton(pos.x, pos.y, down, bmask);
}
Log.Debug("onmousebutton " + (down ? "down" : "up") +
", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
this.onmousebutton(pos.x, pos.y, down, bmask);
stopEvent(e);
},
@ -122,11 +124,11 @@ Mouse.prototype = {
_generateWheelStepX: function () {
if (this._accumulatedWheelDeltaX < 0) {
this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 5);
this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 5);
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 5);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 5);
} else if (this._accumulatedWheelDeltaX > 0) {
this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 6);
this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 6);
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 6);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 6);
}
this._accumulatedWheelDeltaX = 0;
@ -135,11 +137,11 @@ Mouse.prototype = {
_generateWheelStepY: function () {
if (this._accumulatedWheelDeltaY < 0) {
this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 3);
this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 3);
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 3);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 3);
} else if (this._accumulatedWheelDeltaY > 0) {
this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 4);
this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 4);
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 4);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 4);
}
this._accumulatedWheelDeltaY = 0;
@ -153,8 +155,6 @@ Mouse.prototype = {
},
_handleMouseWheel: function (e) {
if (!this._onMouseButton) { return; }
this._resetWheelStepTimers();
this._updateMousePosition(e);
@ -199,9 +199,7 @@ Mouse.prototype = {
_handleMouseMove: function (e) {
this._updateMousePosition(e);
if (this._onMouseMove) {
this._onMouseMove(this._pos.x, this._pos.y);
}
this.onmousemove(this._pos.x, this._pos.y);
stopEvent(e);
},
@ -240,7 +238,8 @@ Mouse.prototype = {
this._pos = {x:x, y:y};
},
// Public methods
// ===== PUBLIC METHODS =====
grab: function () {
var c = this._target;
@ -282,11 +281,3 @@ Mouse.prototype = {
c.removeEventListener('contextmenu', this._eventHandlers.mousedisable);
}
};
make_properties(Mouse, [
['target', 'ro', 'dom'], // DOM element that captures mouse input
['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release
['onMouseMove', 'rw', 'func'], // Handler for mouse movement
['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
]);

View File

@ -13,7 +13,8 @@
import * as Log from './util/logging.js';
import _ from './util/localization.js';
import { decodeUTF8 } from './util/strings.js';
import { set_defaults, make_properties } from './util/properties.js';
import { browserSupportsCursorURIs, isTouchDevice } from './util/browsers.js';
import EventTargetMixin from './util/eventtarget.js';
import Display from "./display.js";
import Keyboard from "./input/keyboard.js";
import Mouse from "./input/mouse.js";
@ -24,50 +25,78 @@ import KeyTable from "./input/keysym.js";
import XtScancode from "./input/xtscancodes.js";
import Inflator from "./inflator.js";
import { encodings, encodingName } from "./encodings.js";
import "./util/polyfill.js";
/*jslint white: false, browser: true */
/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES, KeyTable, Inflator, XtScancode */
export default function RFB(defaults) {
"use strict";
if (!defaults) {
defaults = {};
// How many seconds to wait for a disconnect to finish
var DISCONNECT_TIMEOUT = 3;
export default function RFB(target, url, options) {
if (!target) {
throw Error("Must specify target");
}
if (!url) {
throw Error("Must specify URL");
}
this._rfb_host = '';
this._rfb_port = 5900;
this._rfb_password = '';
this._rfb_path = '';
this._target = target;
this._url = url;
// Connection details
options = options || {}
this._rfb_credentials = options.credentials || {};
this._shared = 'shared' in options ? !!options.shared : true;
this._repeaterID = options.repeaterID || '';
// Internal state
this._rfb_connection_state = '';
this._rfb_init_state = '';
this._rfb_version = 0;
this._rfb_max_version = 3.8;
this._rfb_auth_scheme = '';
this._rfb_disconnect_reason = "";
// Server capabilities
this._rfb_version = 0;
this._rfb_max_version = 3.8;
this._rfb_tightvnc = false;
this._rfb_xvp_ver = 0;
this._encHandlers = {};
this._encStats = {};
this._fb_width = 0;
this._fb_height = 0;
this._sock = null; // Websock object
this._display = null; // Display object
this._flushing = false; // Display flushing state
this._keyboard = null; // Keyboard input handler object
this._mouse = null; // Mouse input handler object
this._disconnTimer = null; // disconnection timer
this._fb_name = "";
this._capabilities = { power: false, resize: false };
this._supportsFence = false;
this._supportsContinuousUpdates = false;
this._enabledContinuousUpdates = false;
// Frame buffer update state
this._supportsSetDesktopSize = false;
this._screen_id = 0;
this._screen_flags = 0;
this._qemuExtKeyEventSupported = false;
// Internal objects
this._sock = null; // Websock object
this._display = null; // Display object
this._flushing = false; // Display flushing state
this._keyboard = null; // Keyboard input handler object
this._mouse = null; // Mouse input handler object
// Timers
this._disconnTimer = null; // disconnection timer
// Decoder states and stats
this._encHandlers = {};
this._encStats = {};
this._FBU = {
rects: 0,
subrects: 0, // RRE
subrects: 0, // RRE and HEXTILE
lines: 0, // RAW
tiles: 0, // HEXTILE
bytes: 0,
@ -78,12 +107,11 @@ export default function RFB(defaults) {
encoding: 0,
subencoding: -1,
background: null,
zlib: [] // TIGHT zlib streams
zlibs: [] // TIGHT zlib streams
};
this._fb_width = 0;
this._fb_height = 0;
this._fb_name = "";
for (var i = 0; i < 4; i++) {
this._FBU.zlibs[i] = new Inflator();
}
this._destBuff = null;
this._paletteBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel)
@ -103,10 +131,6 @@ export default function RFB(defaults) {
pixels: 0
};
this._supportsSetDesktopSize = false;
this._screen_id = 0;
this._screen_flags = 0;
// Mouse state
this._mouse_buttonMask = 0;
this._mouse_arr = [];
@ -114,37 +138,6 @@ export default function RFB(defaults) {
this._viewportDragPos = {};
this._viewportHasMoved = false;
// QEMU Extended Key Event support - default to false
this._qemuExtKeyEventSupported = false;
// set the default value on user-facing properties
set_defaults(this, defaults, {
'target': 'null', // VNC display rendering Canvas object
'encrypt': false, // Use TLS/SSL/wss encryption
'local_cursor': false, // Request locally rendered cursor
'shared': true, // Request shared mode
'view_only': false, // Disable client mouse/keyboard
'focus_on_click': true, // Grab focus on canvas on mouse click
'xvp_password_sep': '@', // Separator for XVP password fields
'disconnectTimeout': 3, // Time (s) to wait for disconnection
'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection
'repeaterID': '', // [UltraVNC] RepeaterID to connect to
'viewportDrag': false, // Move the viewport on mouse drags
// Callback functions
'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate): connection state change
'onNotification': function () { }, // onNotification(rfb, msg, level, options): notification for UI
'onDisconnected': function () { }, // onDisconnected(rfb, reason): disconnection finished
'onPasswordRequired': function () { }, // onPasswordRequired(rfb, msg): VNC password is required
'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received
'onBell': function () { }, // onBell(rfb): RFB Bell message received
'onFBUReceive': function () { }, // onFBUReceive(rfb, rect): RFB FBU rect received but not yet processed
'onFBUComplete': function () { }, // onFBUComplete(rfb): RFB FBU received and processed
'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized
'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received
'onXvpInit': function () { } // onXvpInit(version): XVP extensions active for this connection
});
// Bound event handlers
this._eventHandlers = {
focusCanvas: this._focusCanvas.bind(this),
@ -174,19 +167,20 @@ export default function RFB(defaults) {
// NB: nothing that needs explicit teardown should be done
// before this point, since this can throw an exception
try {
this._display = new Display({target: this._target,
onFlush: this._onFlush.bind(this)});
this._display = new Display(this._target);
} catch (exc) {
Log.Error("Display exception: " + exc);
throw exc;
}
this._display.onflush = this._onFlush.bind(this);
this._display.clear();
this._keyboard = new Keyboard({target: this._target,
onKeyEvent: this._handleKeyEvent.bind(this)});
this._keyboard = new Keyboard(this._target);
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
this._mouse = new Mouse({target: this._target,
onMouseButton: this._handleMouseButton.bind(this),
onMouseMove: this._handleMouseMove.bind(this)});
this._mouse = new Mouse(this._target);
this._mouse.onmousebutton = this._handleMouseButton.bind(this);
this._mouse.onmousemove = this._handleMouseMove.bind(this);
this._sock = new Websock();
this._sock.on('message', this._handle_message.bind(this));
@ -236,33 +230,51 @@ export default function RFB(defaults) {
Log.Warn("WebSocket on-error event");
});
this._init_vars();
this._cleanup();
var rmode = this._display.get_render_mode();
Log.Info("Using native WebSockets, render mode: " + rmode);
// Slight delay of the actual connection so that the caller has
// time to set up callbacks
setTimeout(this._updateConnectionState.bind(this, 'connecting'));
Log.Debug("<< RFB.constructor");
};
RFB.prototype = {
// Public methods
connect: function (host, port, password, path) {
this._rfb_host = host;
this._rfb_port = port;
this._rfb_password = (password !== undefined) ? password : "";
this._rfb_path = (path !== undefined) ? path : "";
// ===== PROPERTIES =====
if (!this._rfb_host) {
return this._fail(
_("Must set host"));
dragViewport: false,
focusOnClick: true,
_viewOnly: false,
get viewOnly() { return this._viewOnly; },
set viewOnly(viewOnly) {
this._viewOnly = viewOnly;
if (this._rfb_connection_state === "connecting" ||
this._rfb_connection_state === "connected") {
if (viewOnly) {
this._keyboard.ungrab();
this._mouse.ungrab();
} else {
this._keyboard.grab();
this._mouse.grab();
}
}
this._rfb_init_state = '';
this._updateConnectionState('connecting');
return true;
},
get capabilities() { return this._capabilities; },
get touchButton() { return this._mouse.touchButton; },
set touchButton(button) { this._mouse.touchButton = button; },
get viewportScale() { return this._display.scale; },
set viewportScale(scale) { this._display.scale = scale; },
get clipViewport() { return this._display.clipViewport; },
set clipViewport(viewport) { this._display.clipViewport = viewport; },
get isClipped() { return this._display.isClipped; },
// ===== PUBLIC METHODS =====
disconnect: function () {
this._updateConnectionState('disconnecting');
this._sock.off('error');
@ -270,13 +282,13 @@ RFB.prototype = {
this._sock.off('open');
},
sendPassword: function (passwd) {
this._rfb_password = passwd;
sendCredentials: function (creds) {
this._rfb_credentials = creds;
setTimeout(this._init_msg.bind(this), 0);
},
sendCtrlAltDel: function () {
if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; }
if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
Log.Info("Sending Ctrl-Alt-Del");
this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
@ -285,38 +297,29 @@ RFB.prototype = {
this.sendKey(KeyTable.XK_Delete, "Delete", false);
this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
return true;
},
xvpOp: function (ver, op) {
if (this._rfb_xvp_ver < ver) { return false; }
Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
this._sock.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op));
return true;
machineShutdown: function () {
this._xvpOp(1, 2);
},
xvpShutdown: function () {
return this.xvpOp(1, 2);
machineReboot: function () {
this._xvpOp(1, 3);
},
xvpReboot: function () {
return this.xvpOp(1, 3);
},
xvpReset: function () {
return this.xvpOp(1, 4);
machineReset: function () {
this._xvpOp(1, 4);
},
// Send a key press. If 'down' is not specified then send a down key
// followed by an up key.
sendKey: function (keysym, code, down) {
if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; }
if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
if (down === undefined) {
this.sendKey(keysym, code, true);
this.sendKey(keysym, code, false);
return true;
return;
}
var scancode = XtScancode[code];
@ -330,63 +333,55 @@ RFB.prototype = {
RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
} else {
if (!keysym) {
return false;
return;
}
Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
}
return true;
},
clipboardPasteFrom: function (text) {
if (this._rfb_connection_state !== 'connected' || this._view_only) { return; }
if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
RFB.messages.clientCutText(this._sock, text);
},
autoscale: function (width, height) {
if (this._rfb_connection_state !== 'connected') { return; }
this._display.autoscale(width, height);
},
viewportChangeSize: function(width, height) {
if (this._rfb_connection_state !== 'connected') { return; }
this._display.viewportChangeSize(width, height);
},
// Requests a change of remote desktop size. This message is an extension
// and may only be sent if we have received an ExtendedDesktopSize message
requestDesktopSize: function (width, height) {
if (this._rfb_connection_state !== 'connected' ||
this._view_only) {
return false;
this._viewOnly) {
return;
}
if (this._supportsSetDesktopSize) {
RFB.messages.setDesktopSize(this._sock, width, height,
this._screen_id, this._screen_flags);
this._sock.flush();
return true;
} else {
return false;
if (!this._supportsSetDesktopSize) {
return;
}
RFB.messages.setDesktopSize(this._sock, width, height,
this._screen_id, this._screen_flags);
},
// Private methods
// ===== PRIVATE METHODS =====
_connect: function () {
Log.Debug(">> RFB.connect");
this._init_vars();
var uri;
if (typeof UsingSocketIO !== 'undefined') {
uri = 'http';
} else {
uri = this._encrypt ? 'wss' : 'ws';
}
uri += '://' + this._rfb_host;
if(this._rfb_port) {
uri += ':' + this._rfb_port;
}
uri += '/' + this._rfb_path;
Log.Info("connecting to " + uri);
Log.Info("connecting to " + this._url);
try {
// WebSocket.onopen transitions to the RFB init states
this._sock.open(uri, this._wsProtocols);
this._sock.open(this._url, ['binary']);
} catch (e) {
if (e.name === 'SyntaxError') {
this._fail("Invalid host or port value given", e);
@ -412,29 +407,6 @@ RFB.prototype = {
Log.Debug("<< RFB.disconnect");
},
_init_vars: function () {
// reset state
this._FBU.rects = 0;
this._FBU.subrects = 0; // RRE and HEXTILE
this._FBU.lines = 0; // RAW
this._FBU.tiles = 0; // HEXTILE
this._FBU.zlibs = []; // TIGHT zlib encoders
this._mouse_buttonMask = 0;
this._mouse_arr = [];
this._rfb_tightvnc = false;
// Clear the per connection encoding stats
var stats = this._encStats;
Object.keys(stats).forEach(function (key) {
stats[key][0] = 0;
});
var i;
for (i = 0; i < 4; i++) {
this._FBU.zlibs[i] = new Inflator();
}
},
_print_stats: function () {
var stats = this._encStats;
@ -454,11 +426,11 @@ RFB.prototype = {
},
_cleanup: function () {
if (!this._view_only) { this._keyboard.ungrab(); }
if (!this._view_only) { this._mouse.ungrab(); }
if (!this._viewOnly) { this._keyboard.ungrab(); }
if (!this._viewOnly) { this._mouse.ungrab(); }
this._display.defaultCursor();
if (Log.get_logging() !== 'debug') {
// Show noVNC logo on load and when disconnected, unless in
// Show noVNC logo when disconnected, unless in
// debug mode
this._display.clear();
}
@ -540,7 +512,8 @@ RFB.prototype = {
// State change actions
this._rfb_connection_state = state;
this._onUpdateState(this, state, oldstate);
var event = new CustomEvent("updatestate", { detail: { state: state } });
this.dispatchEvent(event);
var smsg = "New state '" + state + "', was '" + oldstate + "'.";
Log.Debug(smsg);
@ -556,14 +529,16 @@ RFB.prototype = {
switch (state) {
case 'disconnected':
// Call onDisconnected callback after onUpdateState since
// Fire disconnected event after updatestate event since
// we don't know if the UI only displays the latest message
if (this._rfb_disconnect_reason !== "") {
this._onDisconnected(this, this._rfb_disconnect_reason);
event = new CustomEvent("disconnect",
{ detail: { reason: this._rfb_disconnect_reason } });
} else {
// No reason means clean disconnect
this._onDisconnected(this);
event = new CustomEvent("disconnect", { detail: {} });
}
this.dispatchEvent(event);
break;
case 'connecting':
@ -576,7 +551,7 @@ RFB.prototype = {
this._disconnTimer = setTimeout(function () {
this._rfb_disconnect_reason = _("Disconnect timeout");
this._updateConnectionState('disconnected');
}.bind(this), this._disconnectTimeout * 1000);
}.bind(this), DISCONNECT_TIMEOUT * 1000);
break;
}
},
@ -618,11 +593,10 @@ RFB.prototype = {
* Send a notification to the UI. Valid levels are:
* 'normal'|'warn'|'error'
*
* NOTE: Options could be added in the future.
* NOTE: If this function is called multiple times, remember that the
* interface could be only showing the latest notification.
*/
_notification: function(msg, level, options) {
_notification: function(msg, level) {
switch (level) {
case 'normal':
case 'warn':
@ -634,11 +608,16 @@ RFB.prototype = {
return;
}
if (options) {
this._onNotification(this, msg, level, options);
} else {
this._onNotification(this, msg, level);
}
var event = new CustomEvent("notification",
{ detail: { message: msg, level: level } });
this.dispatchEvent(event);
},
_setCapability: function (cap, val) {
this._capabilities[cap] = val;
var event = new CustomEvent("capabilities",
{ detail: { capabilities: this._capabilities } });
this.dispatchEvent(event);
},
_handle_message: function () {
@ -681,7 +660,7 @@ RFB.prototype = {
this._mouse_buttonMask &= ~bmask;
}
if (this._viewportDrag) {
if (this.dragViewport) {
if (down && !this._viewportDragging) {
this._viewportDragging = true;
this._viewportDragPos = {'x': x, 'y': y};
@ -693,14 +672,14 @@ RFB.prototype = {
// If the viewport didn't actually move, then treat as a mouse click event
// Send the button down event here, as the button up event is sent at the end of this function
if (!this._viewportHasMoved && !this._view_only) {
if (!this._viewportHasMoved && !this._viewOnly) {
RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask);
}
this._viewportHasMoved = false;
}
}
if (this._view_only) { return; } // View only, skip mouse events
if (this._viewOnly) { return; } // View only, skip mouse events
if (this._rfb_connection_state !== 'connected') { return; }
RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
@ -727,7 +706,7 @@ RFB.prototype = {
return;
}
if (this._view_only) { return; } // View only, skip mouse events
if (this._viewOnly) { return; } // View only, skip mouse events
if (this._rfb_connection_state !== 'connected') { return; }
RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
@ -768,7 +747,7 @@ RFB.prototype = {
}
if (is_repeater) {
var repeaterID = this._repeaterID;
var repeaterID = "ID:" + this._repeaterID;
while (repeaterID.length < 250) {
repeaterID += "\0";
}
@ -845,21 +824,20 @@ RFB.prototype = {
// authentication
_negotiate_xvp_auth: function () {
var xvp_sep = this._xvp_password_sep;
var xvp_auth = this._rfb_password.split(xvp_sep);
if (xvp_auth.length < 3) {
var msg = 'XVP credentials required (user' + xvp_sep +
'target' + xvp_sep + 'password) -- got only ' + this._rfb_password;
this._onPasswordRequired(this, msg);
if (!this._rfb_credentials.username ||
!this._rfb_credentials.password ||
!this._rfb_credentials.target) {
var event = new CustomEvent("credentialsrequired",
{ detail: { types: ["username", "password", "target"] } });
this.dispatchEvent(event);
return false;
}
var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) +
String.fromCharCode(xvp_auth[1].length) +
xvp_auth[0] +
xvp_auth[1];
var xvp_auth_str = String.fromCharCode(this._rfb_credentials.username.length) +
String.fromCharCode(this._rfb_credentials.target.length) +
this._rfb_credentials.username +
this._rfb_credentials.target;
this._sock.send_string(xvp_auth_str);
this._rfb_password = xvp_auth.slice(2).join(xvp_sep);
this._rfb_auth_scheme = 2;
return this._negotiate_authentication();
},
@ -867,14 +845,16 @@ RFB.prototype = {
_negotiate_std_vnc_auth: function () {
if (this._sock.rQwait("auth challenge", 16)) { return false; }
if (this._rfb_password.length === 0) {
this._onPasswordRequired(this);
if (!this._rfb_credentials.password) {
var event = new CustomEvent("credentialsrequired",
{ detail: { types: ["password"] } });
this.dispatchEvent(event);
return false;
}
// TODO(directxman12): make genDES not require an Array
var challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
var response = RFB.genDES(this._rfb_password, challenge);
var response = RFB.genDES(this._rfb_credentials.password, challenge);
this._sock.send(response);
this._rfb_init_state = "SecurityResult";
return true;
@ -1105,12 +1085,14 @@ RFB.prototype = {
}
// we're past the point where we could backtrack, so it's safe to call this
this._onDesktopName(this, this._fb_name);
var event = new CustomEvent("desktopname",
{ detail: { name: this._fb_name } });
this.dispatchEvent(event);
this._resize(width, height);
if (!this._view_only) { this._keyboard.grab(); }
if (!this._view_only) { this._mouse.grab(); }
if (!this._viewOnly) { this._keyboard.grab(); }
if (!this._viewOnly) { this._mouse.grab(); }
this._fb_depth = 24;
@ -1160,7 +1142,8 @@ RFB.prototype = {
encs.push(encodings.pseudoEncodingFence);
encs.push(encodings.pseudoEncodingContinuousUpdates);
if (this._local_cursor && this._fb_depth == 24) {
if (browserSupportsCursorURIs() &&
!isTouchDevice && this._fb_depth == 24) {
encs.push(encodings.pseudoEncodingCursor);
}
@ -1219,9 +1202,11 @@ RFB.prototype = {
var text = this._sock.rQshiftStr(length);
if (this._view_only) { return true; }
if (this._viewOnly) { return true; }
this._onClipboard(this, text);
var event = new CustomEvent("clipboard",
{ detail: { text: text } });
this.dispatchEvent(event);
return true;
},
@ -1283,7 +1268,7 @@ RFB.prototype = {
case 1: // XVP_INIT
this._rfb_xvp_ver = xvp_ver;
Log.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")");
this._onXvpInit(this._rfb_xvp_ver);
this._setCapability("power", true);
break;
default:
this._fail("Unexpected server message",
@ -1317,7 +1302,8 @@ RFB.prototype = {
case 2: // Bell
Log.Debug("Bell");
this._onBell(this);
var event = new CustomEvent("bell", { detail: {} });
this.dispatchEvent(event);
return true;
case 3: // ServerCutText
@ -1398,12 +1384,6 @@ RFB.prototype = {
this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
(hdr[10] << 8) + hdr[11], 10);
this._onFBUReceive(this,
{'x': this._FBU.x, 'y': this._FBU.y,
'width': this._FBU.width, 'height': this._FBU.height,
'encoding': this._FBU.encoding,
'encodingName': encodingName(this._FBU.encoding)});
if (!this._encHandlers[this._FBU.encoding]) {
this._fail("Unexpected server message",
"Unsupported encoding " +
@ -1458,8 +1438,6 @@ RFB.prototype = {
this._display.flip();
this._onFBUComplete(this);
return true; // We finished this FBU
},
@ -1477,77 +1455,24 @@ RFB.prototype = {
this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4);
this._display.resize(this._fb_width, this._fb_height);
this._onFBResize(this, this._fb_width, this._fb_height);
var event = new CustomEvent("fbresize",
{ detail: { width: this._fb_width,
height: this._fb_height } });
this.dispatchEvent(event);
this._timing.fbu_rt_start = (new Date()).getTime();
this._updateContinuousUpdates();
}
},
_xvpOp: function (ver, op) {
if (this._rfb_xvp_ver < ver) { return; }
Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
RFB.messages.xvpOp(this._sock, ver, op);
},
};
make_properties(RFB, [
['target', 'wo', 'dom'], // VNC display rendering Canvas object
['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption
['local_cursor', 'rw', 'bool'], // Request locally rendered cursor
['shared', 'rw', 'bool'], // Request shared mode
['view_only', 'rw', 'bool'], // Disable client mouse/keyboard
['focus_on_click', 'rw', 'bool'], // Grab focus on canvas on mouse click
['xvp_password_sep', 'rw', 'str'], // Separator for XVP password fields
['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection
['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection
['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to
['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags
// Callback functions
['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate): connection state change
['onNotification', 'rw', 'func'], // onNotification(rfb, msg, level, options): notification for the UI
['onDisconnected', 'rw', 'func'], // onDisconnected(rfb, reason): disconnection finished
['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb, msg): VNC password is required
['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received
['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received
['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed
['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed
['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized
['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received
['onXvpInit', 'rw', 'func'] // onXvpInit(version): XVP extensions active for this connection
]);
RFB.prototype.set_local_cursor = function (cursor) {
if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) {
this._local_cursor = false;
this._display.disableLocalCursor(); //Only show server-side cursor
} else {
if (this._display.get_cursor_uri()) {
this._local_cursor = true;
} else {
Log.Warn("Browser does not support local cursor");
this._display.disableLocalCursor();
}
}
// Need to send an updated list of encodings if we are connected
if (this._rfb_connection_state === "connected") {
this._sendEncodings();
}
};
RFB.prototype.set_view_only = function (view_only) {
this._view_only = view_only;
if (this._rfb_connection_state === "connecting" ||
this._rfb_connection_state === "connected") {
if (view_only) {
this._keyboard.ungrab();
this._mouse.ungrab();
} else {
this._keyboard.grab();
this._mouse.grab();
}
}
};
RFB.prototype.get_display = function () { return this._display; };
RFB.prototype.get_keyboard = function () { return this._keyboard; };
RFB.prototype.get_mouse = function () { return this._mouse; };
Object.assign(RFB.prototype, EventTargetMixin);
// Class Methods
RFB.messages = {
@ -1830,7 +1755,21 @@ RFB.messages = {
sock._sQlen += 10;
sock.flush();
}
},
xvpOp: function (sock, ver, op) {
var buff = sock._sQ;
var offset = sock._sQlen;
buff[offset] = 250; // msg-type
buff[offset + 1] = 0; // padding
buff[offset + 2] = ver;
buff[offset + 3] = op;
sock._sQlen += 4;
sock.flush();
},
};
RFB.genDES = function (password, challenge) {
@ -2361,6 +2300,8 @@ RFB.encodingHandlers = {
if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; }
this._supportsSetDesktopSize = true;
this._setCapability("resize", true);
var number_of_screens = this._sock.rQpeek8();
this._FBU.bytes = 4 + (number_of_screens * 16);

View File

@ -43,11 +43,3 @@ export function browserSupportsCursorURIs () {
return _cursor_uris_supported;
};
export function _forceCursorURIs(enabled) {
if (enabled === undefined || enabled) {
_cursor_uris_supported = true;
} else {
_cursor_uris_supported = false;
}
}

40
core/util/eventtarget.js Normal file
View File

@ -0,0 +1,40 @@
/*
* noVNC: HTML5 VNC client
* Copyright 2017 Pierre Ossman for Cendio AB
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
var EventTargetMixin = {
_listeners: null,
addEventListener: function(type, callback) {
if (!this._listeners) {
this._listeners = new Map();
}
if (!this._listeners.has(type)) {
this._listeners.set(type, new Set());
}
this._listeners.get(type).add(callback);
},
removeEventListener: function(type, callback) {
if (!this._listeners || !this._listeners.has(type)) {
return;
}
this._listeners.get(type).delete(callback);
},
dispatchEvent: function(event) {
if (!this._listeners || !this._listeners.has(event.type)) {
return true;
}
this._listeners.get(event.type).forEach(function (callback) {
callback.call(this, event);
}, this);
return !event.defaultPrevented;
},
};
export default EventTargetMixin;

54
core/util/polyfill.js Normal file
View File

@ -0,0 +1,54 @@
/*
* noVNC: HTML5 VNC client
* Copyright 2017 Pierre Ossman for noVNC
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
/* Polyfills to provide new APIs in old browsers */
/* Object.assign() (taken from MDN) */
if (typeof Object.assign != 'function') {
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, "assign", {
value: function assign(target, varArgs) { // .length of function is 2
'use strict';
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) { // Skip over if undefined or null
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
/* CustomEvent constructor (taken from MDN) */
(function () {
function CustomEvent ( event, params ) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent( 'CustomEvent' );
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
}
CustomEvent.prototype = window.Event.prototype;
if (typeof window.CustomEvent !== "function") {
window.CustomEvent = CustomEvent;
}
})();

View File

@ -1,138 +0,0 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
/*
* Getter/Setter Creation Utilities
*/
import * as Log from './logging.js';
function make_property (proto, name, mode, type) {
"use strict";
var getter;
if (type === 'arr') {
getter = function (idx) {
if (typeof idx !== 'undefined') {
return this['_' + name][idx];
} else {
return this['_' + name];
}
};
} else {
getter = function () {
return this['_' + name];
};
}
var make_setter = function (process_val) {
if (process_val) {
return function (val, idx) {
if (typeof idx !== 'undefined') {
this['_' + name][idx] = process_val(val);
} else {
this['_' + name] = process_val(val);
}
};
} else {
return function (val, idx) {
if (typeof idx !== 'undefined') {
this['_' + name][idx] = val;
} else {
this['_' + name] = val;
}
};
}
};
var setter;
if (type === 'bool') {
setter = make_setter(function (val) {
if (!val || (val in {'0': 1, 'no': 1, 'false': 1})) {
return false;
} else {
return true;
}
});
} else if (type === 'int') {
setter = make_setter(function (val) { return parseInt(val, 10); });
} else if (type === 'float') {
setter = make_setter(parseFloat);
} else if (type === 'str') {
setter = make_setter(String);
} else if (type === 'func') {
setter = make_setter(function (val) {
if (!val) {
return function () {};
} else {
return val;
}
});
} else if (type === 'arr' || type === 'dom' || type == 'raw') {
setter = make_setter();
} else {
throw new Error('Unknown property type ' + type); // some sanity checking
}
// set the getter
if (typeof proto['get_' + name] === 'undefined') {
proto['get_' + name] = getter;
}
// set the setter if needed
if (typeof proto['set_' + name] === 'undefined') {
if (mode === 'rw') {
proto['set_' + name] = setter;
} else if (mode === 'wo') {
proto['set_' + name] = function (val, idx) {
if (typeof this['_' + name] !== 'undefined') {
throw new Error(name + " can only be set once");
}
setter.call(this, val, idx);
};
}
}
// make a special setter that we can use in set defaults
proto['_raw_set_' + name] = function (val, idx) {
setter.call(this, val, idx);
//delete this['_init_set_' + name]; // remove it after use
};
};
export function make_properties (constructor, arr) {
"use strict";
for (var i = 0; i < arr.length; i++) {
make_property(constructor.prototype, arr[i][0], arr[i][1], arr[i][2]);
}
};
export function set_defaults (obj, conf, defaults) {
var defaults_keys = Object.keys(defaults);
var conf_keys = Object.keys(conf);
var keys_obj = {};
var i;
for (i = 0; i < defaults_keys.length; i++) { keys_obj[defaults_keys[i]] = 1; }
for (i = 0; i < conf_keys.length; i++) { keys_obj[conf_keys[i]] = 1; }
var keys = Object.keys(keys_obj);
for (i = 0; i < keys.length; i++) {
var setter = obj['_raw_set_' + keys[i]];
if (!setter) {
Log.Warn('Invalid property ' + keys[i]);
continue;
}
if (keys[i] in conf) {
setter.call(obj, conf[keys[i]]);
} else {
setter.call(obj, defaults[keys[i]]);
}
}
};

126
docs/API-internal.md Normal file
View File

@ -0,0 +1,126 @@
# 1. Internal Modules
The noVNC client is composed of several internal modules that handle
rendering, input, networking, etc. Each of the modules is designed to
be cross-browser and independent from each other.
Note however that the API of these modules is not guaranteed to be
stable, and this documentation is not maintained as well as the
official external API.
## 1.1 Module List
* __Mouse__ (core/input/mouse.js): Mouse input event handler with
limited touch support.
* __Keyboard__ (core/input/keyboard.js): Keyboard input event handler with
non-US keyboard support. Translates keyDown and keyUp events to X11
keysym values.
* __Display__ (core/display.js): Efficient 2D rendering abstraction
layered on the HTML5 canvas element.
* __Websock__ (core/websock.js): Websock client from websockify
with transparent binary data support.
[Websock API](https://github.com/novnc/websockify/wiki/websock.js) wiki page.
## 1.2 Callbacks
For the Mouse, Keyboard and Display objects the callback functions are
assigned to configuration attributes, just as for the RFB object. The
WebSock module has a method named 'on' that takes two parameters: the
callback event name, and the callback function.
## 2. Modules
## 2.1 Mouse Module
### 2.1.1 Configuration Attributes
| name | type | mode | default | description
| ----------- | ---- | ---- | -------- | ------------
| touchButton | int | RW | 1 | Button mask (1, 2, 4) for which click to send on touch devices. 0 means ignore clicks.
### 2.1.2 Methods
| name | parameters | description
| ------ | ---------- | ------------
| grab | () | Begin capturing mouse events
| ungrab | () | Stop capturing mouse events
### 2.1.2 Callbacks
| name | parameters | description
| ------------- | ------------------- | ------------
| onmousebutton | (x, y, down, bmask) | Handler for mouse button click/release
| onmousemove | (x, y) | Handler for mouse movement
## 2.2 Keyboard Module
### 2.2.1 Configuration Attributes
None
### 2.2.2 Methods
| name | parameters | description
| ------ | ---------- | ------------
| grab | () | Begin capturing keyboard events
| ungrab | () | Stop capturing keyboard events
### 2.2.3 Callbacks
| name | parameters | description
| ---------- | -------------------- | ------------
| onkeypress | (keysym, code, down) | Handler for key press/release
## 2.3 Display Module
### 2.3.1 Configuration Attributes
| name | type | mode | default | description
| ------------ | ----- | ---- | ------- | ------------
| logo | raw | RW | | Logo to display when cleared: {"width": width, "height": height, "type": mime-type, "data": data}
| scale | float | RW | 1.0 | Display area scale factor 0.0 - 1.0
| clipViewport | bool | RW | false | Use viewport clipping
| width | int | RO | | Display area width
| height | int | RO | | Display area height
| isClipped | bool | RO | | Is the remote display is larger than the client display
### 2.3.2 Methods
| name | parameters | description
| ------------------ | ------------------------------------------------------- | ------------
| viewportChangePos | (deltaX, deltaY) | Move the viewport relative to the current location
| viewportChangeSize | (width, height) | Change size of the viewport
| absX | (x) | Return X relative to the remote display
| absY | (y) | Return Y relative to the remote display
| resize | (width, height) | Set width and height
| flip | (from_queue) | Update the visible canvas with the contents of the rendering canvas
| clear | () | Clear the display (show logo if set)
| pending | () | Check if there are waiting items in the render queue
| flush | () | Resume processing the render queue unless it's empty
| fillRect | (x, y, width, height, color, from_queue) | Draw a filled in rectangle
| copyImage | (old_x, old_y, new_x, new_y, width, height, from_queue) | Copy a rectangular area
| imageRect | (x, y, mime, arr) | Draw a rectangle with an image
| startTile | (x, y, width, height, color) | Begin updating a tile
| subTile | (tile, x, y, w, h, color) | Update a sub-rectangle within the given tile
| finishTile | () | Draw the current tile to the display
| blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display
| blitRgbImage | (x, y, width, height, arr, offset, from_queue) | Blit RGB encoded image to display
| blitRgbxImage | (x, y, width, height, arr, offset, from_queue) | Blit RGBX encoded image to display
| drawImage | (img, x, y) | Draw image and track damage
| changeCursor | (pixels, mask, hotx, hoty, w, h) | Change cursor appearance
| defaultCursor | () | Restore default cursor appearance
| disableLocalCursor | () | Disable local (client-side) cursor
| autoscale | (containerWidth, containerHeight) | Scale the display
### 2.3.3 Callbacks
| name | parameters | description
| ------- | ---------- | ------------
| onflush | () | A display flush has been requested and we are now ready to resume FBU processing

View File

@ -1,255 +1,424 @@
# 1. Modules / API
# noVNC API
The noVNC client is a composed of several modular components that handle
rendering, input, networking, etc. Each of the modules is designed to
be cross-browser and be useful as a standalone library in other
projects (see LICENSE.txt).
The interface of the noVNC client consists of a single RFB object that
is instantiated once per connection.
## RFB
## 1.1 Module List
The `RFB` object represents a single connection to a VNC server. It
communicates using a WebSocket that must provide a standard RFB
protocol stream.
* __Mouse__ (core/input/mouse.js): Mouse input event handler with
limited touch support.
### Constructor
* __Keyboard__ (core/input/keyboard.js): Keyboard input event handler with
non-US keyboard support. Translates keyDown and keyUp events to X11
keysym values.
[`RFB()`](#rfb-1)
- Creates and returns a new `RFB` object.
* __Display__ (core/display.js): Efficient 2D rendering abstraction
layered on the HTML5 canvas element.
### Properties
* __Websock__ (core/websock.js): Websock client from websockify
with transparent binary data support.
[Websock API](https://github.com/novnc/websockify/wiki/websock.js) wiki page.
`viewOnly`
- Is a `boolean` indicating if any events (e.g. key presses or mouse
movement) should be prevented from being sent to the server.
Disabled by default.
* __RFB__ (core/rfb.js): Main class that implements the RFB
protocol and stitches the other classes together.
`focusOnClick`
- Is a `boolean` indicating if keyboard focus should automatically be
moved to the canvas when a `mousedown` or `touchstart` event is
received.
`touchButton`
- Is a `long` controlling the button mask that should be simulated
when a touch event is recieved. Uses the same values as
[`MouseEvent.button`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button).
Is set to `1` by default.
## 1.2 Configuration Attributes
`viewportScale`
- Is a `double` indicating how the framebuffer contents should be
scaled before being rendered on to the canvas. See also
[`RFB.autoscale()`](#rfbautoscale). Is set to `1.0` by default.
The Mouse, Keyboard, Display and RFB classes have a similar API for
configuration options. Each configuration option has a default value,
which can be overridden by a a configuration object passed to the
constructor. Configuration options can then be read and modified after
initialization with "get_*" and "set_*" methods respectively. For
example, the following initializes an RFB object with the 'encrypt'
configuration option enabled, then confirms it was set, then disables
it.
`clipViewport`
- Is a `boolean` indicating if the canvas should be clipped to its
container. When disabled the container must be able to handle the
resulting overflow. Disabled by default.
var rfb = new RFB({'encrypt': true});
if (rfb.get_encrypt()) {
alert("Encryption is set");
}
rfb.set_encrypt(false);
`dragViewport`
- Is a `boolean` indicating if mouse events should control the
relative position of a clipped canvas. Only relevant if
`clipViewport` is enabled. Disabled by default.
Some attributes are read-only and cannot be changed. For example, the
Display 'render_mode' option will throw an exception if an attempt is
made to set it. The attribute mode is one of the following:
`isClipped` *Read only*
- Is a `boolean` indicating if the framebuffer is larger than the
current canvas, i.e. it is being clipped.
RO - read only
RW - read write
WO - write once
`capabilities` *Read only*
- Is an `Object` indicating which optional extensions are available
on the server. Some methods may only be called if the corresponding
capability is set. The following capabilities are defined:
| name | type | description
| -------- | --------- | -----------
| `power` | `boolean` | Machine power control is available
| `resize` | `boolean` | The framebuffer can be resized
## 1.3 Methods
### Events
In addition to the getter and setter methods to modify configuration
attributes, each of the modules has other methods that are available
in the object instance. For example, the Display module has method
named 'blitImage' which takes an array of pixel data and draws it to
the 2D canvas.
[`updatestate`](#updatestate)
- The `updatestate` event is fired when the connection state of the
`RFB` object changes.
## 1.4 Callbacks
[`notification`](#notification)
- The `notification` event is fired when the `RFB` usage has a
message to display to the user.
Each of the modules has certain events that can be hooked with
callback functions. For the Mouse, Keyboard, Display and RFB classes
the callback functions are assigned to configuration attributes. The
WebSock module has a method named 'on' that takes two parameters: the
callback event name, and the callback function.
[`disconnect`](#disconnected)
- The `disconnect` event is fired when the `RFB` object disconnects.
## 2. Modules
[`credentialsrequired`](#credentialsrequired)
- The `credentialsrequired` event is fired when more credentials must
be given to continue.
## 2.1 Mouse Module
[`clipboard`](#clipboard)
- The `clipboard` event is fired when clipboard data is received from
the server.
### 2.1.1 Configuration Attributes
[`bell`](#bell)
- The `bell` event is fired when a audible bell request is received
from the server.
| name | type | mode | default | description
| ----------- | ---- | ---- | -------- | ------------
| target | DOM | WO | document | DOM element that captures mouse input
| touchButton | int | RW | 1 | Button mask (1, 2, 4) for which click to send on touch devices. 0 means ignore clicks.
[`fbresize`](#fbresize)
- The `fbresize` event is fired when the framebuffer size is changed.
### 2.1.2 Methods
[`desktopname`](#desktopname)
- The `desktopname` event is fired when the remote desktop name
changes.
| name | parameters | description
| ------ | ---------- | ------------
| grab | () | Begin capturing mouse events
| ungrab | () | Stop capturing mouse events
[`capabilities`](#capabilities)
- The `capabilities` event is fired when `RFB.capabilities` is
updated.
### 2.1.2 Callbacks
### Methods
| name | parameters | description
| ------------- | ------------------- | ------------
| onMouseButton | (x, y, down, bmask) | Handler for mouse button click/release
| onMouseMove | (x, y) | Handler for mouse movement
[`RFB.disconnect()`](#rfbdisconnect)
- Disconnect from the server.
[`RFB.sendCredentials()`](#rfbsendcredentials)
- Send credentials to server. Should be called after the
[`credentialsrequired`](#credentialsrequired) event has fired.
## 2.2 Keyboard Module
[`RFB.sendKey()`](#rfbsendKey)
- Send a key event.
### 2.2.1 Configuration Attributes
[`RFB.sendCtrlAltDel()`](#rfbsendctrlaltdel)
- Send Ctrl-Alt-Del key sequence.
| name | type | mode | default | description
| ------- | ---- | ---- | -------- | ------------
| target | DOM | WO | document | DOM element that captures keyboard input
[`RFB.machineShutdown()`](#rfbmachineshutdown)
- Request a shutdown of the remote machine.
### 2.2.2 Methods
[`RFB.machineReboot()`](#rfbmachinereboot)
- Request a reboot of the remote machine.
| name | parameters | description
| ------ | ---------- | ------------
| grab | () | Begin capturing keyboard events
| ungrab | () | Stop capturing keyboard events
[`RFB.machineReset()`](#rfbmachinereset)
- Request a reset of the remote machine.
### 2.2.3 Callbacks
[`RFB.clipboardPasteFrom()`](#rfbclipboardPasteFrom)
- Send clipboard contents to server.
| name | parameters | description
| ---------- | -------------------- | ------------
| onKeyPress | (keysym, code, down) | Handler for key press/release
[`RFB.autoscale()`](#rfbautoscale)
- Set `RFB.viewportScale` so that the framebuffer fits a specified
container.
[`RFB.requestDesktopSize()`](#rfbrequestDesktopSize)
- Send a request to change the remote desktop size.
## 2.3 Display Module
[`RFB.viewportChangeSize()`](#rfbviewportChangeSize)
- Change size of the viewport.
### 2.3.1 Configuration Attributes
### Details
| name | type | mode | default | description
| ----------- | ----- | ---- | ------- | ------------
| target | DOM | WO | | Canvas element for rendering
| context | raw | RO | | Canvas 2D context for rendering
| logo | raw | RW | | Logo to display when cleared: {"width": width, "height": height, "type": mime-type, "data": data}
| scale | float | RW | 1.0 | Display area scale factor 0.0 - 1.0
| viewport | bool | RW | false | Use viewport clipping
| width | int | RO | | Display area width
| height | int | RO | | Display area height
| render_mode | str | RO | '' | Canvas rendering mode
| prefer_js | str | RW | | Prefer JavaScript over canvas methods
| cursor_uri | raw | RW | | Can we render cursor using data URI
#### RFB()
### 2.3.2 Methods
The `RFB()` constructor returns a new `RFB` object and initiates a new
connection to a specified VNC server.
| name | parameters | description
| ------------------ | ------------------------------------------------------- | ------------
| viewportChangePos | (deltaX, deltaY) | Move the viewport relative to the current location
| viewportChangeSize | (width, height) | Change size of the viewport
| absX | (x) | Return X relative to the remote display
| absY | (y) | Return Y relative to the remote display
| resize | (width, height) | Set width and height
| flip | (from_queue) | Update the visible canvas with the contents of the rendering canvas
| clear | () | Clear the display (show logo if set)
| pending | () | Check if there are waiting items in the render queue
| flush | () | Resume processing the render queue unless it's empty
| fillRect | (x, y, width, height, color, from_queue) | Draw a filled in rectangle
| copyImage | (old_x, old_y, new_x, new_y, width, height, from_queue) | Copy a rectangular area
| imageRect | (x, y, mime, arr) | Draw a rectangle with an image
| startTile | (x, y, width, height, color) | Begin updating a tile
| subTile | (tile, x, y, w, h, color) | Update a sub-rectangle within the given tile
| finishTile | () | Draw the current tile to the display
| blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display
| blitRgbImage | (x, y, width, height, arr, offset, from_queue) | Blit RGB encoded image to display
| blitRgbxImage | (x, y, width, height, arr, offset, from_queue) | Blit RGBX encoded image to display
| drawImage | (img, x, y) | Draw image and track damage
| changeCursor | (pixels, mask, hotx, hoty, w, h) | Change cursor appearance
| defaultCursor | () | Restore default cursor appearance
| disableLocalCursor | () | Disable local (client-side) cursor
| clippingDisplay | () | Check if the remote display is larger than the client display
| autoscale | (containerWidth, containerHeight, downscaleOnly) | Scale the display
##### Syntax
### 2.3.3 Callbacks
var rfb = new RFB( target, url [, options] );
| name | parameters | description
| ------- | ---------- | ------------
| onFlush | () | A display flush has been requested and we are now ready to resume FBU processing
###### Parameters
**`target`**
- A [`HTMLCanvasElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement)
that specifies where graphics should be rendered and input events
should be monitored.
## 2.4 RFB Module
**`url`**
- A `DOMString` specifying the VNC server to connect to. This must be
a valid WebSocket URL.
### 2.4.1 Configuration Attributes
**`options`** *Optional*
- An `Object` specifying extra details about how the connection
should be made.
| name | type | mode | default | description
| ----------------- | ---- | ---- | ---------- | ------------
| target | DOM | WO | null | Canvas element for rendering (passed to Display, Mouse and Keyboard)
| encrypt | bool | RW | false | Use TLS/SSL encryption
| local_cursor | bool | RW | false | Request locally rendered cursor
| shared | bool | RW | true | Request shared VNC mode
| view_only | bool | RW | false | Disable client mouse/keyboard
| focus_on_click | bool | RW | true | Grab focus on canvas on mouse click
| xvp_password_sep | str | RW | '@' | Separator for XVP password fields
| disconnectTimeout | int | RW | 3 | Time (in seconds) to wait for disconnection
| wsProtocols | arr | RW | ['binary'] | Protocols to use in the WebSocket connection
| repeaterID | str | RW | '' | UltraVNC RepeaterID to connect to
| viewportDrag | bool | RW | false | Move the viewport on mouse drags
Possible options:
### 2.4.2 Methods
`shared`
- A `boolean` indicating if the remote server should be shared or
if any other connected clients should be disconnected. Enabled
by default.
| name | parameters | description
| ------------------ | ---------------------------- | ------------
| connect | (host, port, password, path) | Connect to the given host:port/path. Optional password and path.
| disconnect | () | Disconnect
| sendPassword | (passwd) | Send password after onPasswordRequired callback
| sendCtrlAltDel | () | Send Ctrl-Alt-Del key sequence
| xvpOp | (ver, op) | Send a XVP operation (2=shutdown, 3=reboot, 4=reset)
| xvpShutdown | () | Send XVP shutdown.
| xvpReboot | () | Send XVP reboot.
| xvpReset | () | Send XVP reset.
| sendKey | (keysym, code, down) | Send a key press event. If down not specified, send a down and up event.
| clipboardPasteFrom | (text) | Send a clipboard paste event
| requestDesktopSize | (width, height) | Send a request to change the remote desktop size.
`credentials`
- An `Object` specifying the credentials to provide to the server
when authenticating. The following credentials are possible:
### 2.4.3 Callbacks
| name | type | description
| ------------ | ----------- | -----------
| `"username"` | `DOMString` | The user that authenticates
| `"password"` | `DOMString` | Password for the user
| `"target"` | `DOMString` | Target machine or session
| name | parameters | description
| ------------------ | -------------------------- | ------------
| onUpdateState | (rfb, state, oldstate) | Connection state change (see details below)
| onNotification | (rfb, msg, level, options) | Notification for the UI (optional options)
| onDisconnected | (rfb, reason) | Disconnection finished with an optional reason. No reason specified means normal disconnect.
| onPasswordRequired | (rfb, msg) | VNC password is required (use sendPassword), optionally comes with a message.
| onClipboard | (rfb, text) | RFB clipboard contents received
| onBell | (rfb) | RFB Bell message received
| onFBUReceive | (rfb, fbu) | RFB FBU received but not yet processed (see details below)
| onFBUComplete | (rfb, fbu) | RFB FBU received and processed (see details below)
| onFBResize | (rfb, width, height) | Frame buffer (remote desktop) size changed
| onDesktopName | (rfb, name) | VNC desktop name recieved
| onXvpInit | (version) | XVP extensions active for this connection.
`repeaterID`
- A `DOMString` specifying the ID to provide to any VNC repeater
encountered.
#### updatestate
__RFB onUpdateState callback details__
The `updatestate` event is fired after the noVNC connection state
changes. The `detail` property is an `Object` containg the property
`state` with the new connection state.
The RFB module has an 'onUpdateState' callback that is invoked after
the noVNC connection state changes. Here is a list of the states that
are reported. Note that the RFB module can not transition from the
disconnected state in any way, a new instance of the object has to be
created for new connections.
Here is a list of the states that are reported:
| connection state | description
| ---------------- | ------------
| connecting | starting to connect
| connected | connected normally
| disconnecting | starting to disconnect
| disconnected | disconnected - permanent end-state for this RFB object
| connection state | description
| ----------------- | ------------
| `"connecting"` | starting to connect
| `"connected"` | connected normally
| `"disconnecting"` | starting to disconnect
| `"disconnected"` | disconnected
__RFB onFBUReceive and on FBUComplete callback details__
Note that a `RFB` objects can not transition from the disconnected
state in any way, a new instance of the object has to be created for
new connections.
The onFBUReceive callback is invoked when a frame buffer update
message has been received from the server but before the RFB class has
done any additional handling. The onFBUComplete callback is invoked
with the same information but after the RFB class has handled the
message.
#### notification
The 'fbu' parameter is an object with the following structure:
The `notification` event is fired when the `RFB` object wants a message
displayed to the user. The `detail` property is an `Object` containing
the following properties:
{
x: FBU_x_position,
y: FBU_y_position,
width: FBU_width,
height: FBU_height,
encoding: FBU_encoding_number,
encodingName: FBU_encoding_string
}
| Property | Type | Description
| --------- | ----------- | -----------
| `message` | `DOMString` | The message to display
| `level` | `DOMString` | The severity of the message
The following levels are currently defined:
- `"normal"`
- `"warn"`
- `"error"`
#### disconnect
The `disconnect` event is fired when the connection has been
terminated. The `detail` property is an `Object` the optionally
contains the property `reason`. `reason` is a `DOMString` specifying
the reason in the event of an unexpected termination. `reason` will be
omitted for a clean termination.
#### credentialsrequired
The `credentialsrequired` event is fired when the server requests more
credentials than were specified to [`RFB()`](#rfb-1). The `detail`
property is an `Object` containing the property `types` which is an
`Array` of `DOMString` listing the credentials that are required.
#### clipboard
The `clipboard` event is fired when the server has sent clipboard data.
The `detail` property is an `Object` containing the property `text`
which is a `DOMString` with the clipboard data.
#### bell
The `bell` event is fired when the server has requested an audible
bell.
#### fbresize
The `fbresize` event is fired when the framebuffer has changed
dimensions. The `detail` property is an `Object` with the properties
`width` and `height` specifying the new dimensions.
#### desktopname
The `desktopname` event is fired when the name of the remote desktop
changes. The `detail` property is an `Object` with the property `name`
which is a `DOMString` specifying the new name.
#### capabilities
The `capabilities` event is fired whenever an entry is added or removed
from `RFB.capabilities`. The `detail` property is an `Object` with the
property `capabilities` containing the new value of `RFB.capabilities`.
#### RFB.disconnect()
The `RFB.disconnect()` method is used to disconnect from the currently
connected server.
##### Syntax
RFB.disconnect( );
#### RFB.sendCredentials()
The `RFB.sendCredentials()` method is used to provide the missing
credentials after a `credentialsrequired` event has been fired.
##### Syntax
RFB.sendCredentials( credentials );
###### Parameters
**`credentials`**
- An `Object` specifying the credentials to provide to the server
when authenticating. See [`RFB()`](#rfb-1) for details.
#### RFB.sendKey()
The `RFB.sendKey()` method is used to send a key event to the server.
##### Syntax
RFB.sendKey( keysym, code [, down] );
###### Parameters
**`keysym`**
- A `long` specifying the RFB keysym to send. Can be `0` if a valid
**`code`** is specified.
**`code`**
- A `DOMString` specifying the physical key to send. Valid values are
those that can be specified to
[`KeyboardEvent.code`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code).
If the physical key cannot be determined then `null` shall be
specified.
**`down`** *Optional*
- A `boolean` specifying if a press or a release event should be
sent. If omitted then both a press and release event are sent.
#### RFB.sendCtrlAltDel()
The `RFB.sendCtrlAltDel()` method is used to send the key sequence
*left Control*, *left Alt*, *Delete*. This is a convenience wrapper
around [`RFB.sendKey()`](#rfbsendkey).
##### Syntax
RFB.sendCtrlAltDel( );
#### RFB.machineShutdown()
The `RFB.machineShutdown()` method is used to request to shut down the
remote machine. The capability `power` must be set for this method to
have any effect.
##### Syntax
RFB.machineShutdown( );
#### RFB.machineReboot()
The `RFB.machineReboot()` method is used to request a clean reboot of
the remote machine. The capability `power` must be set for this method
to have any effect.
##### Syntax
RFB.machineReboot( );
#### RFB.machineReset()
The `RFB.machineReset()` method is used to request a forced reset of
the remote machine. The capability `power` must be set for this method
to have any effect.
##### Syntax
RFB.machineReset( );
#### RFB.clipboardPasteFrom()
The `RFB.clipboardPasteFrom()` method is used to send clipboard data
to the remote server.
##### Syntax
RFB.clipboardPasteFrom( text );
###### Parameters
**`text`**
- A `DOMString` specifying the clipboard data to send. Currently only
characters from ISO 8859-1 are supported.
#### RFB.autoscale()
The `RFB.autoscale()` method is used to automatically adjust
`RFB.viewportScale` to fit given dimensions.
##### Syntax
RFB.autoscale( width, height );
###### Parameters
**`width`**
- A `long` specifying the maximum width of the canvas in CSS pixels.
**`height`**
- A `long` specifying the maximum height of the canvas in CSS pixels.
#### RFB.requestDesktopSize()
The `RFB.requestDesktopSize()` method is used to request a change of
the framebuffer. The capability `resize` must be set for this method to
have any effect.
Note that this is merely a request and the server may deny it.
The [`fbresize`](#fbresize) event will be fired when the framebuffer
actually changes dimensions.
##### Syntax
RFB.requestDesktopSize( width, height );
###### Parameters
**`width`**
- A `long` specifying the new requested width in CSS pixels.
**`height`**
- A `long` specifying the new requested height in CSS pixels.
#### RFB.viewportChangeSize()
The `RFB.viewportChangeSize()` method is used to change the size of the
canvas rather than the underlying framebuffer.
This method has no effect if `RFB.clipViewport` is set to `false`.
##### Syntax
RFB.viewportChangeSize( width, height );
###### Parameters
**`width`**
- A `long` specifying the new width in CSS pixels.
**`height`**
- A `long` specifying the new height in CSS pixels.

View File

@ -77,10 +77,10 @@ export default function RecordingPlayer (frames, encoding, disconnected, notific
RecordingPlayer.prototype = {
run: function (realtime, trafficManagement) {
// initialize a new RFB
this._rfb = new RFB({'target': document.getElementById('VNC_canvas'),
'view_only': true,
'onDisconnected': this._handleDisconnect.bind(this),
'onNotification': this._notification});
this._rfb = new RFB(document.getElementById('VNC_canvas'), 'wss://test');
this._rfb.viewOnly = true;
this._rfb.ondisconnected = this._handleDisconnect.bind(this);
this._rfb.onnotification = this._notification;
this._enablePlaybackMode();
// reset the frame index and timer
@ -92,9 +92,6 @@ RecordingPlayer.prototype = {
this._running = true;
// launch the tests
this._rfb.connect('test', 0, 'bogus');
this._queueNextPacket();
},
@ -104,14 +101,8 @@ RecordingPlayer.prototype = {
this._rfb._sock.close = function () {};
this._rfb._sock.flush = function () {};
this._rfb._checkEvents = function () {};
this._rfb.connect = function (host, port, password, path) {
this._rfb_host = host;
this._rfb_port = port;
this._rfb_password = (password !== undefined) ? password : "";
this._rfb_path = (path !== undefined) ? path : "";
this._rfb._connect = function () {
this._sock.init('binary', 'ws');
this._rfb_connection_state = 'connecting';
this._rfb_init_state = 'ProtocolVersion';
};
},
@ -154,12 +145,12 @@ RecordingPlayer.prototype = {
// Avoid having excessive queue buildup in non-realtime mode
if (this._trafficManagement && this._rfb._flushing) {
let player = this;
let orig = this._rfb._display.get_onFlush();
this._rfb._display.set_onFlush(function () {
player._rfb._display.set_onFlush(orig);
let orig = this._rfb._display.onflush;
this._rfb._display.onflush = function () {
player._rfb._display.onflush = orig;
player._rfb._onFlush();
player._doPacket();
});
};
return;
}
@ -184,12 +175,12 @@ RecordingPlayer.prototype = {
_finish() {
if (this._rfb._display.pending()) {
var player = this;
this._rfb._display.set_onFlush(function () {
this._rfb._display.onflush = function () {
if (player._rfb._flushing) {
player._rfb._onFlush();
}
player._finish();
});
};
this._rfb._display.flush();
} else {
this._running = false;

View File

@ -3,7 +3,6 @@ var expect = chai.expect;
import Base64 from '../core/base64.js';
import Display from '../core/display.js';
import { _forceCursorURIs, browserSupportsCursorURIs } from '../core/util/browsers.js';
import sinon from '../vendor/sinon.js';
@ -37,30 +36,11 @@ describe('Display/Canvas Helper', function () {
return Base64.decode(data);
}
describe('checking for cursor uri support', function () {
it('should disable cursor URIs if there is no support', function () {
_forceCursorURIs(false);
var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false });
expect(display._cursor_uri).to.be.false;
});
it('should enable cursor URIs if there is support', function () {
_forceCursorURIs(true);
var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false });
expect(display._cursor_uri).to.be.true;
});
it('respect the cursor_uri option if there is support', function () {
_forceCursorURIs(false);
var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false, cursor_uri: false });
expect(display._cursor_uri).to.be.false;
});
});
describe('viewport handling', function () {
var display;
beforeEach(function () {
display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true });
display = new Display(document.createElement('canvas'));
display.clipViewport = true;
display.resize(5, 5);
display.viewportChangeSize(3, 3);
display.viewportChangePos(1, 1);
@ -112,18 +92,16 @@ describe('Display/Canvas Helper', function () {
});
it('should report clipping when framebuffer > viewport', function () {
var clipping = display.clippingDisplay();
expect(clipping).to.be.true;
expect(display.isClipped).to.be.true;
});
it('should report not clipping when framebuffer = viewport', function () {
display.viewportChangeSize(5, 5);
var clipping = display.clippingDisplay();
expect(clipping).to.be.false;
expect(display.isClipped).to.be.false;
});
it('should show the entire framebuffer when disabling the viewport', function() {
display.set_viewport(false);
display.clipViewport = false;
expect(display.absX(0)).to.equal(0);
expect(display.absY(0)).to.equal(0);
expect(display._target.width).to.equal(5);
@ -131,7 +109,7 @@ describe('Display/Canvas Helper', function () {
});
it('should ignore viewport changes when the viewport is disabled', function() {
display.set_viewport(false);
display.clipViewport = false;
display.viewportChangeSize(2, 2);
display.viewportChangePos(1, 1);
expect(display.absX(0)).to.equal(0);
@ -141,8 +119,8 @@ describe('Display/Canvas Helper', function () {
});
it('should show the entire framebuffer just after enabling the viewport', function() {
display.set_viewport(false);
display.set_viewport(true);
display.clipViewport = false;
display.clipViewport = true;
expect(display.absX(0)).to.equal(0);
expect(display.absY(0)).to.equal(0);
expect(display._target.width).to.equal(5);
@ -153,7 +131,8 @@ describe('Display/Canvas Helper', function () {
describe('resizing', function () {
var display;
beforeEach(function () {
display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: false });
display = new Display(document.createElement('canvas'));
display.clipViewport = false;
display.resize(4, 4);
});
@ -178,7 +157,7 @@ describe('Display/Canvas Helper', function () {
describe('viewport', function () {
beforeEach(function () {
display.set_viewport(true);
display.clipViewport = true;
display.viewportChangeSize(3, 3);
display.viewportChangePos(1, 1);
});
@ -214,11 +193,12 @@ describe('Display/Canvas Helper', function () {
var canvas;
beforeEach(function () {
display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true });
canvas = document.createElement('canvas');
display = new Display(canvas);
display.clipViewport = true;
display.resize(4, 4);
display.viewportChangeSize(3, 3);
display.viewportChangePos(1, 1);
canvas = display.get_target();
document.body.appendChild(canvas);
});
@ -227,21 +207,21 @@ describe('Display/Canvas Helper', function () {
});
it('should not change the bitmap size of the canvas', function () {
display.set_scale(2.0);
display.scale = 2.0;
expect(canvas.width).to.equal(3);
expect(canvas.height).to.equal(3);
});
it('should change the effective rendered size of the canvas', function () {
display.set_scale(2.0);
display.scale = 2.0;
expect(canvas.clientWidth).to.equal(6);
expect(canvas.clientHeight).to.equal(6);
});
it('should not change when resizing', function () {
display.set_scale(2.0);
display.scale = 2.0;
display.resize(5, 5);
expect(display.get_scale()).to.equal(2.0);
expect(display.scale).to.equal(2.0);
expect(canvas.width).to.equal(3);
expect(canvas.height).to.equal(3);
expect(canvas.clientWidth).to.equal(6);
@ -254,9 +234,10 @@ describe('Display/Canvas Helper', function () {
var canvas;
beforeEach(function () {
display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true });
canvas = document.createElement('canvas');
display = new Display(canvas);
display.clipViewport = true;
display.resize(4, 3);
canvas = display.get_target();
document.body.appendChild(canvas);
});
@ -291,144 +272,125 @@ describe('Display/Canvas Helper', function () {
expect(canvas.width).to.equal(4);
expect(canvas.height).to.equal(3);
});
it('should not upscale when downscaleOnly is true', function () {
display.autoscale(2, 2, true);
expect(display.absX(9)).to.equal(18);
expect(display.absY(18)).to.equal(36);
expect(canvas.clientWidth).to.equal(2);
expect(canvas.clientHeight).to.equal(2);
display.autoscale(16, 9, true);
expect(display.absX(9)).to.equal(9);
expect(display.absY(18)).to.equal(18);
expect(canvas.clientWidth).to.equal(4);
expect(canvas.clientHeight).to.equal(3);
});
});
describe('drawing', function () {
// TODO(directxman12): improve the tests for each of the drawing functions to cover more than just the
// basic cases
function drawing_tests (pref_js) {
var display;
beforeEach(function () {
display = new Display({ target: document.createElement('canvas'), prefer_js: pref_js });
display.resize(4, 4);
});
var display;
beforeEach(function () {
display = new Display(document.createElement('canvas'));
display.resize(4, 4);
});
it('should clear the screen on #clear without a logo set', function () {
display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]);
display._logo = null;
display.clear();
display.resize(4, 4);
var empty = [];
for (var i = 0; i < 4 * display._fb_width * display._fb_height; i++) { empty[i] = 0; }
expect(display).to.have.displayed(new Uint8Array(empty));
});
it('should clear the screen on #clear without a logo set', function () {
display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]);
display._logo = null;
display.clear();
display.resize(4, 4);
var empty = [];
for (var i = 0; i < 4 * display._fb_width * display._fb_height; i++) { empty[i] = 0; }
expect(display).to.have.displayed(new Uint8Array(empty));
});
it('should draw the logo on #clear with a logo set', function (done) {
display._logo = { width: 4, height: 4, type: "image/png", data: make_image_png(checked_data) };
display.clear();
display.set_onFlush(function () {
expect(display).to.have.displayed(checked_data);
expect(display._fb_width).to.equal(4);
expect(display._fb_height).to.equal(4);
done();
});
display.flush();
});
it('should not draw directly on the target canvas', function () {
display.fillRect(0, 0, 4, 4, [0, 0, 0xff]);
display.flip();
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
var expected = [];
for (var i = 0; i < 4 * display._fb_width * display._fb_height; i += 4) {
expected[i] = 0xff;
expected[i+1] = expected[i+2] = 0;
expected[i+3] = 0xff;
}
expect(display).to.have.displayed(new Uint8Array(expected));
});
it('should support filling a rectangle with particular color via #fillRect', function () {
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
display.fillRect(0, 0, 2, 2, [0xff, 0, 0]);
display.fillRect(2, 2, 2, 2, [0xff, 0, 0]);
display.flip();
it('should draw the logo on #clear with a logo set', function (done) {
display._logo = { width: 4, height: 4, type: "image/png", data: make_image_png(checked_data) };
display.clear();
display.onflush = function () {
expect(display).to.have.displayed(checked_data);
});
expect(display._fb_width).to.equal(4);
expect(display._fb_height).to.equal(4);
done();
};
display.flush();
});
it('should support copying an portion of the canvas via #copyImage', function () {
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
display.fillRect(0, 0, 2, 2, [0xff, 0, 0x00]);
display.copyImage(0, 0, 2, 2, 2, 2);
display.flip();
it('should not draw directly on the target canvas', function () {
display.fillRect(0, 0, 4, 4, [0, 0, 0xff]);
display.flip();
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
var expected = [];
for (var i = 0; i < 4 * display._fb_width * display._fb_height; i += 4) {
expected[i] = 0xff;
expected[i+1] = expected[i+2] = 0;
expected[i+3] = 0xff;
}
expect(display).to.have.displayed(new Uint8Array(expected));
});
it('should support filling a rectangle with particular color via #fillRect', function () {
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
display.fillRect(0, 0, 2, 2, [0xff, 0, 0]);
display.fillRect(2, 2, 2, 2, [0xff, 0, 0]);
display.flip();
expect(display).to.have.displayed(checked_data);
});
it('should support copying an portion of the canvas via #copyImage', function () {
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
display.fillRect(0, 0, 2, 2, [0xff, 0, 0x00]);
display.copyImage(0, 0, 2, 2, 2, 2);
display.flip();
expect(display).to.have.displayed(checked_data);
});
it('should support drawing images via #imageRect', function (done) {
display.imageRect(0, 0, "image/png", make_image_png(checked_data));
display.flip();
display.onflush = function () {
expect(display).to.have.displayed(checked_data);
});
done();
};
display.flush();
});
it('should support drawing images via #imageRect', function (done) {
display.imageRect(0, 0, "image/png", make_image_png(checked_data));
display.flip();
display.set_onFlush(function () {
expect(display).to.have.displayed(checked_data);
done();
});
display.flush();
});
it('should support drawing tile data with a background color and sub tiles', function () {
display.startTile(0, 0, 4, 4, [0, 0xff, 0]);
display.subTile(0, 0, 2, 2, [0xff, 0, 0]);
display.subTile(2, 2, 2, 2, [0xff, 0, 0]);
display.finishTile();
display.flip();
expect(display).to.have.displayed(checked_data);
});
it('should support drawing tile data with a background color and sub tiles', function () {
display.startTile(0, 0, 4, 4, [0, 0xff, 0]);
display.subTile(0, 0, 2, 2, [0xff, 0, 0]);
display.subTile(2, 2, 2, 2, [0xff, 0, 0]);
display.finishTile();
display.flip();
expect(display).to.have.displayed(checked_data);
});
it('should support drawing BGRX blit images with true color via #blitImage', function () {
var data = [];
for (var i = 0; i < 16; i++) {
data[i * 4] = checked_data[i * 4 + 2];
data[i * 4 + 1] = checked_data[i * 4 + 1];
data[i * 4 + 2] = checked_data[i * 4];
data[i * 4 + 3] = checked_data[i * 4 + 3];
}
display.blitImage(0, 0, 4, 4, data, 0);
display.flip();
expect(display).to.have.displayed(checked_data);
});
it('should support drawing BGRX blit images with true color via #blitImage', function () {
var data = [];
for (var i = 0; i < 16; i++) {
data[i * 4] = checked_data[i * 4 + 2];
data[i * 4 + 1] = checked_data[i * 4 + 1];
data[i * 4 + 2] = checked_data[i * 4];
data[i * 4 + 3] = checked_data[i * 4 + 3];
}
display.blitImage(0, 0, 4, 4, data, 0);
display.flip();
expect(display).to.have.displayed(checked_data);
});
it('should support drawing RGB blit images with true color via #blitRgbImage', function () {
var data = [];
for (var i = 0; i < 16; i++) {
data[i * 3] = checked_data[i * 4];
data[i * 3 + 1] = checked_data[i * 4 + 1];
data[i * 3 + 2] = checked_data[i * 4 + 2];
}
display.blitRgbImage(0, 0, 4, 4, data, 0);
display.flip();
expect(display).to.have.displayed(checked_data);
});
it('should support drawing RGB blit images with true color via #blitRgbImage', function () {
var data = [];
for (var i = 0; i < 16; i++) {
data[i * 3] = checked_data[i * 4];
data[i * 3 + 1] = checked_data[i * 4 + 1];
data[i * 3 + 2] = checked_data[i * 4 + 2];
}
display.blitRgbImage(0, 0, 4, 4, data, 0);
display.flip();
expect(display).to.have.displayed(checked_data);
});
it('should support drawing an image object via #drawImage', function () {
var img = make_image_canvas(checked_data);
display.drawImage(img, 0, 0);
display.flip();
expect(display).to.have.displayed(checked_data);
});
}
describe('(prefering native methods)', function () { drawing_tests.call(this, false); });
describe('(prefering JavaScript)', function () { drawing_tests.call(this, true); });
it('should support drawing an image object via #drawImage', function () {
var img = make_image_canvas(checked_data);
display.drawImage(img, 0, 0);
display.flip();
expect(display).to.have.displayed(checked_data);
});
});
describe('the render queue processor', function () {
var display;
beforeEach(function () {
display = new Display({ target: document.createElement('canvas'), prefer_js: false });
display = new Display(document.createElement('canvas'));
display.resize(4, 4);
sinon.spy(display, '_scan_renderQ');
});
@ -468,11 +430,11 @@ describe('Display/Canvas Helper', function () {
});
it('should call callback when queue is flushed', function () {
display.set_onFlush(sinon.spy());
display.onflush = sinon.spy();
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
expect(display.get_onFlush()).to.not.have.been.called;
expect(display.onflush).to.not.have.been.called;
display.flush();
expect(display.get_onFlush()).to.have.been.calledOnce;
expect(display.onflush).to.have.been.calledOnce;
});
it('should draw a blit image on type "blit"', function () {

View File

@ -31,105 +31,105 @@ describe('Key Event Handling', function() {
describe('Decode Keyboard Events', function() {
it('should decode keydown events', function(done) {
if (isIE() || isEdge()) this.skip();
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
done();
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
});
it('should decode keyup events', function(done) {
if (isIE() || isEdge()) this.skip();
var calls = 0;
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
if (calls++ === 1) {
expect(down).to.be.equal(false);
done();
}
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));
});
describe('Legacy keypress Events', function() {
it('should wait for keypress when needed', function() {
var callback = sinon.spy();
var kbd = new Keyboard({onKeyEvent: callback});
var kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
expect(callback).to.not.have.been.called;
expect(kbd.onkeyevent).to.not.have.been.called;
});
it('should decode keypress events', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
done();
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
kbd._handleKeyPress(keyevent('keypress', {code: 'KeyA', charCode: 0x61}));
});
it('should ignore keypress with different code', function() {
var callback = sinon.spy();
var kbd = new Keyboard({onKeyEvent: callback});
var kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
kbd._handleKeyPress(keyevent('keypress', {code: 'KeyB', charCode: 0x61}));
expect(callback).to.not.have.been.called;
expect(kbd.onkeyevent).to.not.have.been.called;
});
it('should handle keypress with missing code', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
done();
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
kbd._handleKeyPress(keyevent('keypress', {charCode: 0x61}));
});
it('should guess key if no keypress and numeric key', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x32);
expect(code).to.be.equal('Digit2');
expect(down).to.be.equal(true);
done();
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'Digit2', keyCode: 0x32}));
});
it('should guess key if no keypress and alpha key', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
done();
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41, shiftKey: false}));
});
it('should guess key if no keypress and alpha key (with shift)', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x41);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
done();
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41, shiftKey: true}));
});
it('should not guess key if no keypress and unknown key', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
done();
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x09}));
});
});
@ -139,7 +139,7 @@ describe('Key Event Handling', function() {
if (isIE() || isEdge()) this.skip();
});
it('should suppress anything with a valid key', function() {
var kbd = new Keyboard({});
var kbd = new Keyboard(document, {});
var evt = keyevent('keydown', {code: 'KeyA', key: 'a'});
kbd._handleKeyDown(evt);
expect(evt.preventDefault).to.have.been.called;
@ -148,13 +148,13 @@ describe('Key Event Handling', function() {
expect(evt.preventDefault).to.have.been.called;
});
it('should not suppress keys without key', function() {
var kbd = new Keyboard({});
var kbd = new Keyboard(document, {});
var evt = keyevent('keydown', {code: 'KeyA', keyCode: 0x41});
kbd._handleKeyDown(evt);
expect(evt.preventDefault).to.not.have.been.called;
});
it('should suppress the following keypress event', function() {
var kbd = new Keyboard({});
var kbd = new Keyboard(document, {});
var evt = keyevent('keydown', {code: 'KeyA', keyCode: 0x41});
kbd._handleKeyDown(evt);
var evt = keyevent('keypress', {code: 'KeyA', charCode: 0x41});
@ -168,8 +168,8 @@ describe('Key Event Handling', function() {
it('should fake keyup events for virtual keyboards', function(done) {
if (isIE() || isEdge()) this.skip();
var count = 0;
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
switch (count++) {
case 0:
expect(keysym).to.be.equal(0x61);
@ -182,7 +182,7 @@ describe('Key Event Handling', function() {
expect(down).to.be.equal(false);
done();
}
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'Unidentified', key: 'a'}));
});
@ -215,8 +215,8 @@ describe('Key Event Handling', function() {
it('should fake keyup events on iOS', function(done) {
if (isIE() || isEdge()) this.skip();
var count = 0;
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
switch (count++) {
case 0:
expect(keysym).to.be.equal(0x61);
@ -229,7 +229,7 @@ describe('Key Event Handling', function() {
expect(down).to.be.equal(false);
done();
}
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
});
});
@ -240,67 +240,67 @@ describe('Key Event Handling', function() {
if (isIE() || isEdge()) this.skip();
});
it('should send release using the same keysym as the press', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
if (!down) {
done();
}
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'b'}));
});
it('should send the same keysym for multiple presses', function() {
var count = 0;
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
count++;
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'b'}));
expect(count).to.be.equal(2);
});
it('should do nothing on keyup events if no keys are down', function() {
var callback = sinon.spy();
var kbd = new Keyboard({onKeyEvent: callback});
var kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));
expect(callback).to.not.have.been.called;
expect(kbd.onkeyevent).to.not.have.been.called;
});
describe('Legacy Events', function() {
it('should track keys using keyCode if no code', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('Platform65');
if (!down) {
done();
}
}});
};
kbd._handleKeyDown(keyevent('keydown', {keyCode: 65, key: 'a'}));
kbd._handleKeyUp(keyevent('keyup', {keyCode: 65, key: 'b'}));
});
it('should ignore compositing code', function() {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('Unidentified');
}});
};
kbd._handleKeyDown(keyevent('keydown', {keyCode: 229, key: 'a'}));
});
it('should track keys using keyIdentifier if no code', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('Platform65');
if (!down) {
done();
}
}});
};
kbd._handleKeyDown(keyevent('keydown', {keyIdentifier: 'U+0041', key: 'a'}));
kbd._handleKeyUp(keyevent('keyup', {keyIdentifier: 'U+0041', key: 'b'}));
});
@ -335,8 +335,8 @@ describe('Key Event Handling', function() {
it('should change Alt to AltGraph', function() {
var count = 0;
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
switch (count++) {
case 0:
expect(keysym).to.be.equal(0xFF7E);
@ -347,27 +347,27 @@ describe('Key Event Handling', function() {
expect(code).to.be.equal('AltRight');
break;
}
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt', location: 1}));
kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2}));
expect(count).to.be.equal(2);
});
it('should change left Super to Alt', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0xFFE9);
expect(code).to.be.equal('MetaLeft');
done();
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'MetaLeft', key: 'Meta', location: 1}));
});
it('should change right Super to left Super', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
expect(keysym).to.be.equal(0xFFEB);
expect(code).to.be.equal('MetaRight');
done();
}});
};
kbd._handleKeyDown(keyevent('keydown', {code: 'MetaRight', key: 'Meta', location: 2}));
});
});
@ -400,8 +400,8 @@ describe('Key Event Handling', function() {
it('should generate fake undo/redo events on press when AltGraph is down', function() {
var times_called = 0;
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
switch(times_called++) {
case 0:
expect(keysym).to.be.equal(0xFFE3);
@ -439,7 +439,7 @@ describe('Key Event Handling', function() {
expect(down).to.be.equal(true);
break;
}
}});
};
// First the modifier combo
kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));
kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2}));
@ -449,8 +449,8 @@ describe('Key Event Handling', function() {
});
it('should no do anything on key release', function() {
var times_called = 0;
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
switch(times_called++) {
case 7:
expect(keysym).to.be.equal(0x61);
@ -458,7 +458,7 @@ describe('Key Event Handling', function() {
expect(down).to.be.equal(false);
break;
}
}});
};
// First the modifier combo
kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));
kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2}));
@ -469,8 +469,8 @@ describe('Key Event Handling', function() {
});
it('should not consider a char modifier to be down on the modifier key itself', function() {
var times_called = 0;
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
var kbd = new Keyboard(document);
kbd.onkeyevent = function(keysym, code, down) {
switch(times_called++) {
case 0:
expect(keysym).to.be.equal(0xFFE3);
@ -488,7 +488,7 @@ describe('Key Event Handling', function() {
expect(down).to.be.equal(true);
break;
}
}});
};
// First the modifier combo
kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));
kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt', location: 1}));

View File

@ -33,59 +33,51 @@ describe('Mouse Event Handling', function() {
describe('Decode Mouse Events', function() {
it('should decode mousedown events', function(done) {
var mouse = new Mouse({
onMouseButton: function(x, y, down, bmask) {
expect(bmask).to.be.equal(0x01);
expect(down).to.be.equal(1);
done();
},
target: target
});
var mouse = new Mouse(target);
mouse.onmousebutton = function(x, y, down, bmask) {
expect(bmask).to.be.equal(0x01);
expect(down).to.be.equal(1);
done();
};
mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' }));
});
it('should decode mouseup events', function(done) {
var calls = 0;
var mouse = new Mouse({
onMouseButton: function(x, y, down, bmask) {
expect(bmask).to.be.equal(0x01);
if (calls++ === 1) {
expect(down).to.not.be.equal(1);
done();
}
},
target: target
});
var mouse = new Mouse(target);
mouse.onmousebutton = function(x, y, down, bmask) {
expect(bmask).to.be.equal(0x01);
if (calls++ === 1) {
expect(down).to.not.be.equal(1);
done();
}
};
mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' }));
mouse._handleMouseUp(mouseevent('mouseup', { button: '0x01' }));
});
it('should decode mousemove events', function(done) {
var mouse = new Mouse({
onMouseMove: function(x, y) {
// Note that target relative coordinates are sent
expect(x).to.be.equal(40);
expect(y).to.be.equal(10);
done();
},
target: target
});
var mouse = new Mouse(target);
mouse.onmousemove = function(x, y) {
// Note that target relative coordinates are sent
expect(x).to.be.equal(40);
expect(y).to.be.equal(10);
done();
};
mouse._handleMouseMove(mouseevent('mousemove',
{ clientX: 50, clientY: 20 }));
});
it('should decode mousewheel events', function(done) {
var calls = 0;
var mouse = new Mouse({
onMouseButton: function(x, y, down, bmask) {
calls++;
expect(bmask).to.be.equal(1<<6);
if (calls === 1) {
expect(down).to.be.equal(1);
} else if (calls === 2) {
expect(down).to.not.be.equal(1);
done();
}
},
target: target
});
var mouse = new Mouse(target);
mouse.onmousebutton = function(x, y, down, bmask) {
calls++;
expect(bmask).to.be.equal(1<<6);
if (calls === 1) {
expect(down).to.be.equal(1);
} else if (calls === 2) {
expect(down).to.not.be.equal(1);
done();
}
};
mouse._handleMouseWheel(mouseevent('mousewheel',
{ deltaX: 50, deltaY: 0,
deltaMode: 0}));
@ -99,22 +91,20 @@ describe('Mouse Event Handling', function() {
it('should use same pos for 2nd tap if close enough', function(done) {
var calls = 0;
var mouse = new Mouse({
onMouseButton: function(x, y, down, bmask) {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
done();
}
},
target: target
});
var mouse = new Mouse(target);
mouse.onmousebutton = function(x, y, down, bmask) {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
done();
}
};
// touch events are sent in an array of events
// with one item for each touch point
mouse._handleMouseDown(touchevent(
@ -132,22 +122,20 @@ describe('Mouse Event Handling', function() {
it('should not modify 2nd tap pos if far apart', function(done) {
var calls = 0;
var mouse = new Mouse({
onMouseButton: function(x, y, down, bmask) {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.not.be.equal(68);
expect(y).to.not.be.equal(36);
done();
}
},
target: target
});
var mouse = new Mouse(target);
mouse.onmousebutton = function(x, y, down, bmask) {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.not.be.equal(68);
expect(y).to.not.be.equal(36);
done();
}
};
mouse._handleMouseDown(touchevent(
'touchstart', { touches: [{ clientX: 78, clientY: 46 }]}));
this.clock.tick(10);
@ -163,22 +151,20 @@ describe('Mouse Event Handling', function() {
it('should not modify 2nd tap pos if not soon enough', function(done) {
var calls = 0;
var mouse = new Mouse({
onMouseButton: function(x, y, down, bmask) {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.not.be.equal(68);
expect(y).to.not.be.equal(36);
done();
}
},
target: target
});
var mouse = new Mouse(target);
mouse.onmousebutton = function(x, y, down, bmask) {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.not.be.equal(68);
expect(y).to.not.be.equal(36);
done();
}
};
mouse._handleMouseDown(touchevent(
'touchstart', { touches: [{ clientX: 78, clientY: 46 }]}));
this.clock.tick(10);
@ -194,22 +180,20 @@ describe('Mouse Event Handling', function() {
it('should not modify 2nd tap pos if not touch', function(done) {
var calls = 0;
var mouse = new Mouse({
onMouseButton: function(x, y, down, bmask) {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.not.be.equal(68);
expect(y).to.not.be.equal(36);
done();
}
},
target: target
});
var mouse = new Mouse(target);
mouse.onmousebutton = function(x, y, down, bmask) {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.not.be.equal(68);
expect(y).to.not.be.equal(36);
done();
}
};
mouse._handleMouseDown(mouseevent(
'mousedown', { button: '0x01', clientX: 78, clientY: 46 }));
this.clock.tick(10);
@ -231,8 +215,8 @@ describe('Mouse Event Handling', function() {
afterEach(function () { this.clock.restore(); });
it('should accumulate wheel events if small enough', function () {
var callback = sinon.spy();
var mouse = new Mouse({ onMouseButton: callback, target: target });
var mouse = new Mouse(target);
mouse.onmousebutton = sinon.spy();
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
@ -250,7 +234,7 @@ describe('Mouse Event Handling', function() {
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 4, deltaY: 0, deltaMode: 0 }));
expect(callback).to.have.callCount(2); // mouse down and up
expect(mouse.onmousebutton).to.have.callCount(2); // mouse down and up
this.clock.tick(10);
mouse._handleMouseWheel(mouseevent(
@ -260,12 +244,12 @@ describe('Mouse Event Handling', function() {
expect(mouse._accumulatedWheelDeltaX).to.be.equal(4);
expect(mouse._accumulatedWheelDeltaY).to.be.equal(9);
expect(callback).to.have.callCount(2); // still
expect(mouse.onmousebutton).to.have.callCount(2); // still
});
it('should not accumulate large wheel events', function () {
var callback = sinon.spy();
var mouse = new Mouse({ onMouseButton: callback, target: target });
var mouse = new Mouse(target);
mouse.onmousebutton = sinon.spy();
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
@ -279,24 +263,24 @@ describe('Mouse Event Handling', function() {
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 400, deltaY: 400, deltaMode: 0 }));
expect(callback).to.have.callCount(8); // mouse down and up
expect(mouse.onmousebutton).to.have.callCount(8); // mouse down and up
});
it('should send even small wheel events after a timeout', function () {
var callback = sinon.spy();
var mouse = new Mouse({ onMouseButton: callback, target: target });
var mouse = new Mouse(target);
mouse.onmousebutton = sinon.spy();
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 1, deltaY: 0, deltaMode: 0 }));
this.clock.tick(51); // timeout on 50 ms
expect(callback).to.have.callCount(2); // mouse down and up
expect(mouse.onmousebutton).to.have.callCount(2); // mouse down and up
});
it('should account for non-zero deltaMode', function () {
var callback = sinon.spy();
var mouse = new Mouse({ onMouseButton: callback, target: target });
var mouse = new Mouse(target);
mouse.onmousebutton = sinon.spy();
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
@ -308,7 +292,7 @@ describe('Mouse Event Handling', function() {
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 1, deltaY: 0, deltaMode: 2 }));
expect(callback).to.have.callCount(4); // mouse down and up
expect(mouse.onmousebutton).to.have.callCount(4); // mouse down and up
});
});

File diff suppressed because it is too large Load Diff

View File

@ -152,18 +152,18 @@
</div>
</div>
<!-- XVP Shutdown/Reboot -->
<!-- Shutdown/Reboot -->
<input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
id="noVNC_xvp_button" class="noVNC_button"
id="noVNC_power_button" class="noVNC_button"
title="Shutdown/Reboot..." />
<div class="noVNC_vcenter">
<div id="noVNC_xvp" class="noVNC_panel">
<div id="noVNC_power" class="noVNC_panel">
<div class="noVNC_heading">
<img src="app/images/power.svg"> Power
</div>
<input type="button" id="noVNC_xvp_shutdown_button" value="Shutdown" />
<input type="button" id="noVNC_xvp_reboot_button" value="Reboot" />
<input type="button" id="noVNC_xvp_reset_button" value="Reset" />
<input type="button" id="noVNC_shutdown_button" value="Shutdown" />
<input type="button" id="noVNC_reboot_button" value="Reboot" />
<input type="button" id="noVNC_reset_button" value="Reset" />
</div>
</div>
@ -213,7 +213,6 @@
<select id="noVNC_setting_resize" name="vncResize">
<option value="off">None</option>
<option value="scale">Local Scaling</option>
<option value="downscale">Local Downscaling</option>
<option value="remote">Remote Resizing</option>
</select>
</li>
@ -221,10 +220,6 @@
<li>
<div class="noVNC_expander">Advanced</div>
<div><ul>
<li>
<label><input id="noVNC_setting_cursor" type="checkbox" /> Local Cursor</label>
</li>
<li><hr></li>
<li>
<label for="noVNC_setting_repeaterID">Repeater ID:</label>
<input id="noVNC_setting_repeaterID" type="input" value="" />

View File

@ -80,6 +80,7 @@
import RFB from './core/rfb.js';
var rfb;
var doneInitialResize;
var resizeTimeout;
var desktopName;
@ -92,17 +93,15 @@
rfb.requestDesktopSize(innerW, innerH - controlbarH);
}
}
function FBUComplete(rfb, fbu) {
function initialResize() {
if (doneInitialResize) return;
UIresize();
rfb.set_onFBUComplete(function() { });
doneInitialResize = true;
}
function updateDesktopName(rfb, name) {
desktopName = name;
function updateDesktopName(e) {
desktopName = e.detail.name;
}
function passwordRequired(rfb, msg) {
if (typeof msg === 'undefined') {
msg = 'Password Required: ';
}
function credentials(e) {
var html;
var form = document.createElement('form');
@ -114,26 +113,26 @@
document.getElementById('noVNC_status_bar').setAttribute("class", "noVNC_status_warn");
document.getElementById('noVNC_status').innerHTML = '';
document.getElementById('noVNC_status').appendChild(form);
document.getElementById('noVNC_status').querySelector('label').textContent = msg;
document.getElementById('noVNC_status').querySelector('label').textContent = 'Password Required: ';
}
function setPassword() {
rfb.sendPassword(document.getElementById('password_input').value);
rfb.sendCredentials({ password: document.getElementById('password_input').value });
return false;
}
function sendCtrlAltDel() {
rfb.sendCtrlAltDel();
return false;
}
function xvpShutdown() {
rfb.xvpShutdown();
function machineShutdown() {
rfb.machineShutdown();
return false;
}
function xvpReboot() {
rfb.xvpReboot();
function machineReboot() {
rfb.machineReboot();
return false;
}
function xvpReset() {
rfb.xvpReset();
function machineReset() {
rfb.machineReset();
return false;
}
function status(text, level) {
@ -148,14 +147,16 @@
document.getElementById('noVNC_status_bar').className = "noVNC_status_" + level;
document.getElementById('noVNC_status').textContent = text;
}
function updateState(rfb, state, oldstate) {
function updateState(e) {
var cad = document.getElementById('sendCtrlAltDelButton');
switch (state) {
switch (e.detail.state) {
case 'connecting':
status("Connecting", "normal");
break;
case 'connected':
if (rfb && rfb.get_encrypt()) {
doneInitialResize = false;
if (WebUtil.getConfigVar('encrypt',
(window.location.protocol === "https:"))) {
status("Connected (encrypted) to " +
desktopName, "normal");
} else {
@ -170,25 +171,25 @@
status("Disconnected", "normal");
break;
default:
status(state, "warn");
status(e.detail.state, "warn");
break;
}
if (state === 'connected') {
if (e.detail.state === 'connected') {
cad.disabled = false;
} else {
cad.disabled = true;
xvpInit(0);
updatePowerButtons();
}
}
function disconnected(rfb, reason) {
if (typeof(reason) !== 'undefined') {
status(reason, "error");
function disconnect(e) {
if (typeof(e.detail.reason) !== 'undefined') {
status(e.detail.reason, "error");
}
}
function notification(rfb, msg, level, options) {
status(msg, level);
function notification(e) {
status(e.detail.message, e.detail.level);
}
window.onresize = function () {
@ -201,10 +202,10 @@
}, 500);
};
function xvpInit(ver) {
var xvpbuttons;
xvpbuttons = document.getElementById('noVNC_xvp_buttons');
if (ver >= 1) {
function updatePowerButtons() {
var powerbuttons;
powerbuttons = document.getElementById('noVNC_power_buttons');
if (rfb.capabilities.power) {
xvpbuttons.className= "noVNC_shown";
} else {
xvpbuttons.className = "noVNC_hidden";
@ -212,9 +213,9 @@
}
document.getElementById('sendCtrlAltDelButton').onclick = sendCtrlAltDel;
document.getElementById('xvpShutdownButton').onclick = xvpShutdown;
document.getElementById('xvpRebootButton').onclick = xvpReboot;
document.getElementById('xvpResetButton').onclick = xvpReset;
document.getElementById('machineShutdownButton').onclick = machineShutdown;
document.getElementById('machineRebootButton').onclick = machineReboot;
document.getElementById('machineResetButton').onclick = machineReset;
WebUtil.init_logging(WebUtil.getConfigVar('logging', 'warn'));
document.title = WebUtil.getConfigVar('title', 'noVNC');
@ -252,27 +253,32 @@
status('Must specify host and port in URL', 'error');
}
try {
rfb = new RFB({'target': document.getElementById('noVNC_canvas'),
'encrypt': WebUtil.getConfigVar('encrypt',
(window.location.protocol === "https:")),
'repeaterID': WebUtil.getConfigVar('repeaterID', ''),
'local_cursor': WebUtil.getConfigVar('cursor', true),
'shared': WebUtil.getConfigVar('shared', true),
'view_only': WebUtil.getConfigVar('view_only', false),
'onNotification': notification,
'onUpdateState': updateState,
'onDisconnected': disconnected,
'onXvpInit': xvpInit,
'onPasswordRequired': passwordRequired,
'onFBUComplete': FBUComplete,
'onDesktopName': updateDesktopName});
} catch (exc) {
status('Unable to create RFB client -- ' + exc, 'error');
return; // don't continue trying to connect
var url;
if (WebUtil.getConfigVar('encrypt',
(window.location.protocol === "https:"))) {
url = 'wss';
} else {
url = 'ws';
}
rfb.connect(host, port, password, path);
url += '://' + host;
if(port) {
url += ':' + port;
}
url += '/' + path;
rfb = new RFB(document.getElementById('noVNC_canvas'), url,
{ repeaterID: WebUtil.getConfigVar('repeaterID', ''),
shared: WebUtil.getConfigVar('shared', true),
credentials: { password: password } });
rfb.viewOnly = WebUtil.getConfigVar('view_only', false);
rfb.addEventListener("notification", notification);
rfb.addEventListener("updatestate", updateState);
rfb.addEventListener("disconnect", disconnect);
rfb.addEventListener("capabilities", function () { updatePowerButtons(); initialResize(); });
rfb.addEventListener("credentialsrequired", credentials);
rfb.addEventListener("desktopname", updateDesktopName);
})();
</script>
</head>
@ -284,13 +290,13 @@
<div id="noVNC_buttons">
<input type=button value="Send CtrlAltDel"
id="sendCtrlAltDelButton" class="noVNC_shown">
<span id="noVNC_xvp_buttons" class="noVNC_hidden">
<span id="noVNC_power_buttons" class="noVNC_hidden">
<input type=button value="Shutdown"
id="xvpShutdownButton">
id="machineShutdownButton">
<input type=button value="Reboot"
id="xvpRebootButton">
id="machineRebootButton">
<input type=button value="Reset"
id="xvpResetButton">
id="machineResetButton">
</span>
</div>
</div>