From 6d6f0db0da5a46e530fc891136224d58771c00c6 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Sat, 4 Feb 2017 21:26:00 -0500 Subject: [PATCH] Refactor ES6 module structure/split up Util This commit restructures many of the ES6 modules, splitting them up to actual export multiple functions instead of a single object. It also splits up Util into multiple sub-modules, to make it easier to maintain. Finally, localisation is renamed to localization. --- .gitignore | 2 + LICENSE.txt | 17 +- app/ui.js | 2813 ++++++++++++------------- app/webutil.js | 68 +- core/base64.js | 10 +- core/display.js | 1428 +++++++------ core/inflator.js | 1 - core/input/devices.js | 734 ++++--- core/input/keysym.js | 4 +- core/input/keysymdef.js | 31 +- core/input/util.js | 528 +++-- core/input/xtscancodes.js | 4 +- core/rfb.js | 4109 ++++++++++++++++++------------------- core/util.js | 624 ------ core/util/browsers.js | 120 ++ core/util/events.js | 161 ++ core/util/localization.js | 170 ++ core/util/logging.js | 53 + core/util/properties.js | 138 ++ core/util/strings.js | 15 + core/websock.js | 614 +++--- 21 files changed, 5806 insertions(+), 5838 deletions(-) delete mode 100644 core/util.js create mode 100644 core/util/browsers.js create mode 100644 core/util/events.js create mode 100644 core/util/localization.js create mode 100644 core/util/logging.js create mode 100644 core/util/properties.js create mode 100644 core/util/strings.js diff --git a/.gitignore b/.gitignore index 921551cc..e9c84876 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ utils/websockify /build /lib recordings +*.swp +*~ diff --git a/LICENSE.txt b/LICENSE.txt index 39cbe3f2..460060a0 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -5,20 +5,9 @@ Public License 2.0). The noVNC core library is composed of the Javascript code necessary for full noVNC operation. This includes (but is not limited to): - core/base64.js - core/des.js - core/display.js - core/input/devices.js - core/input/keysym.js - core/logo.js - core/playback.js - core/rfb.js - app/ui.js - core/util.js - core/websock.js - app/webutil.js - core/input/xtscancodes.js - core/inflator.js + core/**/*.js + app/*.js + test/playback.js The HTML, CSS, font and images files that included with the noVNC source distibution (or repository) are not considered part of the diff --git a/app/ui.js b/app/ui.js index ce1d9c5c..c53ddf70 100644 --- a/app/ui.js +++ b/app/ui.js @@ -11,443 +11,446 @@ /* jslint white: false, browser: true */ /* global window, document.getElementById, Util, WebUtil, RFB, Display */ -import Util from "../core/util.js"; +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 { setCapture, getPointerEvent } from '../core/util/events.js'; import KeyTable from "../core/input/keysym.js"; import keysyms from "../core/input/keysymdef.js"; import RFB from "../core/rfb.js"; import Display from "../core/display.js"; -import WebUtil from "./webutil.js"; +import * as WebUtil from "./webutil.js"; -var UI; +// Fallback for all uncought errors +window.addEventListener('error', function(event) { + try { + var msg, div, text; -(function () { - "use strict"; + msg = document.getElementById('noVNC_fallback_errormsg'); - // Fallback for all uncought errors - window.addEventListener('error', function(event) { - try { - var msg, div, text; - - msg = document.getElementById('noVNC_fallback_errormsg'); - - // Only show the initial error - if (msg.hasChildNodes()) { - return false; - } - - div = document.createElement("div"); - div.appendChild(document.createTextNode(event.message)); - msg.appendChild(div); - - div = document.createElement("div"); - div.className = 'noVNC_location'; - text = event.filename + ":" + event.lineno + ":" + event.colno; - div.appendChild(document.createTextNode(text)); - msg.appendChild(div); - - if ((event.error !== undefined) && - (event.error.stack !== undefined)) { - div = document.createElement("div"); - div.className = 'noVNC_stack'; - div.appendChild(document.createTextNode(event.error.stack)); - msg.appendChild(div); - } - - document.getElementById('noVNC_fallback_error') - .classList.add("noVNC_open"); - } catch (exc) { - document.write("noVNC encountered an error."); + // Only show the initial error + if (msg.hasChildNodes()) { + return false; } - // Don't return true since this would prevent the error - // from being printed to the browser console. - return false; - }); - var _ = Util.Localisation.get; + div = document.createElement("div"); + div.appendChild(document.createTextNode(event.message)); + msg.appendChild(div); - UI = { + div = document.createElement("div"); + div.className = 'noVNC_location'; + text = event.filename + ":" + event.lineno + ":" + event.colno; + div.appendChild(document.createTextNode(text)); + msg.appendChild(div); - connected: false, - desktopName: "", + if ((event.error !== undefined) && + (event.error.stack !== undefined)) { + div = document.createElement("div"); + div.className = 'noVNC_stack'; + div.appendChild(document.createTextNode(event.error.stack)); + msg.appendChild(div); + } - resizeTimeout: null, - statusTimeout: null, - hideKeyboardTimeout: null, - idleControlbarTimeout: null, - closeControlbarTimeout: null, + document.getElementById('noVNC_fallback_error') + .classList.add("noVNC_open"); + } catch (exc) { + document.write("noVNC encountered an error."); + } + // Don't return true since this would prevent the error + // from being printed to the browser console. + return false; +}); - controlbarGrabbed: false, - controlbarDrag: false, - controlbarMouseDownClientY: 0, - controlbarMouseDownOffsetY: 0, +const UI = { - isSafari: false, - rememberedClipSetting: null, - lastKeyboardinput: null, - defaultKeyboardinputLen: 100, + connected: false, + desktopName: "", - inhibit_reconnect: true, - reconnect_callback: null, - reconnect_password: null, + resizeTimeout: null, + statusTimeout: null, + hideKeyboardTimeout: null, + idleControlbarTimeout: null, + closeControlbarTimeout: null, - // Setup rfb object, load settings from browser storage, then call - // UI.init to setup the UI/menus - load: function(callback) { - WebUtil.initSettings(UI.start, callback); - }, + controlbarGrabbed: false, + controlbarDrag: false, + controlbarMouseDownClientY: 0, + controlbarMouseDownOffsetY: 0, - // Render default UI and initialize settings menu - start: function(callback) { + isSafari: false, + rememberedClipSetting: null, + lastKeyboardinput: null, + defaultKeyboardinputLen: 100, - // Setup global variables first - UI.isSafari = (navigator.userAgent.indexOf('Safari') !== -1 && - navigator.userAgent.indexOf('Chrome') === -1); + inhibit_reconnect: true, + reconnect_callback: null, + reconnect_password: null, - UI.initSettings(); + prime: function(callback) { + if (document.readyState === "interactive" || document.readyState === "complete") { + UI.load(callback); + } else { + document.addEventListener('DOMContentLoaded', UI.load.bind(UI, callback)); + } + }, - // Translate the DOM - Util.Localisation.translateDOM(); + // Setup rfb object, load settings from browser storage, then call + // UI.init to setup the UI/menus + load: function(callback) { + WebUtil.initSettings(UI.start, callback); + }, - // Adapt the interface for touch screen devices - if (Util.isTouchDevice) { - document.documentElement.classList.add("noVNC_touch"); - // Remove the address bar - setTimeout(function() { window.scrollTo(0, 1); }, 100); + // Render default UI and initialize settings menu + start: function(callback) { + + // Setup global variables first + UI.isSafari = (navigator.userAgent.indexOf('Safari') !== -1 && + navigator.userAgent.indexOf('Chrome') === -1); + + UI.initSettings(); + + // Translate the DOM + l10n.translateDOM(); + + // Adapt the interface for touch screen devices + if (isTouchDevice) { + document.documentElement.classList.add("noVNC_touch"); + // Remove the address bar + setTimeout(function() { window.scrollTo(0, 1); }, 100); + } + + // Restore control bar position + if (WebUtil.readSetting('controlbar_pos') === 'right') { + UI.toggleControlbarSide(); + } + + UI.initFullscreen(); + + // Setup event handlers + UI.addResizeHandlers(); + UI.addControlbarHandlers(); + UI.addTouchSpecificHandlers(); + UI.addExtraKeysHandlers(); + UI.addXvpHandlers(); + UI.addConnectionControlHandlers(); + UI.addClipboardHandlers(); + UI.addSettingsHandlers(); + document.getElementById("noVNC_status") + .addEventListener('click', UI.hideStatus); + + UI.openControlbar(); + + // Show the connect panel on first load unless autoconnecting + if (!autoconnect) { + UI.openConnectPanel(); + } + + UI.updateViewClip(); + + UI.updateVisualState(); + + document.getElementById('noVNC_setting_host').focus(); + + var autoconnect = WebUtil.getConfigVar('autoconnect', false); + if (autoconnect === 'true' || autoconnect == '1') { + autoconnect = true; + UI.connect(); + } else { + autoconnect = false; + } + + if (typeof callback === "function") { + callback(UI.rfb); + } + }, + + initFullscreen: function() { + // Only show the button if fullscreen is properly supported + // * Safari doesn't support alphanumerical input while in fullscreen + if (!UI.isSafari && + (document.documentElement.requestFullscreen || + document.documentElement.mozRequestFullScreen || + document.documentElement.webkitRequestFullscreen || + document.body.msRequestFullscreen)) { + document.getElementById('noVNC_fullscreen_button') + .classList.remove("noVNC_hidden"); + UI.addFullscreenHandlers(); + } + }, + + initSettings: function() { + var i; + + // Logging selection dropdown + var llevels = ['error', 'warn', 'info', 'debug']; + for (i = 0; i < llevels.length; i += 1) { + UI.addOption(document.getElementById('noVNC_setting_logging'),llevels[i], llevels[i]); + } + + // Settings with immediate effects + UI.initSetting('logging', 'warn'); + UI.updateLogging(); + + // if port == 80 (or 443) then it won't be present and should be + // set manually + var port = window.location.port; + if (!port) { + if (window.location.protocol.substring(0,5) == 'https') { + port = 443; } - - // Restore control bar position - if (WebUtil.readSetting('controlbar_pos') === 'right') { - UI.toggleControlbarSide(); + else if (window.location.protocol.substring(0,4) == 'http') { + port = 80; } + } - UI.initFullscreen(); + /* Populate the controls if defaults are provided in the URL */ + UI.initSetting('host', window.location.hostname); + UI.initSetting('port', port); + UI.initSetting('encrypt', (window.location.protocol === "https:")); + UI.initSetting('true_color', true); + UI.initSetting('cursor', !isTouchDevice); + UI.initSetting('clip', false); + UI.initSetting('resize', 'off'); + UI.initSetting('shared', true); + UI.initSetting('view_only', false); + UI.initSetting('path', 'websockify'); + UI.initSetting('repeaterID', ''); + UI.initSetting('reconnect', false); + UI.initSetting('reconnect_delay', 5000); - // Setup event handlers - UI.addResizeHandlers(); - UI.addControlbarHandlers(); - UI.addTouchSpecificHandlers(); - UI.addExtraKeysHandlers(); - UI.addXvpHandlers(); - UI.addConnectionControlHandlers(); - UI.addClipboardHandlers(); - UI.addSettingsHandlers(); - document.getElementById("noVNC_status") - .addEventListener('click', UI.hideStatus); - - UI.openControlbar(); - - // Show the connect panel on first load unless autoconnecting - if (!autoconnect) { - UI.openConnectPanel(); - } - - UI.updateViewClip(); - - UI.updateVisualState(); - - document.getElementById('noVNC_setting_host').focus(); - - var autoconnect = WebUtil.getConfigVar('autoconnect', false); - if (autoconnect === 'true' || autoconnect == '1') { - autoconnect = true; - UI.connect(); + UI.setupSettingLabels(); + }, + // Adds a link to the label elements on the corresponding input elements + setupSettingLabels: function() { + var labels = document.getElementsByTagName('LABEL'); + for (var i = 0; i < labels.length; i++) { + var htmlFor = labels[i].htmlFor; + if (htmlFor != '') { + var elem = document.getElementById(htmlFor); + if (elem) elem.label = labels[i]; } else { - autoconnect = false; - } - - if (typeof callback === "function") { - callback(UI.rfb); - } - }, - - initFullscreen: function() { - // Only show the button if fullscreen is properly supported - // * Safari doesn't support alphanumerical input while in fullscreen - if (!UI.isSafari && - (document.documentElement.requestFullscreen || - document.documentElement.mozRequestFullScreen || - document.documentElement.webkitRequestFullscreen || - document.body.msRequestFullscreen)) { - document.getElementById('noVNC_fullscreen_button') - .classList.remove("noVNC_hidden"); - UI.addFullscreenHandlers(); - } - }, - - initSettings: function() { - var i; - - // Logging selection dropdown - var llevels = ['error', 'warn', 'info', 'debug']; - for (i = 0; i < llevels.length; i += 1) { - UI.addOption(document.getElementById('noVNC_setting_logging'),llevels[i], llevels[i]); - } - - // Settings with immediate effects - UI.initSetting('logging', 'warn'); - UI.updateLogging(); - - // if port == 80 (or 443) then it won't be present and should be - // set manually - var port = window.location.port; - if (!port) { - if (window.location.protocol.substring(0,5) == 'https') { - port = 443; - } - else if (window.location.protocol.substring(0,4) == 'http') { - port = 80; - } - } - - /* Populate the controls if defaults are provided in the URL */ - UI.initSetting('host', window.location.hostname); - UI.initSetting('port', port); - UI.initSetting('encrypt', (window.location.protocol === "https:")); - UI.initSetting('true_color', true); - UI.initSetting('cursor', !Util.isTouchDevice); - UI.initSetting('clip', false); - UI.initSetting('resize', 'off'); - UI.initSetting('shared', true); - UI.initSetting('view_only', false); - UI.initSetting('path', 'websockify'); - UI.initSetting('repeaterID', ''); - UI.initSetting('reconnect', false); - UI.initSetting('reconnect_delay', 5000); - - UI.setupSettingLabels(); - }, - - // Adds a link to the label elements on the corresponding input elements - setupSettingLabels: function() { - var labels = document.getElementsByTagName('LABEL'); - for (var i = 0; i < labels.length; i++) { - var htmlFor = labels[i].htmlFor; - if (htmlFor != '') { - var elem = document.getElementById(htmlFor); - if (elem) elem.label = labels[i]; - } else { - // If 'for' isn't set, use the first input element child - var children = labels[i].children; - for (var j = 0; j < children.length; j++) { - if (children[j].form !== undefined) { - children[j].label = labels[i]; - break; - } + // If 'for' isn't set, use the first input element child + var children = labels[i].children; + for (var j = 0; j < children.length; j++) { + if (children[j].form !== undefined) { + children[j].label = labels[i]; + break; } } } - }, + } + }, - 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; - Util.Error(msg); - UI.showStatus(msg, 'error'); - return false; - } - }, + 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 - * ============== - * EVENT HANDLERS - * ------v------*/ +* /INIT +* ============== +* EVENT HANDLERS +* ------v------*/ - addResizeHandlers: function() { - window.addEventListener('resize', UI.applyResizeMode); - window.addEventListener('resize', UI.updateViewClip); - }, + addResizeHandlers: function() { + window.addEventListener('resize', UI.applyResizeMode); + window.addEventListener('resize', UI.updateViewClip); + }, - addControlbarHandlers: function() { - document.getElementById("noVNC_control_bar") - .addEventListener('mousemove', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('mouseup', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('mousedown', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('keypress', UI.activateControlbar); + addControlbarHandlers: function() { + document.getElementById("noVNC_control_bar") + .addEventListener('mousemove', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('mouseup', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('mousedown', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('keypress', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('mousedown', UI.keepControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('keypress', UI.keepControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('mousedown', UI.keepControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('keypress', UI.keepControlbar); - document.getElementById("noVNC_view_drag_button") - .addEventListener('click', UI.toggleViewDrag); + document.getElementById("noVNC_view_drag_button") + .addEventListener('click', UI.toggleViewDrag); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('mousedown', UI.controlbarHandleMouseDown); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('mouseup', UI.controlbarHandleMouseUp); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('mousemove', UI.dragControlbarHandle); - // resize events aren't available for elements - window.addEventListener('resize', UI.updateControlbarHandle); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('mousedown', UI.controlbarHandleMouseDown); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('mouseup', UI.controlbarHandleMouseUp); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('mousemove', UI.dragControlbarHandle); + // resize events aren't available for elements + window.addEventListener('resize', UI.updateControlbarHandle); - var exps = document.getElementsByClassName("noVNC_expander"); - for (var i = 0;i < exps.length;i++) { - exps[i].addEventListener('click', UI.toggleExpander); - } - }, + var exps = document.getElementsByClassName("noVNC_expander"); + for (var i = 0;i < exps.length;i++) { + exps[i].addEventListener('click', UI.toggleExpander); + } + }, - addTouchSpecificHandlers: function() { - document.getElementById("noVNC_mouse_button0") - .addEventListener('click', function () { UI.setMouseButton(1); }); - document.getElementById("noVNC_mouse_button1") - .addEventListener('click', function () { UI.setMouseButton(2); }); - document.getElementById("noVNC_mouse_button2") - .addEventListener('click', function () { UI.setMouseButton(4); }); - document.getElementById("noVNC_mouse_button4") - .addEventListener('click', function () { UI.setMouseButton(0); }); - document.getElementById("noVNC_keyboard_button") - .addEventListener('click', UI.toggleVirtualKeyboard); + addTouchSpecificHandlers: function() { + document.getElementById("noVNC_mouse_button0") + .addEventListener('click', function () { UI.setMouseButton(1); }); + document.getElementById("noVNC_mouse_button1") + .addEventListener('click', function () { UI.setMouseButton(2); }); + document.getElementById("noVNC_mouse_button2") + .addEventListener('click', function () { UI.setMouseButton(4); }); + document.getElementById("noVNC_mouse_button4") + .addEventListener('click', function () { UI.setMouseButton(0); }); + document.getElementById("noVNC_keyboard_button") + .addEventListener('click', UI.toggleVirtualKeyboard); - document.getElementById("noVNC_keyboardinput") - .addEventListener('input', UI.keyInput); - document.getElementById("noVNC_keyboardinput") - .addEventListener('focus', UI.onfocusVirtualKeyboard); - document.getElementById("noVNC_keyboardinput") - .addEventListener('blur', UI.onblurVirtualKeyboard); - document.getElementById("noVNC_keyboardinput") - .addEventListener('submit', function () { return false; }); + document.getElementById("noVNC_keyboardinput") + .addEventListener('input', UI.keyInput); + document.getElementById("noVNC_keyboardinput") + .addEventListener('focus', UI.onfocusVirtualKeyboard); + document.getElementById("noVNC_keyboardinput") + .addEventListener('blur', UI.onblurVirtualKeyboard); + document.getElementById("noVNC_keyboardinput") + .addEventListener('submit', function () { return false; }); - document.documentElement - .addEventListener('mousedown', UI.keepVirtualKeyboard, true); + document.documentElement + .addEventListener('mousedown', UI.keepVirtualKeyboard, true); - document.getElementById("noVNC_control_bar") - .addEventListener('touchstart', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('touchmove', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('touchend', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('input', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('touchstart', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('touchmove', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('touchend', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('input', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('touchstart', UI.keepControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('input', UI.keepControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('touchstart', UI.keepControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('input', UI.keepControlbar); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('touchstart', UI.controlbarHandleMouseDown); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('touchend', UI.controlbarHandleMouseUp); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('touchmove', UI.dragControlbarHandle); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('touchstart', UI.controlbarHandleMouseDown); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('touchend', UI.controlbarHandleMouseUp); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('touchmove', UI.dragControlbarHandle); - window.addEventListener('load', UI.keyboardinputReset); - }, + window.addEventListener('load', UI.keyboardinputReset); + }, - addExtraKeysHandlers: function() { - document.getElementById("noVNC_toggle_extra_keys_button") - .addEventListener('click', UI.toggleExtraKeys); - document.getElementById("noVNC_toggle_ctrl_button") - .addEventListener('click', UI.toggleCtrl); - document.getElementById("noVNC_toggle_alt_button") - .addEventListener('click', UI.toggleAlt); - document.getElementById("noVNC_send_tab_button") - .addEventListener('click', UI.sendTab); - document.getElementById("noVNC_send_esc_button") - .addEventListener('click', UI.sendEsc); - document.getElementById("noVNC_send_ctrl_alt_del_button") - .addEventListener('click', UI.sendCtrlAltDel); - }, + addExtraKeysHandlers: function() { + document.getElementById("noVNC_toggle_extra_keys_button") + .addEventListener('click', UI.toggleExtraKeys); + document.getElementById("noVNC_toggle_ctrl_button") + .addEventListener('click', UI.toggleCtrl); + document.getElementById("noVNC_toggle_alt_button") + .addEventListener('click', UI.toggleAlt); + document.getElementById("noVNC_send_tab_button") + .addEventListener('click', UI.sendTab); + document.getElementById("noVNC_send_esc_button") + .addEventListener('click', UI.sendEsc); + document.getElementById("noVNC_send_ctrl_alt_del_button") + .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); - }, + 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); + }, - addConnectionControlHandlers: function() { - document.getElementById("noVNC_disconnect_button") - .addEventListener('click', UI.disconnect); - document.getElementById("noVNC_connect_button") - .addEventListener('click', UI.connect); - document.getElementById("noVNC_cancel_reconnect_button") - .addEventListener('click', UI.cancelReconnect); + addConnectionControlHandlers: function() { + document.getElementById("noVNC_disconnect_button") + .addEventListener('click', UI.disconnect); + document.getElementById("noVNC_connect_button") + .addEventListener('click', UI.connect); + document.getElementById("noVNC_cancel_reconnect_button") + .addEventListener('click', UI.cancelReconnect); - document.getElementById("noVNC_password_button") - .addEventListener('click', UI.setPassword); - }, + document.getElementById("noVNC_password_button") + .addEventListener('click', UI.setPassword); + }, - addClipboardHandlers: function() { - document.getElementById("noVNC_clipboard_button") - .addEventListener('click', UI.toggleClipboardPanel); - document.getElementById("noVNC_clipboard_text") - .addEventListener('focus', UI.displayBlur); - document.getElementById("noVNC_clipboard_text") - .addEventListener('blur', UI.displayFocus); - document.getElementById("noVNC_clipboard_text") - .addEventListener('change', UI.clipboardSend); - document.getElementById("noVNC_clipboard_clear_button") - .addEventListener('click', UI.clipboardClear); - }, + addClipboardHandlers: function() { + document.getElementById("noVNC_clipboard_button") + .addEventListener('click', UI.toggleClipboardPanel); + document.getElementById("noVNC_clipboard_text") + .addEventListener('focus', UI.displayBlur); + document.getElementById("noVNC_clipboard_text") + .addEventListener('blur', UI.displayFocus); + document.getElementById("noVNC_clipboard_text") + .addEventListener('change', UI.clipboardSend); + document.getElementById("noVNC_clipboard_clear_button") + .addEventListener('click', UI.clipboardClear); + }, - // Add a call to save settings when the element changes, - // unless the optional parameter changeFunc is used instead. - addSettingChangeHandler: function(name, changeFunc) { - var settingElem = document.getElementById("noVNC_setting_" + name); - if (changeFunc === undefined) { - changeFunc = function () { UI.saveSetting(name); }; - } - settingElem.addEventListener('change', changeFunc); - }, + // Add a call to save settings when the element changes, + // unless the optional parameter changeFunc is used instead. + addSettingChangeHandler: function(name, changeFunc) { + var settingElem = document.getElementById("noVNC_setting_" + name); + if (changeFunc === undefined) { + changeFunc = function () { UI.saveSetting(name); }; + } + settingElem.addEventListener('change', changeFunc); + }, - addSettingsHandlers: function() { - document.getElementById("noVNC_settings_button") - .addEventListener('click', UI.toggleSettingsPanel); + addSettingsHandlers: function() { + document.getElementById("noVNC_settings_button") + .addEventListener('click', UI.toggleSettingsPanel); - UI.addSettingChangeHandler('encrypt'); - UI.addSettingChangeHandler('true_color'); - UI.addSettingChangeHandler('cursor'); - UI.addSettingChangeHandler('cursor', UI.updateLocalCursor); - UI.addSettingChangeHandler('resize'); - UI.addSettingChangeHandler('resize', UI.enableDisableViewClip); - UI.addSettingChangeHandler('resize', UI.applyResizeMode); - UI.addSettingChangeHandler('clip'); - UI.addSettingChangeHandler('clip', UI.updateViewClip); - UI.addSettingChangeHandler('shared'); - UI.addSettingChangeHandler('view_only'); - UI.addSettingChangeHandler('view_only', UI.updateViewOnly); - UI.addSettingChangeHandler('host'); - UI.addSettingChangeHandler('port'); - UI.addSettingChangeHandler('path'); - UI.addSettingChangeHandler('repeaterID'); - UI.addSettingChangeHandler('logging'); - UI.addSettingChangeHandler('logging', UI.updateLogging); - UI.addSettingChangeHandler('reconnect'); - UI.addSettingChangeHandler('reconnect_delay'); - }, + UI.addSettingChangeHandler('encrypt'); + UI.addSettingChangeHandler('true_color'); + UI.addSettingChangeHandler('cursor'); + UI.addSettingChangeHandler('cursor', UI.updateLocalCursor); + UI.addSettingChangeHandler('resize'); + UI.addSettingChangeHandler('resize', UI.enableDisableViewClip); + UI.addSettingChangeHandler('resize', UI.applyResizeMode); + UI.addSettingChangeHandler('clip'); + UI.addSettingChangeHandler('clip', UI.updateViewClip); + UI.addSettingChangeHandler('shared'); + UI.addSettingChangeHandler('view_only'); + UI.addSettingChangeHandler('view_only', UI.updateViewOnly); + UI.addSettingChangeHandler('host'); + UI.addSettingChangeHandler('port'); + UI.addSettingChangeHandler('path'); + UI.addSettingChangeHandler('repeaterID'); + UI.addSettingChangeHandler('logging'); + UI.addSettingChangeHandler('logging', UI.updateLogging); + UI.addSettingChangeHandler('reconnect'); + UI.addSettingChangeHandler('reconnect_delay'); + }, - addFullscreenHandlers: function() { - document.getElementById("noVNC_fullscreen_button") - .addEventListener('click', UI.toggleFullscreen); + addFullscreenHandlers: function() { + document.getElementById("noVNC_fullscreen_button") + .addEventListener('click', UI.toggleFullscreen); - window.addEventListener('fullscreenchange', UI.updateFullscreenButton); - window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton); - window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton); - window.addEventListener('msfullscreenchange', UI.updateFullscreenButton); - }, + window.addEventListener('fullscreenchange', UI.updateFullscreenButton); + window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton); + window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton); + window.addEventListener('msfullscreenchange', UI.updateFullscreenButton); + }, /* ------^------- * /EVENT HANDLERS @@ -455,341 +458,341 @@ var UI; * VISUAL * ------v------*/ - updateState: function(rfb, state, oldstate) { - var msg; + updateState: function(rfb, state, oldstate) { + var msg; - document.documentElement.classList.remove("noVNC_connecting"); - document.documentElement.classList.remove("noVNC_connected"); - document.documentElement.classList.remove("noVNC_disconnecting"); - document.documentElement.classList.remove("noVNC_reconnecting"); + document.documentElement.classList.remove("noVNC_connecting"); + document.documentElement.classList.remove("noVNC_connected"); + document.documentElement.classList.remove("noVNC_disconnecting"); + document.documentElement.classList.remove("noVNC_reconnecting"); - switch (state) { - case 'connecting': - document.getElementById("noVNC_transition_text").textContent = _("Connecting..."); - document.documentElement.classList.add("noVNC_connecting"); - break; - case 'connected': - UI.connected = true; - UI.inhibit_reconnect = false; - document.documentElement.classList.add("noVNC_connected"); - if (rfb && rfb.get_encrypt()) { - msg = _("Connected (encrypted) to ") + UI.desktopName; - } else { - msg = _("Connected (unencrypted) to ") + UI.desktopName; - } - UI.showStatus(msg); - break; - case 'disconnecting': - UI.connected = false; - document.getElementById("noVNC_transition_text").textContent = _("Disconnecting..."); - document.documentElement.classList.add("noVNC_disconnecting"); - break; - case 'disconnected': - UI.showStatus(_("Disconnected")); - break; - default: - msg = "Invalid UI state"; - Util.Error(msg); - UI.showStatus(msg, 'error'); - break; - } + switch (state) { + case 'connecting': + document.getElementById("noVNC_transition_text").textContent = _("Connecting..."); + document.documentElement.classList.add("noVNC_connecting"); + break; + case 'connected': + UI.connected = true; + UI.inhibit_reconnect = false; + document.documentElement.classList.add("noVNC_connected"); + if (rfb && rfb.get_encrypt()) { + msg = _("Connected (encrypted) to ") + UI.desktopName; + } else { + msg = _("Connected (unencrypted) to ") + UI.desktopName; + } + UI.showStatus(msg); + break; + case 'disconnecting': + UI.connected = false; + document.getElementById("noVNC_transition_text").textContent = _("Disconnecting..."); + document.documentElement.classList.add("noVNC_disconnecting"); + break; + case 'disconnected': + UI.showStatus(_("Disconnected")); + break; + default: + msg = "Invalid UI state"; + Log.Error(msg); + UI.showStatus(msg, 'error'); + break; + } - UI.updateVisualState(); - }, + UI.updateVisualState(); + }, - // Disable/enable controls depending on connection state - updateVisualState: function() { - //Util.Debug(">> updateVisualState"); + // Disable/enable controls depending on connection state + updateVisualState: function() { + //Log.Debug(">> updateVisualState"); - UI.enableDisableViewClip(); + UI.enableDisableViewClip(); - if (Util.browserSupportsCursorURIs() && !Util.isTouchDevice) { - UI.enableSetting('cursor'); - } else { - UI.disableSetting('cursor'); - } + if (cursorURIsSupported() && !isTouchDevice) { + UI.enableSetting('cursor'); + } else { + UI.disableSetting('cursor'); + } - if (UI.connected) { - UI.disableSetting('encrypt'); - UI.disableSetting('true_color'); - UI.disableSetting('shared'); - UI.disableSetting('host'); - UI.disableSetting('port'); - UI.disableSetting('path'); - UI.disableSetting('repeaterID'); - UI.updateViewClip(); - UI.setMouseButton(1); + if (UI.connected) { + UI.disableSetting('encrypt'); + UI.disableSetting('true_color'); + UI.disableSetting('shared'); + UI.disableSetting('host'); + UI.disableSetting('port'); + UI.disableSetting('path'); + UI.disableSetting('repeaterID'); + UI.updateViewClip(); + UI.setMouseButton(1); - // Hide the controlbar after 2 seconds - UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000); - } else { - UI.enableSetting('encrypt'); - UI.enableSetting('true_color'); - UI.enableSetting('shared'); - UI.enableSetting('host'); - UI.enableSetting('port'); - UI.enableSetting('path'); - UI.enableSetting('repeaterID'); - UI.updateXvpButton(0); - UI.keepControlbar(); - } + // Hide the controlbar after 2 seconds + UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000); + } else { + UI.enableSetting('encrypt'); + UI.enableSetting('true_color'); + UI.enableSetting('shared'); + UI.enableSetting('host'); + UI.enableSetting('port'); + UI.enableSetting('path'); + UI.enableSetting('repeaterID'); + UI.updateXvpButton(0); + UI.keepControlbar(); + } - // Hide input related buttons in view only mode - if (UI.rfb && UI.rfb.get_view_only()) { - document.getElementById('noVNC_keyboard_button') - .classList.add('noVNC_hidden'); - document.getElementById('noVNC_toggle_extra_keys_button') - .classList.add('noVNC_hidden'); - } else { - document.getElementById('noVNC_keyboard_button') - .classList.remove('noVNC_hidden'); - document.getElementById('noVNC_toggle_extra_keys_button') - .classList.remove('noVNC_hidden'); - } + // Hide input related buttons in view only mode + if (UI.rfb && UI.rfb.get_view_only()) { + document.getElementById('noVNC_keyboard_button') + .classList.add('noVNC_hidden'); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.add('noVNC_hidden'); + } else { + document.getElementById('noVNC_keyboard_button') + .classList.remove('noVNC_hidden'); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.remove('noVNC_hidden'); + } - // State change disables viewport dragging. - // It is enabled (toggled) by direct click on the button - UI.setViewDrag(false); + // State change disables viewport dragging. + // It is enabled (toggled) by direct click on the button + UI.setViewDrag(false); - // State change also closes the password dialog - document.getElementById('noVNC_password_dlg') - .classList.remove('noVNC_open'); + // State change also closes the password dialog + document.getElementById('noVNC_password_dlg') + .classList.remove('noVNC_open'); - //Util.Debug("<< updateVisualState"); - }, + //Log.Debug("<< updateVisualState"); + }, - showStatus: function(text, status_type, time) { - var statusElem = document.getElementById('noVNC_status'); + showStatus: function(text, status_type, time) { + var statusElem = document.getElementById('noVNC_status'); - clearTimeout(UI.statusTimeout); + clearTimeout(UI.statusTimeout); - if (typeof status_type === 'undefined') { - status_type = 'normal'; - } + if (typeof status_type === 'undefined') { + status_type = 'normal'; + } - statusElem.classList.remove("noVNC_status_normal"); - statusElem.classList.remove("noVNC_status_warn"); - statusElem.classList.remove("noVNC_status_error"); + statusElem.classList.remove("noVNC_status_normal"); + statusElem.classList.remove("noVNC_status_warn"); + statusElem.classList.remove("noVNC_status_error"); - switch (status_type) { - case 'warning': - case 'warn': - statusElem.classList.add("noVNC_status_warn"); - break; - case 'error': - statusElem.classList.add("noVNC_status_error"); - break; - case 'normal': - case 'info': - default: - statusElem.classList.add("noVNC_status_normal"); - break; - } + switch (status_type) { + case 'warning': + case 'warn': + statusElem.classList.add("noVNC_status_warn"); + break; + case 'error': + statusElem.classList.add("noVNC_status_error"); + break; + case 'normal': + case 'info': + default: + statusElem.classList.add("noVNC_status_normal"); + break; + } - statusElem.textContent = text; - statusElem.classList.add("noVNC_open"); + statusElem.textContent = text; + statusElem.classList.add("noVNC_open"); - // If no time was specified, show the status for 1.5 seconds - if (typeof time === 'undefined') { - time = 1500; - } + // If no time was specified, show the status for 1.5 seconds + if (typeof time === 'undefined') { + time = 1500; + } - // Error messages do not timeout - if (status_type !== 'error') { - UI.statusTimeout = window.setTimeout(UI.hideStatus, time); - } - }, + // Error messages do not timeout + if (status_type !== 'error') { + UI.statusTimeout = window.setTimeout(UI.hideStatus, time); + } + }, - hideStatus: function() { - clearTimeout(UI.statusTimeout); - document.getElementById('noVNC_status').classList.remove("noVNC_open"); - }, + hideStatus: function() { + clearTimeout(UI.statusTimeout); + document.getElementById('noVNC_status').classList.remove("noVNC_open"); + }, - notification: function (rfb, msg, level, options) { - UI.showStatus(msg, level); - }, + notification: function (rfb, msg, level, options) { + UI.showStatus(msg, level); + }, - activateControlbar: function(event) { - clearTimeout(UI.idleControlbarTimeout); - // We manipulate the anchor instead of the actual control - // bar in order to avoid creating new a stacking group - document.getElementById('noVNC_control_bar_anchor') - .classList.remove("noVNC_idle"); - UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000); - }, + activateControlbar: function(event) { + clearTimeout(UI.idleControlbarTimeout); + // We manipulate the anchor instead of the actual control + // bar in order to avoid creating new a stacking group + document.getElementById('noVNC_control_bar_anchor') + .classList.remove("noVNC_idle"); + UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000); + }, - idleControlbar: function() { - document.getElementById('noVNC_control_bar_anchor') - .classList.add("noVNC_idle"); - }, + idleControlbar: function() { + document.getElementById('noVNC_control_bar_anchor') + .classList.add("noVNC_idle"); + }, - keepControlbar: function() { - clearTimeout(UI.closeControlbarTimeout); - }, + keepControlbar: function() { + clearTimeout(UI.closeControlbarTimeout); + }, - openControlbar: function() { - document.getElementById('noVNC_control_bar') - .classList.add("noVNC_open"); - }, + openControlbar: function() { + document.getElementById('noVNC_control_bar') + .classList.add("noVNC_open"); + }, - closeControlbar: function() { - UI.closeAllPanels(); - document.getElementById('noVNC_control_bar') - .classList.remove("noVNC_open"); - }, + closeControlbar: function() { + UI.closeAllPanels(); + document.getElementById('noVNC_control_bar') + .classList.remove("noVNC_open"); + }, - toggleControlbar: function() { - if (document.getElementById('noVNC_control_bar') - .classList.contains("noVNC_open")) { - UI.closeControlbar(); - } else { - UI.openControlbar(); - } - }, + toggleControlbar: function() { + if (document.getElementById('noVNC_control_bar') + .classList.contains("noVNC_open")) { + UI.closeControlbar(); + } else { + UI.openControlbar(); + } + }, - toggleControlbarSide: function () { - // Temporarily disable animation to avoid weird movement - var bar = document.getElementById('noVNC_control_bar'); - bar.style.transitionDuration = '0s'; - bar.addEventListener('transitionend', function () { this.style.transitionDuration = ""; }); + toggleControlbarSide: function () { + // Temporarily disable animation to avoid weird movement + var bar = document.getElementById('noVNC_control_bar'); + bar.style.transitionDuration = '0s'; + bar.addEventListener('transitionend', function () { this.style.transitionDuration = ""; }); - var anchor = document.getElementById('noVNC_control_bar_anchor'); + var anchor = document.getElementById('noVNC_control_bar_anchor'); + if (anchor.classList.contains("noVNC_right")) { + WebUtil.writeSetting('controlbar_pos', 'left'); + anchor.classList.remove("noVNC_right"); + } else { + WebUtil.writeSetting('controlbar_pos', 'right'); + anchor.classList.add("noVNC_right"); + } + + // Consider this a movement of the handle + UI.controlbarDrag = true; + }, + + dragControlbarHandle: function (e) { + if (!UI.controlbarGrabbed) return; + + var ptr = getPointerEvent(e); + + var anchor = document.getElementById('noVNC_control_bar_anchor'); + if (ptr.clientX < (window.innerWidth * 0.1)) { if (anchor.classList.contains("noVNC_right")) { - WebUtil.writeSetting('controlbar_pos', 'left'); - anchor.classList.remove("noVNC_right"); - } else { - WebUtil.writeSetting('controlbar_pos', 'right'); - anchor.classList.add("noVNC_right"); + UI.toggleControlbarSide(); } + } else if (ptr.clientX > (window.innerWidth * 0.9)) { + if (!anchor.classList.contains("noVNC_right")) { + UI.toggleControlbarSide(); + } + } + + if (!UI.controlbarDrag) { + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + var dragThreshold = 10 * (window.devicePixelRatio || 1); + var dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY); + + if (dragDistance < dragThreshold) return; - // Consider this a movement of the handle UI.controlbarDrag = true; - }, + } - dragControlbarHandle: function (e) { - if (!UI.controlbarGrabbed) return; + var eventY = ptr.clientY - UI.controlbarMouseDownOffsetY; - var ptr = Util.getPointerEvent(e); + UI.moveControlbarHandle(eventY); - var anchor = document.getElementById('noVNC_control_bar_anchor'); - if (ptr.clientX < (window.innerWidth * 0.1)) { - if (anchor.classList.contains("noVNC_right")) { - UI.toggleControlbarSide(); - } - } else if (ptr.clientX > (window.innerWidth * 0.9)) { - if (!anchor.classList.contains("noVNC_right")) { - UI.toggleControlbarSide(); - } - } + e.preventDefault(); + e.stopPropagation(); + UI.keepControlbar(); + UI.activateControlbar(); + }, - if (!UI.controlbarDrag) { - // The goal is to trigger on a certain physical width, the - // devicePixelRatio brings us a bit closer but is not optimal. - var dragThreshold = 10 * (window.devicePixelRatio || 1); - var dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY); + // Move the handle but don't allow any position outside the bounds + moveControlbarHandle: function (viewportRelativeY) { + var handle = document.getElementById("noVNC_control_bar_handle"); + var handleHeight = handle.getBoundingClientRect().height; + var controlbarBounds = document.getElementById("noVNC_control_bar") + .getBoundingClientRect(); + var margin = 10; - if (dragDistance < dragThreshold) return; + // These heights need to be non-zero for the below logic to work + if (handleHeight === 0 || controlbarBounds.height === 0) { + return; + } - UI.controlbarDrag = true; - } + var newY = viewportRelativeY; - var eventY = ptr.clientY - UI.controlbarMouseDownOffsetY; + // Check if the coordinates are outside the control bar + if (newY < controlbarBounds.top + margin) { + // Force coordinates to be below the top of the control bar + newY = controlbarBounds.top + margin; - UI.moveControlbarHandle(eventY); + } else if (newY > controlbarBounds.top + + controlbarBounds.height - handleHeight - margin) { + // Force coordinates to be above the bottom of the control bar + newY = controlbarBounds.top + + controlbarBounds.height - handleHeight - margin; + } + // Corner case: control bar too small for stable position + if (controlbarBounds.height < (handleHeight + margin * 2)) { + newY = controlbarBounds.top + + (controlbarBounds.height - handleHeight) / 2; + } + + // The transform needs coordinates that are relative to the parent + var parentRelativeY = newY - controlbarBounds.top; + handle.style.transform = "translateY(" + parentRelativeY + "px)"; + }, + + updateControlbarHandle: function () { + // Since the control bar is fixed on the viewport and not the page, + // the move function expects coordinates relative the the viewport. + var handle = document.getElementById("noVNC_control_bar_handle"); + var handleBounds = handle.getBoundingClientRect(); + UI.moveControlbarHandle(handleBounds.top); + }, + + controlbarHandleMouseUp: function(e) { + if ((e.type == "mouseup") && (e.button != 0)) return; + + // mouseup and mousedown on the same place toggles the controlbar + if (UI.controlbarGrabbed && !UI.controlbarDrag) { + UI.toggleControlbar(); e.preventDefault(); e.stopPropagation(); UI.keepControlbar(); UI.activateControlbar(); - }, + } + UI.controlbarGrabbed = false; + }, - // Move the handle but don't allow any position outside the bounds - moveControlbarHandle: function (viewportRelativeY) { - var handle = document.getElementById("noVNC_control_bar_handle"); - var handleHeight = handle.getBoundingClientRect().height; - var controlbarBounds = document.getElementById("noVNC_control_bar") - .getBoundingClientRect(); - var margin = 10; + controlbarHandleMouseDown: function(e) { + if ((e.type == "mousedown") && (e.button != 0)) return; - // These heights need to be non-zero for the below logic to work - if (handleHeight === 0 || controlbarBounds.height === 0) { - return; - } + var ptr = getPointerEvent(e); - var newY = viewportRelativeY; + var handle = document.getElementById("noVNC_control_bar_handle"); + var bounds = handle.getBoundingClientRect(); - // Check if the coordinates are outside the control bar - if (newY < controlbarBounds.top + margin) { - // Force coordinates to be below the top of the control bar - newY = controlbarBounds.top + margin; + setCapture(handle); + UI.controlbarGrabbed = true; + UI.controlbarDrag = false; - } else if (newY > controlbarBounds.top + - controlbarBounds.height - handleHeight - margin) { - // Force coordinates to be above the bottom of the control bar - newY = controlbarBounds.top + - controlbarBounds.height - handleHeight - margin; - } + UI.controlbarMouseDownClientY = ptr.clientY; + UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top; + e.preventDefault(); + e.stopPropagation(); + UI.keepControlbar(); + UI.activateControlbar(); + }, - // Corner case: control bar too small for stable position - if (controlbarBounds.height < (handleHeight + margin * 2)) { - newY = controlbarBounds.top + - (controlbarBounds.height - handleHeight) / 2; - } - - // The transform needs coordinates that are relative to the parent - var parentRelativeY = newY - controlbarBounds.top; - handle.style.transform = "translateY(" + parentRelativeY + "px)"; - }, - - updateControlbarHandle: function () { - // Since the control bar is fixed on the viewport and not the page, - // the move function expects coordinates relative the the viewport. - var handle = document.getElementById("noVNC_control_bar_handle"); - var handleBounds = handle.getBoundingClientRect(); - UI.moveControlbarHandle(handleBounds.top); - }, - - controlbarHandleMouseUp: function(e) { - if ((e.type == "mouseup") && (e.button != 0)) return; - - // mouseup and mousedown on the same place toggles the controlbar - if (UI.controlbarGrabbed && !UI.controlbarDrag) { - UI.toggleControlbar(); - e.preventDefault(); - e.stopPropagation(); - UI.keepControlbar(); - UI.activateControlbar(); - } - UI.controlbarGrabbed = false; - }, - - controlbarHandleMouseDown: function(e) { - if ((e.type == "mousedown") && (e.button != 0)) return; - - var ptr = Util.getPointerEvent(e); - - var handle = document.getElementById("noVNC_control_bar_handle"); - var bounds = handle.getBoundingClientRect(); - - Util.setCapture(handle); - UI.controlbarGrabbed = true; - UI.controlbarDrag = false; - - UI.controlbarMouseDownClientY = ptr.clientY; - UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top; - e.preventDefault(); - e.stopPropagation(); - UI.keepControlbar(); - UI.activateControlbar(); - }, - - toggleExpander: function(e) { - if (this.classList.contains("noVNC_open")) { - this.classList.remove("noVNC_open"); - } else { - this.classList.add("noVNC_open"); - } - }, + toggleExpander: function(e) { + if (this.classList.contains("noVNC_open")) { + this.classList.remove("noVNC_open"); + } else { + this.classList.add("noVNC_open"); + } + }, /* ------^------- * /VISUAL @@ -797,93 +800,93 @@ var UI; * SETTINGS * ------v------*/ - // Initial page load read/initialization of settings - initSetting: function(name, defVal) { - // Check Query string followed by cookie - var val = WebUtil.getConfigVar(name); - if (val === null) { - val = WebUtil.readSetting(name, defVal); - } - UI.updateSetting(name, val); - return val; - }, + // Initial page load read/initialization of settings + initSetting: function(name, defVal) { + // Check Query string followed by cookie + var val = WebUtil.getConfigVar(name); + if (val === null) { + val = WebUtil.readSetting(name, defVal); + } + UI.updateSetting(name, val); + return val; + }, - // Update cookie and form control setting. If value is not set, then - // updates from control to current cookie setting. - updateSetting: function(name, value) { + // Update cookie and form control setting. If value is not set, then + // updates from control to current cookie setting. + updateSetting: function(name, value) { - // Save the cookie for this session - if (typeof value !== 'undefined') { - WebUtil.writeSetting(name, value); - } + // Save the cookie for this session + if (typeof value !== 'undefined') { + WebUtil.writeSetting(name, value); + } - // Update the settings control - value = UI.getSetting(name); + // Update the settings control + value = UI.getSetting(name); - var ctrl = document.getElementById('noVNC_setting_' + name); - if (ctrl.type === 'checkbox') { - ctrl.checked = value; + var ctrl = document.getElementById('noVNC_setting_' + name); + if (ctrl.type === 'checkbox') { + ctrl.checked = value; - } else if (typeof ctrl.options !== 'undefined') { - for (var i = 0; i < ctrl.options.length; i += 1) { - if (ctrl.options[i].value === value) { - ctrl.selectedIndex = i; - break; - } + } else if (typeof ctrl.options !== 'undefined') { + for (var i = 0; i < ctrl.options.length; i += 1) { + if (ctrl.options[i].value === value) { + ctrl.selectedIndex = i; + break; } + } + } else { + /*Weird IE9 error leads to 'null' appearring + in textboxes instead of ''.*/ + if (value === null) { + value = ""; + } + ctrl.value = value; + } + }, + + // Save control setting to cookie + saveSetting: function(name) { + var val, ctrl = document.getElementById('noVNC_setting_' + name); + if (ctrl.type === 'checkbox') { + val = ctrl.checked; + } else if (typeof ctrl.options !== 'undefined') { + val = ctrl.options[ctrl.selectedIndex].value; + } else { + val = ctrl.value; + } + WebUtil.writeSetting(name, val); + //Log.Debug("Setting saved '" + name + "=" + val + "'"); + return val; + }, + + // Read form control compatible setting from cookie + getSetting: function(name) { + var ctrl = document.getElementById('noVNC_setting_' + name); + var val = WebUtil.readSetting(name); + if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { + if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { + val = false; } else { - /*Weird IE9 error leads to 'null' appearring - in textboxes instead of ''.*/ - if (value === null) { - value = ""; - } - ctrl.value = value; + val = true; } - }, + } + return val; + }, - // Save control setting to cookie - saveSetting: function(name) { - var val, ctrl = document.getElementById('noVNC_setting_' + name); - if (ctrl.type === 'checkbox') { - val = ctrl.checked; - } else if (typeof ctrl.options !== 'undefined') { - val = ctrl.options[ctrl.selectedIndex].value; - } else { - val = ctrl.value; - } - WebUtil.writeSetting(name, val); - //Util.Debug("Setting saved '" + name + "=" + val + "'"); - return val; - }, + // These helpers compensate for the lack of parent-selectors and + // previous-sibling-selectors in CSS which are needed when we want to + // disable the labels that belong to disabled input elements. + disableSetting: function(name) { + var ctrl = document.getElementById('noVNC_setting_' + name); + ctrl.disabled = true; + ctrl.label.classList.add('noVNC_disabled'); + }, - // Read form control compatible setting from cookie - getSetting: function(name) { - var ctrl = document.getElementById('noVNC_setting_' + name); - var val = WebUtil.readSetting(name); - if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { - if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { - val = false; - } else { - val = true; - } - } - return val; - }, - - // These helpers compensate for the lack of parent-selectors and - // previous-sibling-selectors in CSS which are needed when we want to - // disable the labels that belong to disabled input elements. - disableSetting: function(name) { - var ctrl = document.getElementById('noVNC_setting_' + name); - ctrl.disabled = true; - ctrl.label.classList.add('noVNC_disabled'); - }, - - enableSetting: function(name) { - var ctrl = document.getElementById('noVNC_setting_' + name); - ctrl.disabled = false; - ctrl.label.classList.remove('noVNC_disabled'); - }, + enableSetting: function(name) { + var ctrl = document.getElementById('noVNC_setting_' + name); + ctrl.disabled = false; + ctrl.label.classList.remove('noVNC_disabled'); + }, /* ------^------- * /SETTINGS @@ -891,12 +894,12 @@ var UI; * PANELS * ------v------*/ - closeAllPanels: function() { - UI.closeSettingsPanel(); - UI.closeXvpPanel(); - UI.closeClipboardPanel(); - UI.closeExtraKeys(); - }, + closeAllPanels: function() { + UI.closeSettingsPanel(); + UI.closeXvpPanel(); + UI.closeClipboardPanel(); + UI.closeExtraKeys(); + }, /* ------^------- * /PANELS @@ -904,50 +907,50 @@ var UI; * SETTINGS (panel) * ------v------*/ - openSettingsPanel: function() { - UI.closeAllPanels(); - UI.openControlbar(); + openSettingsPanel: function() { + UI.closeAllPanels(); + UI.openControlbar(); - // Refresh UI elements from saved cookies - UI.updateSetting('encrypt'); - UI.updateSetting('true_color'); - if (Util.browserSupportsCursorURIs()) { - UI.updateSetting('cursor'); - } else { - UI.updateSetting('cursor', !Util.isTouchDevice); - UI.disableSetting('cursor'); - } - UI.updateSetting('clip'); - UI.updateSetting('resize'); - UI.updateSetting('shared'); - UI.updateSetting('view_only'); - UI.updateSetting('path'); - UI.updateSetting('repeaterID'); - UI.updateSetting('logging'); - UI.updateSetting('reconnect'); - UI.updateSetting('reconnect_delay'); + // Refresh UI elements from saved cookies + UI.updateSetting('encrypt'); + UI.updateSetting('true_color'); + if (cursorURIsSupported()) { + UI.updateSetting('cursor'); + } else { + UI.updateSetting('cursor', !isTouchDevice); + UI.disableSetting('cursor'); + } + UI.updateSetting('clip'); + UI.updateSetting('resize'); + UI.updateSetting('shared'); + UI.updateSetting('view_only'); + UI.updateSetting('path'); + UI.updateSetting('repeaterID'); + UI.updateSetting('logging'); + UI.updateSetting('reconnect'); + UI.updateSetting('reconnect_delay'); - document.getElementById('noVNC_settings') - .classList.add("noVNC_open"); - document.getElementById('noVNC_settings_button') - .classList.add("noVNC_selected"); - }, + document.getElementById('noVNC_settings') + .classList.add("noVNC_open"); + document.getElementById('noVNC_settings_button') + .classList.add("noVNC_selected"); + }, - closeSettingsPanel: function() { - document.getElementById('noVNC_settings') - .classList.remove("noVNC_open"); - document.getElementById('noVNC_settings_button') - .classList.remove("noVNC_selected"); - }, + closeSettingsPanel: function() { + document.getElementById('noVNC_settings') + .classList.remove("noVNC_open"); + document.getElementById('noVNC_settings_button') + .classList.remove("noVNC_selected"); + }, - toggleSettingsPanel: function() { - if (document.getElementById('noVNC_settings') - .classList.contains("noVNC_open")) { - UI.closeSettingsPanel(); - } else { - UI.openSettingsPanel(); - } - }, + toggleSettingsPanel: function() { + if (document.getElementById('noVNC_settings') + .classList.contains("noVNC_open")) { + UI.closeSettingsPanel(); + } else { + UI.openSettingsPanel(); + } + }, /* ------^------- * /SETTINGS @@ -955,44 +958,44 @@ var UI; * XVP * ------v------*/ - openXvpPanel: function() { - UI.closeAllPanels(); - UI.openControlbar(); + openXvpPanel: function() { + UI.closeAllPanels(); + UI.openControlbar(); - document.getElementById('noVNC_xvp') - .classList.add("noVNC_open"); + document.getElementById('noVNC_xvp') + .classList.add("noVNC_open"); + document.getElementById('noVNC_xvp_button') + .classList.add("noVNC_selected"); + }, + + closeXvpPanel: function() { + document.getElementById('noVNC_xvp') + .classList.remove("noVNC_open"); + document.getElementById('noVNC_xvp_button') + .classList.remove("noVNC_selected"); + }, + + toggleXvpPanel: function() { + if (document.getElementById('noVNC_xvp') + .classList.contains("noVNC_open")) { + UI.closeXvpPanel(); + } else { + UI.openXvpPanel(); + } + }, + + // Disable/enable XVP button + updateXvpButton: function(ver) { + if (ver >= 1 && !UI.rfb.get_view_only()) { document.getElementById('noVNC_xvp_button') - .classList.add("noVNC_selected"); - }, - - closeXvpPanel: function() { - document.getElementById('noVNC_xvp') - .classList.remove("noVNC_open"); + .classList.remove("noVNC_hidden"); + } else { document.getElementById('noVNC_xvp_button') - .classList.remove("noVNC_selected"); - }, - - toggleXvpPanel: function() { - if (document.getElementById('noVNC_xvp') - .classList.contains("noVNC_open")) { - UI.closeXvpPanel(); - } else { - UI.openXvpPanel(); - } - }, - - // Disable/enable XVP button - updateXvpButton: function(ver) { - if (ver >= 1 && !UI.rfb.get_view_only()) { - document.getElementById('noVNC_xvp_button') - .classList.remove("noVNC_hidden"); - } else { - document.getElementById('noVNC_xvp_button') - .classList.add("noVNC_hidden"); - // Close XVP panel if open - UI.closeXvpPanel(); - } - }, + .classList.add("noVNC_hidden"); + // Close XVP panel if open + UI.closeXvpPanel(); + } + }, /* ------^------- * /XVP @@ -1000,49 +1003,49 @@ var UI; * CLIPBOARD * ------v------*/ - openClipboardPanel: function() { - UI.closeAllPanels(); - UI.openControlbar(); + openClipboardPanel: function() { + UI.closeAllPanels(); + UI.openControlbar(); - document.getElementById('noVNC_clipboard') - .classList.add("noVNC_open"); - document.getElementById('noVNC_clipboard_button') - .classList.add("noVNC_selected"); - }, + document.getElementById('noVNC_clipboard') + .classList.add("noVNC_open"); + document.getElementById('noVNC_clipboard_button') + .classList.add("noVNC_selected"); + }, - closeClipboardPanel: function() { - document.getElementById('noVNC_clipboard') - .classList.remove("noVNC_open"); - document.getElementById('noVNC_clipboard_button') - .classList.remove("noVNC_selected"); - }, + closeClipboardPanel: function() { + document.getElementById('noVNC_clipboard') + .classList.remove("noVNC_open"); + document.getElementById('noVNC_clipboard_button') + .classList.remove("noVNC_selected"); + }, - toggleClipboardPanel: function() { - if (document.getElementById('noVNC_clipboard') - .classList.contains("noVNC_open")) { - UI.closeClipboardPanel(); - } else { - UI.openClipboardPanel(); - } - }, + toggleClipboardPanel: function() { + if (document.getElementById('noVNC_clipboard') + .classList.contains("noVNC_open")) { + UI.closeClipboardPanel(); + } else { + UI.openClipboardPanel(); + } + }, - clipboardReceive: function(rfb, text) { - Util.Debug(">> UI.clipboardReceive: " + text.substr(0,40) + "..."); - document.getElementById('noVNC_clipboard_text').value = text; - Util.Debug("<< UI.clipboardReceive"); - }, + clipboardReceive: function(rfb, text) { + Log.Debug(">> UI.clipboardReceive: " + text.substr(0,40) + "..."); + document.getElementById('noVNC_clipboard_text').value = text; + Log.Debug("<< UI.clipboardReceive"); + }, - clipboardClear: function() { - document.getElementById('noVNC_clipboard_text').value = ""; - UI.rfb.clipboardPasteFrom(""); - }, + clipboardClear: function() { + document.getElementById('noVNC_clipboard_text').value = ""; + UI.rfb.clipboardPasteFrom(""); + }, - clipboardSend: function() { - var text = document.getElementById('noVNC_clipboard_text').value; - Util.Debug(">> UI.clipboardSend: " + text.substr(0,40) + "..."); - UI.rfb.clipboardPasteFrom(text); - Util.Debug("<< UI.clipboardSend"); - }, + clipboardSend: function() { + var text = document.getElementById('noVNC_clipboard_text').value; + Log.Debug(">> UI.clipboardSend: " + text.substr(0,40) + "..."); + UI.rfb.clipboardPasteFrom(text); + Log.Debug("<< UI.clipboardSend"); + }, /* ------^------- * /CLIPBOARD @@ -1050,102 +1053,102 @@ var UI; * CONNECTION * ------v------*/ - openConnectPanel: function() { - document.getElementById('noVNC_connect_dlg') - .classList.add("noVNC_open"); - }, + openConnectPanel: function() { + document.getElementById('noVNC_connect_dlg') + .classList.add("noVNC_open"); + }, - closeConnectPanel: function() { - document.getElementById('noVNC_connect_dlg') - .classList.remove("noVNC_open"); - }, + closeConnectPanel: function() { + document.getElementById('noVNC_connect_dlg') + .classList.remove("noVNC_open"); + }, - connect: function(event, password) { - var host = UI.getSetting('host'); - var port = UI.getSetting('port'); - var path = UI.getSetting('path'); + connect: function(event, password) { + var host = UI.getSetting('host'); + var port = UI.getSetting('port'); + var path = UI.getSetting('path'); - if (typeof password === 'undefined') { - password = WebUtil.getConfigVar('password'); - } + if (typeof password === 'undefined') { + password = WebUtil.getConfigVar('password'); + } - if (password === null) { - password = undefined; - } + if (password === null) { + password = undefined; + } - if ((!host) || (!port)) { - var msg = _("Must set host and port"); - Util.Error(msg); - UI.showStatus(msg, 'error'); - return; - } + if ((!host) || (!port)) { + var msg = _("Must set host and port"); + Log.Error(msg); + UI.showStatus(msg, 'error'); + return; + } - if (!UI.initRFB()) return; + if (!UI.initRFB()) return; - UI.closeAllPanels(); - UI.closeConnectPanel(); + UI.closeAllPanels(); + UI.closeConnectPanel(); - UI.rfb.set_encrypt(UI.getSetting('encrypt')); - UI.rfb.set_true_color(UI.getSetting('true_color')); - UI.rfb.set_shared(UI.getSetting('shared')); - UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); + UI.rfb.set_encrypt(UI.getSetting('encrypt')); + UI.rfb.set_true_color(UI.getSetting('true_color')); + UI.rfb.set_shared(UI.getSetting('shared')); + UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); - UI.updateLocalCursor(); - UI.updateViewOnly(); + UI.updateLocalCursor(); + UI.updateViewOnly(); - UI.rfb.connect(host, port, password, path); - }, + UI.rfb.connect(host, port, password, path); + }, - disconnect: function() { - UI.closeAllPanels(); - UI.rfb.disconnect(); + disconnect: function() { + UI.closeAllPanels(); + UI.rfb.disconnect(); - // Disable automatic reconnecting - UI.inhibit_reconnect = true; + // Disable automatic reconnecting + UI.inhibit_reconnect = true; - // Restore the callback used for initial resize - UI.rfb.set_onFBUComplete(UI.initialResize); + // Restore the callback used for initial resize + UI.rfb.set_onFBUComplete(UI.initialResize); - // Don't display the connection settings until we're actually disconnected - }, + // Don't display the connection settings until we're actually disconnected + }, - reconnect: function() { + reconnect: function() { + UI.reconnect_callback = null; + + // if reconnect has been disabled in the meantime, do nothing. + if (UI.inhibit_reconnect) { + return; + } + + UI.connect(null, UI.reconnect_password); + }, + + disconnectFinished: function (rfb, reason) { + if (typeof reason !== 'undefined') { + UI.showStatus(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"); + + var delay = parseInt(UI.getSetting('reconnect_delay')); + UI.reconnect_callback = setTimeout(UI.reconnect, delay); + return; + } + + UI.openControlbar(); + UI.openConnectPanel(); + }, + + cancelReconnect: function() { + if (UI.reconnect_callback !== null) { + clearTimeout(UI.reconnect_callback); UI.reconnect_callback = null; + } - // if reconnect has been disabled in the meantime, do nothing. - if (UI.inhibit_reconnect) { - return; - } - - UI.connect(null, UI.reconnect_password); - }, - - disconnectFinished: function (rfb, reason) { - if (typeof reason !== 'undefined') { - UI.showStatus(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"); - - var delay = parseInt(UI.getSetting('reconnect_delay')); - UI.reconnect_callback = setTimeout(UI.reconnect, delay); - return; - } - - UI.openControlbar(); - UI.openConnectPanel(); - }, - - cancelReconnect: function() { - if (UI.reconnect_callback !== null) { - clearTimeout(UI.reconnect_callback); - UI.reconnect_callback = null; - } - - document.documentElement.classList.remove("noVNC_reconnecting"); - UI.openControlbar(); - UI.openConnectPanel(); - }, + document.documentElement.classList.remove("noVNC_reconnecting"); + UI.openControlbar(); + UI.openConnectPanel(); + }, /* ------^------- * /CONNECTION @@ -1153,31 +1156,31 @@ var UI; * PASSWORD * ------v------*/ - passwordRequired: function(rfb, msg) { + passwordRequired: function(rfb, msg) { - document.getElementById('noVNC_password_dlg') - .classList.add('noVNC_open'); + document.getElementById('noVNC_password_dlg') + .classList.add('noVNC_open'); - setTimeout(function () { - document.getElementById('noVNC_password_input').focus(); - }, 100); + setTimeout(function () { + document.getElementById('noVNC_password_input').focus(); + }, 100); - if (typeof msg === 'undefined') { - msg = _("Password is required"); - } - Util.Warn(msg); - UI.showStatus(msg, "warning"); - }, + if (typeof msg === 'undefined') { + msg = _("Password is required"); + } + Log.Warn(msg); + UI.showStatus(msg, "warning"); + }, - setPassword: function(e) { - var password = document.getElementById('noVNC_password_input').value; - UI.rfb.sendPassword(password); - UI.reconnect_password = password; - document.getElementById('noVNC_password_dlg') - .classList.remove('noVNC_open'); - // Prevent actually submitting the form - e.preventDefault(); - }, + setPassword: function(e) { + var password = document.getElementById('noVNC_password_input').value; + UI.rfb.sendPassword(password); + UI.reconnect_password = password; + document.getElementById('noVNC_password_dlg') + .classList.remove('noVNC_open'); + // Prevent actually submitting the form + e.preventDefault(); + }, /* ------^------- * /PASSWORD @@ -1185,47 +1188,47 @@ var UI; * FULLSCREEN * ------v------*/ - toggleFullscreen: function() { - if (document.fullscreenElement || // alternative standard method - document.mozFullScreenElement || // currently working methods - document.webkitFullscreenElement || - document.msFullscreenElement) { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); - } - } else { - if (document.documentElement.requestFullscreen) { - document.documentElement.requestFullscreen(); - } else if (document.documentElement.mozRequestFullScreen) { - document.documentElement.mozRequestFullScreen(); - } else if (document.documentElement.webkitRequestFullscreen) { - document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); - } else if (document.body.msRequestFullscreen) { - document.body.msRequestFullscreen(); - } + toggleFullscreen: function() { + if (document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); } - UI.enableDisableViewClip(); - UI.updateFullscreenButton(); - }, + } else { + if (document.documentElement.requestFullscreen) { + document.documentElement.requestFullscreen(); + } else if (document.documentElement.mozRequestFullScreen) { + document.documentElement.mozRequestFullScreen(); + } else if (document.documentElement.webkitRequestFullscreen) { + document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (document.body.msRequestFullscreen) { + document.body.msRequestFullscreen(); + } + } + UI.enableDisableViewClip(); + UI.updateFullscreenButton(); + }, - updateFullscreenButton: function() { - if (document.fullscreenElement || // alternative standard method - document.mozFullScreenElement || // currently working methods - document.webkitFullscreenElement || - document.msFullscreenElement ) { - document.getElementById('noVNC_fullscreen_button') - .classList.add("noVNC_selected"); - } else { - document.getElementById('noVNC_fullscreen_button') - .classList.remove("noVNC_selected"); - } - }, + updateFullscreenButton: function() { + if (document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement ) { + document.getElementById('noVNC_fullscreen_button') + .classList.add("noVNC_selected"); + } else { + document.getElementById('noVNC_fullscreen_button') + .classList.remove("noVNC_selected"); + } + }, /* ------^------- * /FULLSCREEN @@ -1233,62 +1236,62 @@ var UI; * RESIZE * ------v------*/ - // Apply remote resizing or local scaling - applyResizeMode: function() { - if (!UI.rfb) return; + // Apply remote resizing or local scaling + applyResizeMode: function() { + if (!UI.rfb) return; - var screen = UI.screenSize(); + var screen = UI.screenSize(); - if (screen && UI.connected && UI.rfb.get_display()) { + if (screen && UI.connected && UI.rfb.get_display()) { - var display = UI.rfb.get_display(); - var resizeMode = UI.getSetting('resize'); - display.set_scale(1); + var display = UI.rfb.get_display(); + var resizeMode = UI.getSetting('resize'); + display.set_scale(1); - // Make sure the viewport is adjusted first - UI.updateViewClip(); + // Make sure the viewport is adjusted first + UI.updateViewClip(); - if (resizeMode === 'remote') { + if (resizeMode === 'remote') { - // Request changing the resolution of the remote display to - // the size of the local browser viewport. + // Request changing the resolution of the remote display to + // the size of the local browser viewport. - // In order to not send multiple requests before the browser-resize - // is finished we wait 0.5 seconds before sending the request. - clearTimeout(UI.resizeTimeout); - UI.resizeTimeout = setTimeout(function(){ - // Request a remote size covering the viewport - if (UI.rfb.requestDesktopSize(screen.w, screen.h)) { - Util.Debug('Requested new desktop size: ' + - screen.w + 'x' + screen.h); - } - }, 500); + // In order to not send multiple requests before the browser-resize + // is finished we wait 0.5 seconds before sending the request. + clearTimeout(UI.resizeTimeout); + UI.resizeTimeout = setTimeout(function(){ + // Request a remote size covering the viewport + if (UI.rfb.requestDesktopSize(screen.w, screen.h)) { + Log.Debug('Requested new desktop size: ' + + screen.w + 'x' + screen.h); + } + }, 500); - } else if (resizeMode === 'scale' || resizeMode === 'downscale') { - var downscaleOnly = resizeMode === 'downscale'; - display.autoscale(screen.w, screen.h, downscaleOnly); - UI.fixScrollbars(); - } + } else if (resizeMode === 'scale' || resizeMode === 'downscale') { + var downscaleOnly = resizeMode === 'downscale'; + display.autoscale(screen.w, screen.h, downscaleOnly); + UI.fixScrollbars(); } - }, + } + }, - // Gets the the size of the available viewport in the browser window - screenSize: function() { - var screen = document.getElementById('noVNC_screen'); - return {w: screen.offsetWidth, h: screen.offsetHeight}; - }, + // Gets the the size of the available viewport in the browser window + screenSize: function() { + var screen = document.getElementById('noVNC_screen'); + return {w: screen.offsetWidth, h: screen.offsetHeight}; + }, - // 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) { - UI.applyResizeMode(); - // After doing this once, we remove the callback. - UI.rfb.set_onFBUComplete(function() { }); - }, + // 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) { + UI.applyResizeMode(); + // After doing this once, we remove the callback. + UI.rfb.set_onFBUComplete(function() { }); + }, /* ------^------- * /RESIZE @@ -1296,58 +1299,58 @@ var UI; * CLIPPING * ------v------*/ - // Set and configure viewport clipping - setViewClip: function(clip) { - UI.updateSetting('clip', clip); - UI.updateViewClip(); - }, + // Set and configure viewport clipping + setViewClip: function(clip) { + UI.updateSetting('clip', clip); + UI.updateViewClip(); + }, - // Update parameters that depend on the clip setting - updateViewClip: function() { - if (!UI.rfb) return; + // Update parameters that depend on the clip setting + updateViewClip: function() { + if (!UI.rfb) return; - var display = UI.rfb.get_display(); - var cur_clip = display.get_viewport(); - var new_clip = UI.getSetting('clip'); + var display = UI.rfb.get_display(); + var cur_clip = display.get_viewport(); + var new_clip = UI.getSetting('clip'); - var resizeSetting = UI.getSetting('resize'); - if (resizeSetting === 'downscale' || resizeSetting === 'scale') { - // Disable clipping if we are scaling - new_clip = false; - } else if (Util.isTouchDevice) { - // Touch devices usually have shit scrollbars - new_clip = true; - } + var resizeSetting = UI.getSetting('resize'); + if (resizeSetting === 'downscale' || resizeSetting === 'scale') { + // Disable clipping if we are scaling + new_clip = false; + } else if (isTouchDevice) { + // Touch devices usually have shit scrollbars + new_clip = true; + } - if (cur_clip !== new_clip) { - display.set_viewport(new_clip); - } + if (cur_clip !== new_clip) { + display.set_viewport(new_clip); + } - var size = UI.screenSize(); + var size = UI.screenSize(); - 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.fixScrollbars(); - } + 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.fixScrollbars(); + } - // Changing the viewport may change the state of - // the dragging button - UI.updateViewDrag(); - }, + // Changing the viewport may change the state of + // the dragging button + UI.updateViewDrag(); + }, - // Handle special cases where clipping is forced on/off or locked - enableDisableViewClip: function() { - var resizeSetting = UI.getSetting('resize'); - // Disable clipping if we are scaling, connected or on touch - if (resizeSetting === 'downscale' || resizeSetting === 'scale' || - Util.isTouchDevice) { - UI.disableSetting('clip'); - } else { - UI.enableSetting('clip'); - } - }, + // Handle special cases where clipping is forced on/off or locked + enableDisableViewClip: function() { + var resizeSetting = UI.getSetting('resize'); + // Disable clipping if we are scaling, connected or on touch + if (resizeSetting === 'downscale' || resizeSetting === 'scale' || + isTouchDevice) { + UI.disableSetting('clip'); + } else { + UI.enableSetting('clip'); + } + }, /* ------^------- * /CLIPPING @@ -1355,70 +1358,70 @@ var UI; * VIEWDRAG * ------v------*/ - toggleViewDrag: function() { - if (!UI.rfb) return; + toggleViewDrag: function() { + if (!UI.rfb) return; - var drag = UI.rfb.get_viewportDrag(); - UI.setViewDrag(!drag); - }, + var drag = UI.rfb.get_viewportDrag(); + UI.setViewDrag(!drag); + }, - // Set the view drag mode which moves the viewport on mouse drags - setViewDrag: function(drag) { - if (!UI.rfb) return; + // Set the view drag mode which moves the viewport on mouse drags + setViewDrag: function(drag) { + if (!UI.rfb) return; - UI.rfb.set_viewportDrag(drag); + UI.rfb.set_viewportDrag(drag); - UI.updateViewDrag(); - }, + UI.updateViewDrag(); + }, - updateViewDrag: function() { - var clipping = false; + updateViewDrag: function() { + var clipping = false; - if (!UI.connected) return; + if (!UI.connected) return; - // 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()) { - clipping = true; - } + // 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()) { + clipping = true; + } - var viewDragButton = document.getElementById('noVNC_view_drag_button'); + var viewDragButton = document.getElementById('noVNC_view_drag_button'); - if (!clipping && - UI.rfb.get_viewportDrag()) { - // 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); - } + if (!clipping && + UI.rfb.get_viewportDrag()) { + // 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); + } - if (UI.rfb.get_viewportDrag()) { - viewDragButton.classList.add("noVNC_selected"); - } else { - viewDragButton.classList.remove("noVNC_selected"); - } + if (UI.rfb.get_viewportDrag()) { + viewDragButton.classList.add("noVNC_selected"); + } else { + viewDragButton.classList.remove("noVNC_selected"); + } - // Different behaviour for touch vs non-touch - // The button is disabled instead of hidden on touch devices - if (Util.isTouchDevice) { - viewDragButton.classList.remove("noVNC_hidden"); + // Different behaviour for touch vs non-touch + // The button is disabled instead of hidden on touch devices + if (isTouchDevice) { + viewDragButton.classList.remove("noVNC_hidden"); - if (clipping) { - viewDragButton.disabled = false; - } else { - viewDragButton.disabled = true; - } - } else { + if (clipping) { viewDragButton.disabled = false; - - if (clipping) { - viewDragButton.classList.remove("noVNC_hidden"); - } else { - viewDragButton.classList.add("noVNC_hidden"); - } + } else { + viewDragButton.disabled = true; } - }, + } else { + viewDragButton.disabled = false; + + if (clipping) { + viewDragButton.classList.remove("noVNC_hidden"); + } else { + viewDragButton.classList.add("noVNC_hidden"); + } + } + }, /* ------^------- * /VIEWDRAG @@ -1426,155 +1429,155 @@ var UI; * KEYBOARD * ------v------*/ - showVirtualKeyboard: function() { - if (!Util.isTouchDevice) return; + showVirtualKeyboard: function() { + if (!isTouchDevice) return; - var input = document.getElementById('noVNC_keyboardinput'); + var input = document.getElementById('noVNC_keyboardinput'); - if (document.activeElement == input) return; + if (document.activeElement == input) return; - input.focus(); + input.focus(); - try { - var l = input.value.length; - // Move the caret to the end - input.setSelectionRange(l, l); - } catch (err) {} // setSelectionRange is undefined in Google Chrome - }, + try { + var l = input.value.length; + // Move the caret to the end + input.setSelectionRange(l, l); + } catch (err) {} // setSelectionRange is undefined in Google Chrome + }, - hideVirtualKeyboard: function() { - if (!Util.isTouchDevice) return; + hideVirtualKeyboard: function() { + if (!isTouchDevice) return; - var input = document.getElementById('noVNC_keyboardinput'); + var input = document.getElementById('noVNC_keyboardinput'); - if (document.activeElement != input) return; + if (document.activeElement != input) return; - input.blur(); - }, + input.blur(); + }, - toggleVirtualKeyboard: function () { - if (document.getElementById('noVNC_keyboard_button') - .classList.contains("noVNC_selected")) { - UI.hideVirtualKeyboard(); - } else { - UI.showVirtualKeyboard(); + toggleVirtualKeyboard: function () { + if (document.getElementById('noVNC_keyboard_button') + .classList.contains("noVNC_selected")) { + UI.hideVirtualKeyboard(); + } else { + UI.showVirtualKeyboard(); + } + }, + + onfocusVirtualKeyboard: function(event) { + document.getElementById('noVNC_keyboard_button') + .classList.add("noVNC_selected"); + }, + + onblurVirtualKeyboard: function(event) { + document.getElementById('noVNC_keyboard_button') + .classList.remove("noVNC_selected"); + }, + + keepVirtualKeyboard: function(event) { + var input = document.getElementById('noVNC_keyboardinput'); + + // Only prevent focus change if the virtual keyboard is active + if (document.activeElement != input) { + return; + } + + // Only allow focus to move to other elements that need + // focus to function properly + if (event.target.form !== undefined) { + switch (event.target.type) { + case 'text': + case 'email': + case 'search': + case 'password': + case 'tel': + case 'url': + case 'textarea': + case 'select-one': + case 'select-multiple': + return; } - }, + } - onfocusVirtualKeyboard: function(event) { - document.getElementById('noVNC_keyboard_button') - .classList.add("noVNC_selected"); - }, + event.preventDefault(); + }, - onblurVirtualKeyboard: function(event) { - document.getElementById('noVNC_keyboard_button') - .classList.remove("noVNC_selected"); - }, + keyboardinputReset: function() { + var kbi = document.getElementById('noVNC_keyboardinput'); + kbi.value = new Array(UI.defaultKeyboardinputLen).join("_"); + UI.lastKeyboardinput = kbi.value; + }, - keepVirtualKeyboard: function(event) { - var input = document.getElementById('noVNC_keyboardinput'); + // When normal keyboard events are left uncought, use the input events from + // the keyboardinput element instead and generate the corresponding key events. + // This code is required since some browsers on Android are inconsistent in + // sending keyCodes in the normal keyboard events when using on screen keyboards. + keyInput: function(event) { - // Only prevent focus change if the virtual keyboard is active - if (document.activeElement != input) { - return; + if (!UI.rfb) return; + + var newValue = event.target.value; + + if (!UI.lastKeyboardinput) { + UI.keyboardinputReset(); + } + var oldValue = UI.lastKeyboardinput; + + var newLen; + try { + // Try to check caret position since whitespace at the end + // will not be considered by value.length in some browsers + newLen = Math.max(event.target.selectionStart, newValue.length); + } catch (err) { + // selectionStart is undefined in Google Chrome + newLen = newValue.length; + } + var oldLen = oldValue.length; + + var backspaces; + var inputs = newLen - oldLen; + if (inputs < 0) { + backspaces = -inputs; + } else { + backspaces = 0; + } + + // Compare the old string with the new to account for + // text-corrections or other input that modify existing text + var i; + for (i = 0; i < Math.min(oldLen, newLen); i++) { + if (newValue.charAt(i) != oldValue.charAt(i)) { + inputs = newLen - i; + backspaces = oldLen - i; + break; } + } - // Only allow focus to move to other elements that need - // focus to function properly - if (event.target.form !== undefined) { - switch (event.target.type) { - case 'text': - case 'email': - case 'search': - case 'password': - case 'tel': - case 'url': - case 'textarea': - case 'select-one': - case 'select-multiple': - return; - } - } + // Send the key events + for (i = 0; i < backspaces; i++) { + UI.rfb.sendKey(KeyTable.XK_BackSpace); + } + for (i = newLen - inputs; i < newLen; i++) { + UI.rfb.sendKey(keysyms.fromUnicode(newValue.charCodeAt(i)).keysym); + } - event.preventDefault(); - }, - - keyboardinputReset: function() { - var kbi = document.getElementById('noVNC_keyboardinput'); - kbi.value = new Array(UI.defaultKeyboardinputLen).join("_"); - UI.lastKeyboardinput = kbi.value; - }, - - // When normal keyboard events are left uncought, use the input events from - // the keyboardinput element instead and generate the corresponding key events. - // This code is required since some browsers on Android are inconsistent in - // sending keyCodes in the normal keyboard events when using on screen keyboards. - keyInput: function(event) { - - if (!UI.rfb) return; - - var newValue = event.target.value; - - if (!UI.lastKeyboardinput) { - UI.keyboardinputReset(); - } - var oldValue = UI.lastKeyboardinput; - - var newLen; - try { - // Try to check caret position since whitespace at the end - // will not be considered by value.length in some browsers - newLen = Math.max(event.target.selectionStart, newValue.length); - } catch (err) { - // selectionStart is undefined in Google Chrome - newLen = newValue.length; - } - var oldLen = oldValue.length; - - var backspaces; - var inputs = newLen - oldLen; - if (inputs < 0) { - backspaces = -inputs; - } else { - backspaces = 0; - } - - // Compare the old string with the new to account for - // text-corrections or other input that modify existing text - var i; - for (i = 0; i < Math.min(oldLen, newLen); i++) { - if (newValue.charAt(i) != oldValue.charAt(i)) { - inputs = newLen - i; - backspaces = oldLen - i; - break; - } - } - - // Send the key events - for (i = 0; i < backspaces; i++) { - UI.rfb.sendKey(KeyTable.XK_BackSpace); - } - for (i = newLen - inputs; i < newLen; i++) { - UI.rfb.sendKey(keysyms.fromUnicode(newValue.charCodeAt(i)).keysym); - } - - // Control the text content length in the keyboardinput element - if (newLen > 2 * UI.defaultKeyboardinputLen) { - UI.keyboardinputReset(); - } else if (newLen < 1) { - // There always have to be some text in the keyboardinput - // element with which backspace can interact. - UI.keyboardinputReset(); - // This sometimes causes the keyboard to disappear for a second - // but it is required for the android keyboard to recognize that - // text has been added to the field - event.target.blur(); - // This has to be ran outside of the input handler in order to work - setTimeout(event.target.focus.bind(event.target), 0); - } else { - UI.lastKeyboardinput = newValue; - } - }, + // Control the text content length in the keyboardinput element + if (newLen > 2 * UI.defaultKeyboardinputLen) { + UI.keyboardinputReset(); + } else if (newLen < 1) { + // There always have to be some text in the keyboardinput + // element with which backspace can interact. + UI.keyboardinputReset(); + // This sometimes causes the keyboard to disappear for a second + // but it is required for the android keyboard to recognize that + // text has been added to the field + event.target.blur(); + // This has to be ran outside of the input handler in order to work + setTimeout(event.target.focus.bind(event.target), 0); + } else { + UI.lastKeyboardinput = newValue; + } + }, /* ------^------- * /KEYBOARD @@ -1582,65 +1585,65 @@ var UI; * EXTRA KEYS * ------v------*/ - openExtraKeys: function() { - UI.closeAllPanels(); - UI.openControlbar(); + openExtraKeys: function() { + UI.closeAllPanels(); + UI.openControlbar(); - document.getElementById('noVNC_modifiers') - .classList.add("noVNC_open"); - document.getElementById('noVNC_toggle_extra_keys_button') - .classList.add("noVNC_selected"); - }, + document.getElementById('noVNC_modifiers') + .classList.add("noVNC_open"); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.add("noVNC_selected"); + }, - closeExtraKeys: function() { - document.getElementById('noVNC_modifiers') - .classList.remove("noVNC_open"); - document.getElementById('noVNC_toggle_extra_keys_button') - .classList.remove("noVNC_selected"); - }, + closeExtraKeys: function() { + document.getElementById('noVNC_modifiers') + .classList.remove("noVNC_open"); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.remove("noVNC_selected"); + }, - toggleExtraKeys: function() { - if(document.getElementById('noVNC_modifiers') - .classList.contains("noVNC_open")) { - UI.closeExtraKeys(); - } else { - UI.openExtraKeys(); - } - }, + toggleExtraKeys: function() { + if(document.getElementById('noVNC_modifiers') + .classList.contains("noVNC_open")) { + UI.closeExtraKeys(); + } else { + UI.openExtraKeys(); + } + }, - sendEsc: function() { - UI.rfb.sendKey(KeyTable.XK_Escape); - }, + sendEsc: function() { + UI.rfb.sendKey(KeyTable.XK_Escape); + }, - sendTab: function() { - UI.rfb.sendKey(KeyTable.XK_Tab); - }, + sendTab: function() { + UI.rfb.sendKey(KeyTable.XK_Tab); + }, - toggleCtrl: function() { - var btn = document.getElementById('noVNC_toggle_ctrl_button'); - if (btn.classList.contains("noVNC_selected")) { - UI.rfb.sendKey(KeyTable.XK_Control_L, false); - btn.classList.remove("noVNC_selected"); - } else { - UI.rfb.sendKey(KeyTable.XK_Control_L, true); - btn.classList.add("noVNC_selected"); - } - }, + toggleCtrl: function() { + var btn = document.getElementById('noVNC_toggle_ctrl_button'); + if (btn.classList.contains("noVNC_selected")) { + UI.rfb.sendKey(KeyTable.XK_Control_L, false); + btn.classList.remove("noVNC_selected"); + } else { + UI.rfb.sendKey(KeyTable.XK_Control_L, true); + btn.classList.add("noVNC_selected"); + } + }, - toggleAlt: function() { - var btn = document.getElementById('noVNC_toggle_alt_button'); - if (btn.classList.contains("noVNC_selected")) { - UI.rfb.sendKey(KeyTable.XK_Alt_L, false); - btn.classList.remove("noVNC_selected"); - } else { - UI.rfb.sendKey(KeyTable.XK_Alt_L, true); - btn.classList.add("noVNC_selected"); - } - }, + toggleAlt: function() { + var btn = document.getElementById('noVNC_toggle_alt_button'); + if (btn.classList.contains("noVNC_selected")) { + UI.rfb.sendKey(KeyTable.XK_Alt_L, false); + btn.classList.remove("noVNC_selected"); + } else { + UI.rfb.sendKey(KeyTable.XK_Alt_L, true); + btn.classList.add("noVNC_selected"); + } + }, - sendCtrlAltDel: function() { - UI.rfb.sendCtrlAltDel(); - }, + sendCtrlAltDel: function() { + UI.rfb.sendCtrlAltDel(); + }, /* ------^------- * /EXTRA KEYS @@ -1648,108 +1651,108 @@ var UI; * MISC * ------v------*/ - setMouseButton: function(num) { - var view_only = UI.rfb.get_view_only(); - if (UI.rfb && !view_only) { - UI.rfb.get_mouse().set_touchButton(num); + setMouseButton: function(num) { + var view_only = UI.rfb.get_view_only(); + if (UI.rfb && !view_only) { + UI.rfb.get_mouse().set_touchButton(num); + } + + var blist = [0, 1,2,4]; + for (var b = 0; b < blist.length; b++) { + var button = document.getElementById('noVNC_mouse_button' + + blist[b]); + if (blist[b] === num && !view_only) { + button.classList.remove("noVNC_hidden"); + } else { + button.classList.add("noVNC_hidden"); } + } + }, - var blist = [0, 1,2,4]; - for (var b = 0; b < blist.length; b++) { - var button = document.getElementById('noVNC_mouse_button' + - blist[b]); - if (blist[b] === num && !view_only) { - button.classList.remove("noVNC_hidden"); - } else { - button.classList.add("noVNC_hidden"); - } - } - }, + displayBlur: function() { + if (UI.rfb && !UI.rfb.get_view_only()) { + UI.rfb.get_keyboard().set_focused(false); + UI.rfb.get_mouse().set_focused(false); + } + }, - displayBlur: function() { - if (UI.rfb && !UI.rfb.get_view_only()) { - UI.rfb.get_keyboard().set_focused(false); - UI.rfb.get_mouse().set_focused(false); - } - }, + displayFocus: function() { + if (UI.rfb && !UI.rfb.get_view_only()) { + UI.rfb.get_keyboard().set_focused(true); + UI.rfb.get_mouse().set_focused(true); + } + }, - displayFocus: function() { - if (UI.rfb && !UI.rfb.get_view_only()) { - UI.rfb.get_keyboard().set_focused(true); - UI.rfb.get_mouse().set_focused(true); - } - }, + updateLocalCursor: function() { + UI.rfb.set_local_cursor(UI.getSetting('cursor')); + }, - updateLocalCursor: function() { - UI.rfb.set_local_cursor(UI.getSetting('cursor')); - }, + updateViewOnly: function() { + UI.rfb.set_view_only(UI.getSetting('view_only')); + }, - updateViewOnly: function() { - UI.rfb.set_view_only(UI.getSetting('view_only')); - }, + updateLogging: function() { + WebUtil.init_logging(UI.getSetting('logging')); + }, - updateLogging: function() { - WebUtil.init_logging(UI.getSetting('logging')); - }, + updateSessionSize: function(rfb, width, height) { + UI.updateViewClip(); + UI.fixScrollbars(); + }, - updateSessionSize: function(rfb, width, height) { - UI.updateViewClip(); - UI.fixScrollbars(); - }, + fixScrollbars: function() { + // This is a hack because Chrome screws up the calculation + // for when scrollbars are needed. So to fix it we temporarily + // toggle them off and on. + var screen = document.getElementById('noVNC_screen'); + screen.style.overflow = 'hidden'; + // Force Chrome to recalculate the layout by asking for + // an element's dimensions + screen.getBoundingClientRect(); + screen.style.overflow = null; + }, - fixScrollbars: function() { - // This is a hack because Chrome screws up the calculation - // for when scrollbars are needed. So to fix it we temporarily - // toggle them off and on. - var screen = document.getElementById('noVNC_screen'); - screen.style.overflow = 'hidden'; - // Force Chrome to recalculate the layout by asking for - // an element's dimensions - screen.getBoundingClientRect(); - screen.style.overflow = null; - }, + updateDesktopName: function(rfb, name) { + UI.desktopName = name; + // Display the desktop name in the document title + document.title = name + " - noVNC"; + }, - updateDesktopName: function(rfb, name) { - UI.desktopName = name; - // Display the desktop name in the document title - document.title = name + " - noVNC"; - }, + bell: function(rfb) { + if (WebUtil.getConfigVar('bell', 'on') === 'on') { + document.getElementById('noVNC_bell').play(); + } + }, - bell: function(rfb) { - if (WebUtil.getConfigVar('bell', 'on') === 'on') { - document.getElementById('noVNC_bell').play(); - } - }, - - //Helper to add options to dropdown. - addOption: function(selectbox, text, value) { - var optn = document.createElement("OPTION"); - optn.text = text; - optn.value = value; - selectbox.options.add(optn); - }, + //Helper to add options to dropdown. + addOption: function(selectbox, text, value) { + var optn = document.createElement("OPTION"); + optn.text = text; + optn.value = value; + selectbox.options.add(optn); + }, /* ------^------- * /MISC * ============== */ - }; +}; - // Set up translations - var LINGUAS = ["de", "el", "nl", "sv"]; - Util.Localisation.setup(LINGUAS); - if (Util.Localisation.language !== "en" && Util.Localisation.dictionary === undefined) { - WebUtil.fetchJSON('app/locale/' + Util.Localisation.language + '.json', function (translations) { - Util.Localisation.dictionary = translations; +// Set up translations +var LINGUAS = ["de", "el", "nl", "sv"]; +l10n.setup(LINGUAS); +if (l10n.language !== "en" && l10n.dictionary === undefined) { + WebUtil.fetchJSON('app/locale/' + l10n.language + '.json', function (translations) { + l10n.dictionary = translations; + + // wait for translations to load before loading the UI + UI.prime(); + }, function (err) { + throw err; + }); +} else { + UI.prime(); +} - // wait for translations to load before loading the UI - UI.load(); - }, function (err) { - throw err; - }); - } else { - UI.load(); - } -})(); export default UI; diff --git a/app/webutil.js b/app/webutil.js index 2ce1b2d3..7cee466c 100644 --- a/app/webutil.js +++ b/app/webutil.js @@ -10,31 +10,21 @@ /*jslint bitwise: false, white: false, browser: true, devel: true */ /*global Util, window, document */ -import Util from "../core/util.js"; - -// Globals defined here -var WebUtil = {}; - -/* - * ------------------------------------------------------ - * Namespaced in WebUtil - * ------------------------------------------------------ - */ +import { init_logging as main_init_logging } from '../core/util/logging.js'; // init log level reading the logging HTTP param -WebUtil.init_logging = function (level) { +export function init_logging (level) { "use strict"; if (typeof level !== "undefined") { - Util._log_level = level; + main_init_logging(level); } else { var param = document.location.href.match(/logging=([A-Za-z0-9\._\-]*)/); - Util._log_level = (param || ['', Util._log_level])[1]; + main_init_logging(param || undefined); } - Util.init_logging(); }; // Read a query string variable -WebUtil.getQueryVar = function (name, defVal) { +export function getQueryVar (name, defVal) { "use strict"; var re = new RegExp('.*[?&]' + name + '=([^&#]*)'), match = document.location.href.match(re); @@ -47,7 +37,7 @@ WebUtil.getQueryVar = function (name, defVal) { }; // Read a hash fragment variable -WebUtil.getHashVar = function (name, defVal) { +export function getHashVar (name, defVal) { "use strict"; var re = new RegExp('.*[&#]' + name + '=([^&]*)'), match = document.location.hash.match(re); @@ -61,11 +51,11 @@ WebUtil.getHashVar = function (name, defVal) { // Read a variable from the fragment or the query string // Fragment takes precedence -WebUtil.getConfigVar = function (name, defVal) { +export function getConfigVar (name, defVal) { "use strict"; - var val = WebUtil.getHashVar(name); + var val = getHashVar(name); if (val === null) { - val = WebUtil.getQueryVar(name, defVal); + val = getQueryVar(name, defVal); } return val; }; @@ -75,7 +65,7 @@ WebUtil.getConfigVar = function (name, defVal) { */ // No days means only for this browser session -WebUtil.createCookie = function (name, value, days) { +export function createCookie (name, value, days) { "use strict"; var date, expires; if (days) { @@ -95,7 +85,7 @@ WebUtil.createCookie = function (name, value, days) { document.cookie = name + "=" + value + expires + "; path=/" + secure; }; -WebUtil.readCookie = function (name, defaultValue) { +export function readCookie (name, defaultValue) { "use strict"; var nameEQ = name + "=", ca = document.cookie.split(';'); @@ -108,22 +98,24 @@ WebUtil.readCookie = function (name, defaultValue) { return (typeof defaultValue !== 'undefined') ? defaultValue : null; }; -WebUtil.eraseCookie = function (name) { +export function eraseCookie (name) { "use strict"; - WebUtil.createCookie(name, "", -1); + createCookie(name, "", -1); }; /* * Setting handling. */ -WebUtil.initSettings = function (callback /*, ...callbackArgs */) { +var settings = {}; + +export function initSettings (callback /*, ...callbackArgs */) { "use strict"; var callbackArgs = Array.prototype.slice.call(arguments, 1); if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.get(function (cfg) { - WebUtil.settings = cfg; - console.log(WebUtil.settings); + settings = cfg; + console.log(settings); if (callback) { callback.apply(this, callbackArgs); } @@ -137,24 +129,24 @@ WebUtil.initSettings = function (callback /*, ...callbackArgs */) { }; // No days means only for this browser session -WebUtil.writeSetting = function (name, value) { +export function writeSetting (name, value) { "use strict"; if (window.chrome && window.chrome.storage) { //console.log("writeSetting:", name, value); - if (WebUtil.settings[name] !== value) { - WebUtil.settings[name] = value; - window.chrome.storage.sync.set(WebUtil.settings); + if (settings[name] !== value) { + settings[name] = value; + window.chrome.storage.sync.set(settings); } } else { localStorage.setItem(name, value); } }; -WebUtil.readSetting = function (name, defaultValue) { +export function readSetting (name, defaultValue) { "use strict"; var value; if (window.chrome && window.chrome.storage) { - value = WebUtil.settings[name]; + value = settings[name]; } else { value = localStorage.getItem(name); } @@ -168,17 +160,17 @@ WebUtil.readSetting = function (name, defaultValue) { } }; -WebUtil.eraseSetting = function (name) { +export function eraseSetting (name) { "use strict"; if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.remove(name); - delete WebUtil.settings[name]; + delete settings[name]; } else { localStorage.removeItem(name); } }; -WebUtil.injectParamIfMissing = function (path, param, value) { +export function injectParamIfMissing (path, param, value) { // force pretend that we're dealing with a relative path // (assume that we wanted an extra if we pass one in) path = "/" + path; @@ -212,7 +204,7 @@ WebUtil.injectParamIfMissing = function (path, param, value) { // IE11 support or polyfill promises and fetch in IE11. // resolve will receive an object on success, while reject // will receive either an event or an error on failure. -WebUtil.fetchJSON = function (path, resolve, reject) { +export function fetchJSON(path, resolve, reject) { // NB: IE11 doesn't support JSON as a responseType const req = new XMLHttpRequest(); req.open('GET', path); @@ -240,6 +232,4 @@ WebUtil.fetchJSON = function (path, resolve, reject) { }; req.send(); -}; - -export default WebUtil; +} diff --git a/core/base64.js b/core/base64.js index 3b9ebe54..9d24a25f 100644 --- a/core/base64.js +++ b/core/base64.js @@ -7,7 +7,7 @@ /*jslint white: false */ /*global console */ -var Base64 = { +export default { /* Convert data (an array of integers) to a Base64 string. */ toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), base64Pad : '=', @@ -15,7 +15,7 @@ var Base64 = { encode: function (data) { "use strict"; var result = ''; - var toBase64Table = Base64.toBase64Table; + var toBase64Table = this.toBase64Table; var length = data.length; var lengthpad = (length % 3); // Convert every three bytes to 4 ascii characters. @@ -63,8 +63,8 @@ var Base64 = { decode: function (data, offset) { "use strict"; offset = typeof(offset) !== 'undefined' ? offset : 0; - var toBinaryTable = Base64.toBinaryTable; - var base64Pad = Base64.base64Pad; + var toBinaryTable = this.toBinaryTable; + var base64Pad = this.base64Pad; var result, result_length; var leftbits = 0; // number of bits decoded, but yet to be appended var leftdata = 0; // bits decoded, but yet to be appended @@ -111,5 +111,3 @@ var Base64 = { return result; } }; /* End of Base64 namespace */ - -export default Base64; diff --git a/core/display.js b/core/display.js index a1dac7a4..818c911d 100644 --- a/core/display.js +++ b/core/display.js @@ -10,10 +10,11 @@ /*jslint browser: true, white: false */ /*global Util, Base64, changeCursor */ -import Util from "./util.js"; +import { Engine, 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) { this._drawCtx = null; this._c_forceCanvas = false; @@ -31,7 +32,7 @@ export default function Display(defaults) { this._tile_x = 0; this._tile_y = 0; - Util.set_defaults(this, defaults, { + set_defaults(this, defaults, { 'true_color': true, 'colourMap': [], 'scale': 1.0, @@ -40,7 +41,7 @@ export default function Display(defaults) { "onFlush": function () {}, }); - Util.Debug(">> Display.constructor"); + Log.Debug(">> Display.constructor"); // The visible canvas if (!this._target) { @@ -68,11 +69,11 @@ export default function Display(defaults) { right: this._backbuffer.width, bottom: this._backbuffer.height }; - Util.Debug("User Agent: " + navigator.userAgent); - if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); } - if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); } - if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); } - if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); } + Log.Debug("User Agent: " + navigator.userAgent); + if (Engine.gecko) { Log.Debug("Browser: gecko " + Engine.gecko); } + if (Engine.webkit) { Log.Debug("Browser: webkit " + Engine.webkit); } + if (Engine.trident) { Log.Debug("Browser: trident " + Engine.trident); } + if (Engine.presto) { Log.Debug("Browser: presto " + Engine.presto); } this.clear(); @@ -84,788 +85,783 @@ export default function Display(defaults) { } if (this._prefer_js === null) { - Util.Info("Prefering javascript operations"); + 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 = Util.browserSupportsCursorURIs(); + this._cursor_uri = cursorURIsSupported(); } - Util.Debug("<< Display.constructor"); + Log.Debug("<< Display.constructor"); }; -(function () { - "use strict"; +var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false; +try { + new ImageData(new Uint8ClampedArray(4), 1, 1); + SUPPORTS_IMAGEDATA_CONSTRUCTOR = true; +} catch (ex) { + // ignore failure +} - var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false; - try { - new ImageData(new Uint8ClampedArray(4), 1, 1); - SUPPORTS_IMAGEDATA_CONSTRUCTOR = true; - } catch (ex) { - // ignore failure - } +Display.prototype = { + // Public methods + viewportChangePos: function (deltaX, deltaY) { + var vp = this._viewportLoc; + deltaX = Math.floor(deltaX); + deltaY = Math.floor(deltaY); + if (!this._viewport) { + deltaX = -vp.w; // clamped later of out of bounds + deltaY = -vp.h; + } - Display.prototype = { - // Public methods - viewportChangePos: function (deltaX, deltaY) { - var vp = this._viewportLoc; - deltaX = Math.floor(deltaX); - deltaY = Math.floor(deltaY); + var vx2 = vp.x + vp.w - 1; + var vy2 = vp.y + vp.h - 1; - if (!this._viewport) { - deltaX = -vp.w; // clamped later of out of bounds - deltaY = -vp.h; - } + // Position change - var vx2 = vp.x + vp.w - 1; - var vy2 = vp.y + vp.h - 1; + if (deltaX < 0 && vp.x + deltaX < 0) { + deltaX = -vp.x; + } + if (vx2 + deltaX >= this._fb_width) { + deltaX -= vx2 + deltaX - this._fb_width + 1; + } - // Position change + if (vp.y + deltaY < 0) { + deltaY = -vp.y; + } + if (vy2 + deltaY >= this._fb_height) { + deltaY -= (vy2 + deltaY - this._fb_height + 1); + } - if (deltaX < 0 && vp.x + deltaX < 0) { - deltaX = -vp.x; - } - if (vx2 + deltaX >= this._fb_width) { - deltaX -= vx2 + deltaX - this._fb_width + 1; - } + if (deltaX === 0 && deltaY === 0) { + return; + } + Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); - if (vp.y + deltaY < 0) { - deltaY = -vp.y; - } - if (vy2 + deltaY >= this._fb_height) { - deltaY -= (vy2 + deltaY - this._fb_height + 1); - } + vp.x += deltaX; + vp.y += deltaY; - if (deltaX === 0 && deltaY === 0) { - return; - } - Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); + this._damage(vp.x, vp.y, vp.w, vp.h); - vp.x += deltaX; - vp.y += deltaY; + this.flip(); + }, + + viewportChangeSize: function(width, height) { + + if (!this._viewport || + typeof(width) === "undefined" || + typeof(height) === "undefined") { + + Log.Debug("Setting viewport to full display region"); + width = this._fb_width; + height = this._fb_height; + } + + if (width > this._fb_width) { + width = this._fb_width; + } + if (height > this._fb_height) { + height = this._fb_height; + } + + var vp = this._viewportLoc; + if (vp.w !== width || vp.h !== height) { + vp.w = width; + vp.h = height; + + var canvas = this._target; + canvas.width = width; + canvas.height = height; + + // The position might need to be updated if we've grown + this.viewportChangePos(0, 0); this._damage(vp.x, vp.y, vp.w, vp.h); - this.flip(); - }, - viewportChangeSize: function(width, height) { + // Update the visible size of the target canvas + this._rescale(this._scale); + } + }, - if (!this._viewport || - typeof(width) === "undefined" || - typeof(height) === "undefined") { + absX: function (x) { + return x / this._scale + this._viewportLoc.x; + }, - Util.Debug("Setting viewport to full display region"); - width = this._fb_width; - height = this._fb_height; + absY: function (y) { + return y / this._scale + this._viewportLoc.y; + }, + + resize: function (width, height) { + this._prevDrawStyle = ""; + + this._fb_width = width; + this._fb_height = height; + + var canvas = this._backbuffer; + if (canvas.width !== width || canvas.height !== height) { + + // We have to save the canvas data since changing the size will clear it + var saveImg = null; + if (canvas.width > 0 && canvas.height > 0) { + saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height); } - if (width > this._fb_width) { - width = this._fb_width; - } - if (height > this._fb_height) { - height = this._fb_height; - } - - var vp = this._viewportLoc; - if (vp.w !== width || vp.h !== height) { - vp.w = width; - vp.h = height; - - var canvas = this._target; + if (canvas.width !== width) { canvas.width = width; + } + if (canvas.height !== height) { canvas.height = height; - - // The position might need to be updated if we've grown - this.viewportChangePos(0, 0); - - this._damage(vp.x, vp.y, vp.w, vp.h); - this.flip(); - - // Update the visible size of the target canvas - this._rescale(this._scale); - } - }, - - absX: function (x) { - return x / this._scale + this._viewportLoc.x; - }, - - absY: function (y) { - return y / this._scale + this._viewportLoc.y; - }, - - resize: function (width, height) { - this._prevDrawStyle = ""; - - this._fb_width = width; - this._fb_height = height; - - var canvas = this._backbuffer; - if (canvas.width !== width || canvas.height !== height) { - - // We have to save the canvas data since changing the size will clear it - var saveImg = null; - if (canvas.width > 0 && canvas.height > 0) { - saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height); - } - - if (canvas.width !== width) { - canvas.width = width; - } - if (canvas.height !== height) { - canvas.height = height; - } - - if (saveImg) { - this._drawCtx.putImageData(saveImg, 0, 0); - } } - // Readjust the viewport as it may be incorrectly sized - // and positioned - var vp = this._viewportLoc; - this.viewportChangeSize(vp.w, vp.h); - this.viewportChangePos(0, 0); - }, - - // Track what parts of the visible canvas that need updating - _damage: function(x, y, w, h) { - if (x < this._damageBounds.left) { - this._damageBounds.left = x; + if (saveImg) { + this._drawCtx.putImageData(saveImg, 0, 0); } - if (y < this._damageBounds.top) { - this._damageBounds.top = y; - } - if ((x + w) > this._damageBounds.right) { - this._damageBounds.right = x + w; - } - if ((y + h) > this._damageBounds.bottom) { - this._damageBounds.bottom = y + h; - } - }, + } - // Update the visible canvas with the contents of the - // rendering canvas - flip: function(from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - this._renderQ_push({ - 'type': 'flip' - }); - } else { - var x, y, vx, vy, w, h; + // Readjust the viewport as it may be incorrectly sized + // and positioned + var vp = this._viewportLoc; + this.viewportChangeSize(vp.w, vp.h); + this.viewportChangePos(0, 0); + }, - x = this._damageBounds.left; - y = this._damageBounds.top; - w = this._damageBounds.right - x; - h = this._damageBounds.bottom - y; + // Track what parts of the visible canvas that need updating + _damage: function(x, y, w, h) { + if (x < this._damageBounds.left) { + this._damageBounds.left = x; + } + if (y < this._damageBounds.top) { + this._damageBounds.top = y; + } + if ((x + w) > this._damageBounds.right) { + this._damageBounds.right = x + w; + } + if ((y + h) > this._damageBounds.bottom) { + this._damageBounds.bottom = y + h; + } + }, - vx = x - this._viewportLoc.x; - vy = y - this._viewportLoc.y; - - if (vx < 0) { - w += vx; - x -= vx; - vx = 0; - } - if (vy < 0) { - h += vy; - y -= vy; - vy = 0; - } - - if ((vx + w) > this._viewportLoc.w) { - w = this._viewportLoc.w - vx; - } - if ((vy + h) > this._viewportLoc.h) { - h = this._viewportLoc.h - vy; - } - - if ((w > 0) && (h > 0)) { - // FIXME: We may need to disable image smoothing here - // as well (see copyImage()), but we haven't - // noticed any problem yet. - this._targetCtx.drawImage(this._backbuffer, - x, y, w, h, - vx, vy, w, h); - } - - this._damageBounds.left = this._damageBounds.top = 65535; - this._damageBounds.right = this._damageBounds.bottom = 0; - } - }, - - clear: function () { - if (this._logo) { - this.resize(this._logo.width, this._logo.height); - this.imageRect(0, 0, this._logo.type, this._logo.data); - } else { - this.resize(240, 20); - this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height); - } - this.flip(); - }, - - pending: function() { - return this._renderQ.length > 0; - }, - - flush: function() { - if (this._renderQ.length === 0) { - this._onFlush(); - } else { - this._flushing = true; - } - }, - - fillRect: function (x, y, width, height, color, from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - this._renderQ_push({ - 'type': 'fill', - 'x': x, - 'y': y, - 'width': width, - 'height': height, - 'color': color - }); - } else { - this._setFillColor(color); - this._drawCtx.fillRect(x, y, width, height); - this._damage(x, y, width, height); - } - }, - - copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - this._renderQ_push({ - 'type': 'copy', - 'old_x': old_x, - 'old_y': old_y, - 'x': new_x, - 'y': new_y, - 'width': w, - 'height': h, - }); - } else { - // Due to this bug among others [1] we need to disable the image-smoothing to - // avoid getting a blur effect when copying data. - // - // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719 - // - // We need to set these every time since all properties are reset - // when the the size is changed - this._drawCtx.mozImageSmoothingEnabled = false; - this._drawCtx.webkitImageSmoothingEnabled = false; - this._drawCtx.msImageSmoothingEnabled = false; - this._drawCtx.imageSmoothingEnabled = false; - - this._drawCtx.drawImage(this._backbuffer, - old_x, old_y, w, h, - new_x, new_y, w, h); - this._damage(new_x, new_y, w, h); - } - }, - - imageRect: function(x, y, mime, arr) { - var img = new Image(); - img.src = "data: " + mime + ";base64," + Base64.encode(arr); + // Update the visible canvas with the contents of the + // rendering canvas + flip: function(from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { this._renderQ_push({ - 'type': 'img', - 'img': img, - 'x': x, - 'y': y + 'type': 'flip' }); - }, + } else { + var x, y, vx, vy, w, h; - // start updating a tile - startTile: function (x, y, width, height, color) { - this._tile_x = x; - this._tile_y = y; - if (width === 16 && height === 16) { - this._tile = this._tile16x16; - } else { - this._tile = this._drawCtx.createImageData(width, height); + x = this._damageBounds.left; + y = this._damageBounds.top; + w = this._damageBounds.right - x; + h = this._damageBounds.bottom - y; + + vx = x - this._viewportLoc.x; + vy = y - this._viewportLoc.y; + + if (vx < 0) { + w += vx; + x -= vx; + vx = 0; + } + if (vy < 0) { + h += vy; + y -= vy; + vy = 0; } - if (this._prefer_js) { - var bgr; - if (this._true_color) { - bgr = color; - } else { - bgr = this._colourMap[color[0]]; - } - var red = bgr[2]; - var green = bgr[1]; - var blue = bgr[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); + if ((vx + w) > this._viewportLoc.w) { + w = this._viewportLoc.w - vx; } - }, - - // update sub-rectangle of the current tile - subTile: function (x, y, w, h, color) { - if (this._prefer_js) { - var bgr; - if (this._true_color) { - bgr = color; - } else { - bgr = this._colourMap[color[0]]; - } - var red = bgr[2]; - var green = bgr[1]; - var blue = bgr[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; - } - } - } 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 - }, - - blitImage: function (x, y, width, height, arr, offset, from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - // NB(directxman12): it's technically more performant here to use preallocated arrays, - // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, - // this probably isn't getting called *nearly* as much - var new_arr = new Uint8Array(width * height * 4); - new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); - this._renderQ_push({ - 'type': 'blit', - 'data': new_arr, - 'x': x, - 'y': y, - 'width': width, - 'height': height, - }); - } else if (this._true_color) { - this._bgrxImageData(x, y, width, height, arr, offset); - } else { - this._cmapImageData(x, y, width, height, arr, offset); - } - }, - - blitRgbImage: function (x, y , width, height, arr, offset, from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - // NB(directxman12): it's technically more performant here to use preallocated arrays, - // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, - // this probably isn't getting called *nearly* as much - var new_arr = new Uint8Array(width * height * 3); - new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); - this._renderQ_push({ - 'type': 'blitRgb', - 'data': new_arr, - 'x': x, - 'y': y, - 'width': width, - 'height': height, - }); - } else if (this._true_color) { - this._rgbImageData(x, y, width, height, arr, offset); - } else { - // probably wrong? - this._cmapImageData(x, y, width, height, arr, offset); - } - }, - - blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - // NB(directxman12): it's technically more performant here to use preallocated arrays, - // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, - // this probably isn't getting called *nearly* as much - var new_arr = new Uint8Array(width * height * 4); - new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); - this._renderQ_push({ - 'type': 'blitRgbx', - 'data': new_arr, - 'x': x, - 'y': y, - 'width': width, - 'height': height, - }); - } else { - this._rgbxImageData(x, y, width, height, arr, offset); - } - }, - - drawImage: function (img, x, y) { - this._drawCtx.drawImage(img, x, y); - this._damage(x, y, img.width, img.height); - }, - - changeCursor: function (pixels, mask, hotx, hoty, w, h) { - if (this._cursor_uri === false) { - Util.Warn("changeCursor called but no cursor data URI support"); - return; + if ((vy + h) > this._viewportLoc.h) { + h = this._viewportLoc.h - vy; } - if (this._true_color) { - Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h); - } else { - Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap); - } - }, - - defaultCursor: function () { - this._target.style.cursor = "default"; - }, - - disableLocalCursor: function () { - 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) { - var vp = this._viewportLoc; - var targetAspectRatio = containerWidth / containerHeight; - var fbAspectRatio = vp.w / vp.h; - - var scaleRatio; - if (fbAspectRatio >= targetAspectRatio) { - scaleRatio = containerWidth / vp.w; - } else { - scaleRatio = containerHeight / vp.h; + if ((w > 0) && (h > 0)) { + // FIXME: We may need to disable image smoothing here + // as well (see copyImage()), but we haven't + // noticed any problem yet. + this._targetCtx.drawImage(this._backbuffer, + x, y, w, h, + vx, vy, w, h); } - if (scaleRatio > 1.0 && downscaleOnly) { - scaleRatio = 1.0; - } + this._damageBounds.left = this._damageBounds.top = 65535; + this._damageBounds.right = this._damageBounds.bottom = 0; + } + }, - this._rescale(scaleRatio); - }, + clear: function () { + if (this._logo) { + this.resize(this._logo.width, this._logo.height); + this.imageRect(0, 0, this._logo.type, this._logo.data); + } else { + this.resize(240, 20); + this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height); + } + this.flip(); + }, - // Private Methods - _rescale: function (factor) { - this._scale = factor; - var vp = this._viewportLoc; + pending: function() { + return this._renderQ.length > 0; + }, - // NB(directxman12): If you set the width directly, or set the - // style width to a number, the canvas is cleared. - // However, if you set the style width to a string - // ('NNNpx'), the canvas is scaled without clearing. - var width = Math.round(factor * vp.w) + 'px'; - var height = Math.round(factor * vp.h) + 'px'; + flush: function() { + if (this._renderQ.length === 0) { + this._onFlush(); + } else { + this._flushing = true; + } + }, - if ((this._target.style.width !== width) || - (this._target.style.height !== height)) { - this._target.style.width = width; - this._target.style.height = height; - } - }, + fillRect: function (x, y, width, height, color, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this._renderQ_push({ + 'type': 'fill', + 'x': x, + 'y': y, + 'width': width, + 'height': height, + 'color': color + }); + } else { + this._setFillColor(color); + this._drawCtx.fillRect(x, y, width, height); + this._damage(x, y, width, height); + } + }, - _setFillColor: function (color) { + copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this._renderQ_push({ + 'type': 'copy', + 'old_x': old_x, + 'old_y': old_y, + 'x': new_x, + 'y': new_y, + 'width': w, + 'height': h, + }); + } else { + // Due to this bug among others [1] we need to disable the image-smoothing to + // avoid getting a blur effect when copying data. + // + // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719 + // + // We need to set these every time since all properties are reset + // when the the size is changed + this._drawCtx.mozImageSmoothingEnabled = false; + this._drawCtx.webkitImageSmoothingEnabled = false; + this._drawCtx.msImageSmoothingEnabled = false; + this._drawCtx.imageSmoothingEnabled = false; + + this._drawCtx.drawImage(this._backbuffer, + old_x, old_y, w, h, + new_x, new_y, w, h); + this._damage(new_x, new_y, w, h); + } + }, + + imageRect: function(x, y, mime, arr) { + var img = new Image(); + img.src = "data: " + mime + ";base64," + Base64.encode(arr); + this._renderQ_push({ + 'type': 'img', + 'img': img, + 'x': x, + 'y': y + }); + }, + + // start updating a tile + startTile: function (x, y, width, height, color) { + this._tile_x = x; + this._tile_y = y; + if (width === 16 && height === 16) { + this._tile = this._tile16x16; + } else { + this._tile = this._drawCtx.createImageData(width, height); + } + + if (this._prefer_js) { var bgr; if (this._true_color) { bgr = color; } else { - bgr = this._colourMap[color]; + bgr = this._colourMap[color[0]]; } + var red = bgr[2]; + var green = bgr[1]; + var blue = bgr[0]; - var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')'; - if (newStyle !== this._prevDrawStyle) { - this._drawCtx.fillStyle = newStyle; - this._prevDrawStyle = newStyle; + 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; } - }, - - _rgbImageData: function (x, y, width, height, arr, offset) { - var img = this._drawCtx.createImageData(width, height); - var data = img.data; - for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) { - data[i] = arr[j]; - data[i + 1] = arr[j + 1]; - data[i + 2] = arr[j + 2]; - data[i + 3] = 255; // Alpha - } - this._drawCtx.putImageData(img, x, y); - this._damage(x, y, img.width, img.height); - }, - - _bgrxImageData: function (x, y, width, height, arr, offset) { - var img = this._drawCtx.createImageData(width, height); - var data = img.data; - for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) { - data[i] = arr[j + 2]; - data[i + 1] = arr[j + 1]; - data[i + 2] = arr[j]; - data[i + 3] = 255; // Alpha - } - this._drawCtx.putImageData(img, x, y); - this._damage(x, y, img.width, img.height); - }, - - _rgbxImageData: function (x, y, width, height, arr, offset) { - // NB(directxman12): arr must be an Type Array view - var img; - if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { - img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height); - } else { - img = this._drawCtx.createImageData(width, height); - img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4)); - } - this._drawCtx.putImageData(img, x, y); - this._damage(x, y, img.width, img.height); - }, - - _cmapImageData: function (x, y, width, height, arr, offset) { - var img = this._drawCtx.createImageData(width, height); - var data = img.data; - var cmap = this._colourMap; - for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) { - var bgr = cmap[arr[j]]; - data[i] = bgr[2]; - data[i + 1] = bgr[1]; - data[i + 2] = bgr[0]; - data[i + 3] = 255; // Alpha - } - this._drawCtx.putImageData(img, x, y); - this._damage(x, y, img.width, img.height); - }, - - _renderQ_push: function (action) { - this._renderQ.push(action); - if (this._renderQ.length === 1) { - // If this can be rendered immediately it will be, otherwise - // the scanner will wait for the relevant event - this._scan_renderQ(); - } - }, - - _resume_renderQ: function() { - // "this" is the object that is ready, not the - // display object - this.removeEventListener('load', this._noVNC_display._resume_renderQ); - this._noVNC_display._scan_renderQ(); - }, - - _scan_renderQ: function () { - var ready = true; - while (ready && this._renderQ.length > 0) { - var a = this._renderQ[0]; - switch (a.type) { - case 'flip': - this.flip(true); - break; - case 'copy': - this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true); - break; - case 'fill': - this.fillRect(a.x, a.y, a.width, a.height, a.color, true); - break; - case 'blit': - this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); - break; - case 'blitRgb': - this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true); - break; - case 'blitRgbx': - this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); - break; - case 'img': - if (a.img.complete) { - this.drawImage(a.img, a.x, a.y); - } else { - a.img._noVNC_display = this; - a.img.addEventListener('load', this._resume_renderQ); - // We need to wait for this image to 'load' - // to keep things in-order - ready = false; - } - break; - } - - if (ready) { - this._renderQ.shift(); - } - } - - if (this._renderQ.length === 0 && this._flushing) { - this._flushing = false; - this._onFlush(); - } - }, - }; - - Util.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} - ['true_color', 'rw', 'bool'], // Use true-color pixel data - ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color) - ['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, w0, h0, cmap) { - var w = w0; - var h = h0; - if (h < w) { - h = w; // increase h to make it square } else { - w = h; // increase w to make it square + this.fillRect(x, y, width, height, color, true); + } + }, + + // update sub-rectangle of the current tile + subTile: function (x, y, w, h, color) { + if (this._prefer_js) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color[0]]; + } + var red = bgr[2]; + var green = bgr[1]; + var blue = bgr[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; + } + } + } 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 + }, + + blitImage: function (x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + var new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this._renderQ_push({ + 'type': 'blit', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else if (this._true_color) { + this._bgrxImageData(x, y, width, height, arr, offset); + } else { + this._cmapImageData(x, y, width, height, arr, offset); + } + }, + + blitRgbImage: function (x, y , width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + var new_arr = new Uint8Array(width * height * 3); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this._renderQ_push({ + 'type': 'blitRgb', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else if (this._true_color) { + this._rgbImageData(x, y, width, height, arr, offset); + } else { + // probably wrong? + this._cmapImageData(x, y, width, height, arr, offset); + } + }, + + blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + var new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this._renderQ_push({ + 'type': 'blitRgbx', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else { + this._rgbxImageData(x, y, width, height, arr, offset); + } + }, + + drawImage: function (img, x, y) { + this._drawCtx.drawImage(img, x, y); + this._damage(x, y, img.width, img.height); + }, + + changeCursor: function (pixels, mask, hotx, hoty, w, h) { + if (this._cursor_uri === false) { + Log.Warn("changeCursor called but no cursor data URI support"); + return; } - var cur = []; + if (this._true_color) { + Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h); + } else { + Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap); + } + }, - // Push multi-byte little-endian values - cur.push16le = function (num) { - this.push(num & 0xFF, (num >> 8) & 0xFF); - }; - cur.push32le = function (num) { - this.push(num & 0xFF, - (num >> 8) & 0xFF, - (num >> 16) & 0xFF, - (num >> 24) & 0xFF); - }; + defaultCursor: function () { + this._target.style.cursor = "default"; + }, - var IHDRsz = 40; - var RGBsz = w * h * 4; - var XORsz = Math.ceil((w * h) / 8.0); - var ANDsz = Math.ceil((w * h) / 8.0); + disableLocalCursor: function () { + this._target.style.cursor = "none"; + }, - cur.push16le(0); // 0: Reserved - cur.push16le(2); // 2: .CUR type - cur.push16le(1); // 4: Number of images, 1 for non-animated ico + clippingDisplay: function () { + var vp = this._viewportLoc; + return this._fb_width > vp.w || this._fb_height > vp.h; + }, - // Cursor #1 header (ICONDIRENTRY) - cur.push(w); // 6: width - cur.push(h); // 7: height - cur.push(0); // 8: colors, 0 -> true-color - cur.push(0); // 9: reserved - cur.push16le(hotx); // 10: hotspot x coordinate - cur.push16le(hoty); // 12: hotspot y coordinate - cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz); - // 14: cursor data byte size - cur.push32le(22); // 18: offset of cursor data in the file + // Overridden getters/setters + set_scale: function (scale) { + this._rescale(scale); + }, - // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) - cur.push32le(IHDRsz); // 22: InfoHeader size - cur.push32le(w); // 26: Cursor width - cur.push32le(h * 2); // 30: XOR+AND height - cur.push16le(1); // 34: number of planes - cur.push16le(32); // 36: bits per pixel - cur.push32le(0); // 38: Type of compression + 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); + }, - cur.push32le(XORsz + ANDsz); - // 42: Size of Image - cur.push32le(0); // 46: reserved - cur.push32le(0); // 50: reserved - cur.push32le(0); // 54: reserved - cur.push32le(0); // 58: reserved + get_width: function () { + return this._fb_width; + }, + get_height: function () { + return this._fb_height; + }, - // 62: color data (RGBQUAD icColors[]) - var y, x; - for (y = h - 1; y >= 0; y--) { - for (x = 0; x < w; x++) { - if (x >= w0 || y >= h0) { - cur.push(0); // blue - cur.push(0); // green - cur.push(0); // red - cur.push(0); // alpha - } else { - var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8); - var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; - if (cmap) { - idx = (w0 * y) + x; - var rgb = cmap[pixels[idx]]; - cur.push(rgb[2]); // blue - cur.push(rgb[1]); // green - cur.push(rgb[0]); // red - cur.push(alpha); // alpha + autoscale: function (containerWidth, containerHeight, downscaleOnly) { + var vp = this._viewportLoc; + var targetAspectRatio = containerWidth / containerHeight; + var fbAspectRatio = vp.w / vp.h; + + var scaleRatio; + if (fbAspectRatio >= targetAspectRatio) { + scaleRatio = containerWidth / vp.w; + } else { + scaleRatio = containerHeight / vp.h; + } + + if (scaleRatio > 1.0 && downscaleOnly) { + scaleRatio = 1.0; + } + + this._rescale(scaleRatio); + }, + + // Private Methods + _rescale: function (factor) { + this._scale = factor; + var vp = this._viewportLoc; + + // NB(directxman12): If you set the width directly, or set the + // style width to a number, the canvas is cleared. + // However, if you set the style width to a string + // ('NNNpx'), the canvas is scaled without clearing. + var width = Math.round(factor * vp.w) + 'px'; + var height = Math.round(factor * vp.h) + 'px'; + + if ((this._target.style.width !== width) || + (this._target.style.height !== height)) { + this._target.style.width = width; + this._target.style.height = height; + } + }, + + _setFillColor: function (color) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color]; + } + + var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')'; + if (newStyle !== this._prevDrawStyle) { + this._drawCtx.fillStyle = newStyle; + this._prevDrawStyle = newStyle; + } + }, + + _rgbImageData: function (x, y, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) { + data[i] = arr[j]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j + 2]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x, y); + this._damage(x, y, img.width, img.height); + }, + + _bgrxImageData: function (x, y, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) { + data[i] = arr[j + 2]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x, y); + this._damage(x, y, img.width, img.height); + }, + + _rgbxImageData: function (x, y, width, height, arr, offset) { + // NB(directxman12): arr must be an Type Array view + var img; + if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { + img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height); + } else { + img = this._drawCtx.createImageData(width, height); + img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4)); + } + this._drawCtx.putImageData(img, x, y); + this._damage(x, y, img.width, img.height); + }, + + _cmapImageData: function (x, y, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + var cmap = this._colourMap; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) { + var bgr = cmap[arr[j]]; + data[i] = bgr[2]; + data[i + 1] = bgr[1]; + data[i + 2] = bgr[0]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x, y); + this._damage(x, y, img.width, img.height); + }, + + _renderQ_push: function (action) { + this._renderQ.push(action); + if (this._renderQ.length === 1) { + // If this can be rendered immediately it will be, otherwise + // the scanner will wait for the relevant event + this._scan_renderQ(); + } + }, + + _resume_renderQ: function() { + // "this" is the object that is ready, not the + // display object + this.removeEventListener('load', this._noVNC_display._resume_renderQ); + this._noVNC_display._scan_renderQ(); + }, + + _scan_renderQ: function () { + var ready = true; + while (ready && this._renderQ.length > 0) { + var a = this._renderQ[0]; + switch (a.type) { + case 'flip': + this.flip(true); + break; + case 'copy': + this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true); + break; + case 'fill': + this.fillRect(a.x, a.y, a.width, a.height, a.color, true); + break; + case 'blit': + this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'blitRgb': + this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'blitRgbx': + this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'img': + if (a.img.complete) { + this.drawImage(a.img, a.x, a.y); } else { - idx = ((w0 * y) + x) * 4; - cur.push(pixels[idx]); // blue - cur.push(pixels[idx + 1]); // green - cur.push(pixels[idx + 2]); // red - cur.push(alpha); // alpha + a.img._noVNC_display = this; + a.img.addEventListener('load', this._resume_renderQ); + // We need to wait for this image to 'load' + // to keep things in-order + ready = false; } + break; + } + + if (ready) { + this._renderQ.shift(); + } + } + + if (this._renderQ.length === 0 && this._flushing) { + this._flushing = false; + 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} + ['true_color', 'rw', 'bool'], // Use true-color pixel data + ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color) + ['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, w0, h0, cmap) { + var w = w0; + var h = h0; + if (h < w) { + h = w; // increase h to make it square + } else { + w = h; // increase w to make it square + } + + var cur = []; + + // Push multi-byte little-endian values + cur.push16le = function (num) { + this.push(num & 0xFF, (num >> 8) & 0xFF); + }; + cur.push32le = function (num) { + this.push(num & 0xFF, + (num >> 8) & 0xFF, + (num >> 16) & 0xFF, + (num >> 24) & 0xFF); + }; + + var IHDRsz = 40; + var RGBsz = w * h * 4; + var XORsz = Math.ceil((w * h) / 8.0); + var ANDsz = Math.ceil((w * h) / 8.0); + + cur.push16le(0); // 0: Reserved + cur.push16le(2); // 2: .CUR type + cur.push16le(1); // 4: Number of images, 1 for non-animated ico + + // Cursor #1 header (ICONDIRENTRY) + cur.push(w); // 6: width + cur.push(h); // 7: height + cur.push(0); // 8: colors, 0 -> true-color + cur.push(0); // 9: reserved + cur.push16le(hotx); // 10: hotspot x coordinate + cur.push16le(hoty); // 12: hotspot y coordinate + cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz); + // 14: cursor data byte size + cur.push32le(22); // 18: offset of cursor data in the file + + // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) + cur.push32le(IHDRsz); // 22: InfoHeader size + cur.push32le(w); // 26: Cursor width + cur.push32le(h * 2); // 30: XOR+AND height + cur.push16le(1); // 34: number of planes + cur.push16le(32); // 36: bits per pixel + cur.push32le(0); // 38: Type of compression + + cur.push32le(XORsz + ANDsz); + // 42: Size of Image + cur.push32le(0); // 46: reserved + cur.push32le(0); // 50: reserved + cur.push32le(0); // 54: reserved + cur.push32le(0); // 58: reserved + + // 62: color data (RGBQUAD icColors[]) + var y, x; + for (y = h - 1; y >= 0; y--) { + for (x = 0; x < w; x++) { + if (x >= w0 || y >= h0) { + cur.push(0); // blue + cur.push(0); // green + cur.push(0); // red + cur.push(0); // alpha + } else { + var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8); + var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; + if (cmap) { + idx = (w0 * y) + x; + var rgb = cmap[pixels[idx]]; + cur.push(rgb[2]); // blue + cur.push(rgb[1]); // green + cur.push(rgb[0]); // red + cur.push(alpha); // alpha + } else { + idx = ((w0 * y) + x) * 4; + cur.push(pixels[idx]); // blue + cur.push(pixels[idx + 1]); // green + cur.push(pixels[idx + 2]); // red + cur.push(alpha); // alpha } } } + } - // XOR/bitmask data (BYTE icXOR[]) - // (ignored, just needs to be the right size) - for (y = 0; y < h; y++) { - for (x = 0; x < Math.ceil(w / 8); x++) { - cur.push(0); - } + // XOR/bitmask data (BYTE icXOR[]) + // (ignored, just needs to be the right size) + for (y = 0; y < h; y++) { + for (x = 0; x < Math.ceil(w / 8); x++) { + cur.push(0); } + } - // AND/bitmask data (BYTE icAND[]) - // (ignored, just needs to be the right size) - for (y = 0; y < h; y++) { - for (x = 0; x < Math.ceil(w / 8); x++) { - cur.push(0); - } + // AND/bitmask data (BYTE icAND[]) + // (ignored, just needs to be the right size) + for (y = 0; y < h; y++) { + for (x = 0; x < Math.ceil(w / 8); x++) { + cur.push(0); } + } - var url = 'data:image/x-icon;base64,' + Base64.encode(cur); - target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; - }; -})(); + var url = 'data:image/x-icon;base64,' + Base64.encode(cur); + target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; +}; diff --git a/core/inflator.js b/core/inflator.js index 68fd8298..a4d6ff6a 100644 --- a/core/inflator.js +++ b/core/inflator.js @@ -36,4 +36,3 @@ export default function Inflate() { inflateInit(this.strm, this.windowBits); }; - diff --git a/core/input/devices.js b/core/input/devices.js index 74317ca8..22653e38 100644 --- a/core/input/devices.js +++ b/core/input/devices.js @@ -8,395 +8,387 @@ /*jslint browser: true, white: false */ /*global window, Util */ -import Util from "../util.js"; -import KeyboardUtil from "./util.js"; +import * as Log from '../util/logging.js'; +import { isTouchDevice } from '../util/browsers.js' +import { setCapture, releaseCapture, stopEvent, getPointerEvent } from '../util/events.js'; +import { set_defaults, make_properties } from '../util/properties.js'; +import * as KeyboardUtil from "./util.js"; +// +// Keyboard event handler +// -export var Keyboard; +const Keyboard = function (defaults) { + this._keyDownList = []; // List of depressed keys + // (even if they are happy) -(function () { - "use strict"; + set_defaults(this, defaults, { + 'target': document, + 'focused': true + }); - // - // Keyboard event handler - // - - Keyboard = function (defaults) { - this._keyDownList = []; // List of depressed keys - // (even if they are happy) - - Util.set_defaults(this, defaults, { - 'target': document, - 'focused': true - }); - - // create the keyboard handler - this._handler = new KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), - KeyboardUtil.VerifyCharModifier( /* jshint newcap: false */ - KeyboardUtil.TrackKeyState( - KeyboardUtil.EscapeModifiers(this._handleRfbEvent.bind(this)) - ) + // create the keyboard handler + this._handler = new KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), + KeyboardUtil.VerifyCharModifier( /* jshint newcap: false */ + KeyboardUtil.TrackKeyState( + KeyboardUtil.EscapeModifiers(this._handleRfbEvent.bind(this)) ) - ); /* jshint newcap: true */ + ) + ); /* jshint newcap: true */ - // keep these here so we can refer to them later - this._eventHandlers = { - 'keyup': this._handleKeyUp.bind(this), - 'keydown': this._handleKeyDown.bind(this), - 'keypress': this._handleKeyPress.bind(this), - 'blur': this._allKeysUp.bind(this) - }; + // keep these here so we can refer to them later + this._eventHandlers = { + 'keyup': this._handleKeyUp.bind(this), + 'keydown': this._handleKeyDown.bind(this), + 'keypress': this._handleKeyPress.bind(this), + 'blur': this._allKeysUp.bind(this) }; +}; - Keyboard.prototype = { - // private methods +Keyboard.prototype = { + // private methods - _handleRfbEvent: function (e) { - if (this._onKeyPress) { - Util.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") + - ", keysym: " + e.keysym.keysym + "(" + e.keysym.keyname + ")"); - this._onKeyPress(e); - } - }, - - setQEMUVNCKeyboardHandler: function () { - this._handler = new KeyboardUtil.QEMUKeyEventDecoder(KeyboardUtil.ModifierSync(), - KeyboardUtil.TrackQEMUKeyState( - this._handleRfbEvent.bind(this) - ) - ); - }, - - _handleKeyDown: function (e) { - if (!this._focused) { return; } - - if (this._handler.keydown(e)) { - // Suppress bubbling/default actions - Util.stopEvent(e); - } else { - // Allow the event to bubble and become a keyPress event which - // will have the character code translated - } - }, - - _handleKeyPress: function (e) { - if (!this._focused) { return; } - - if (this._handler.keypress(e)) { - // Suppress bubbling/default actions - Util.stopEvent(e); - } - }, - - _handleKeyUp: function (e) { - if (!this._focused) { return; } - - if (this._handler.keyup(e)) { - // Suppress bubbling/default actions - Util.stopEvent(e); - } - }, - - _allKeysUp: function () { - Util.Debug(">> Keyboard.allKeysUp"); - this._handler.releaseAll(); - Util.Debug("<< Keyboard.allKeysUp"); - }, - - // Public methods - - grab: function () { - //Util.Debug(">> Keyboard.grab"); - var c = this._target; - - c.addEventListener('keydown', this._eventHandlers.keydown); - c.addEventListener('keyup', this._eventHandlers.keyup); - c.addEventListener('keypress', this._eventHandlers.keypress); - - // Release (key up) if window loses focus - window.addEventListener('blur', this._eventHandlers.blur); - - //Util.Debug("<< Keyboard.grab"); - }, - - ungrab: function () { - //Util.Debug(">> Keyboard.ungrab"); - var c = this._target; - - c.removeEventListener('keydown', this._eventHandlers.keydown); - c.removeEventListener('keyup', this._eventHandlers.keyup); - c.removeEventListener('keypress', this._eventHandlers.keypress); - window.removeEventListener('blur', this._eventHandlers.blur); - - // Release (key up) all keys that are in a down state - this._allKeysUp(); - - //Util.Debug(">> Keyboard.ungrab"); - }, - - sync: function (e) { - this._handler.syncModifiers(e); + _handleRfbEvent: function (e) { + if (this._onKeyPress) { + Log.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") + + ", keysym: " + e.keysym.keysym + "(" + e.keysym.keyname + ")"); + this._onKeyPress(e); } + }, + + setQEMUVNCKeyboardHandler: function () { + this._handler = new KeyboardUtil.QEMUKeyEventDecoder(KeyboardUtil.ModifierSync(), + KeyboardUtil.TrackQEMUKeyState( + this._handleRfbEvent.bind(this) + ) + ); + }, + + _handleKeyDown: function (e) { + if (!this._focused) { return; } + + if (this._handler.keydown(e)) { + // Suppress bubbling/default actions + stopEvent(e); + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + } + }, + + _handleKeyPress: function (e) { + if (!this._focused) { return; } + + if (this._handler.keypress(e)) { + // Suppress bubbling/default actions + stopEvent(e); + } + }, + + _handleKeyUp: function (e) { + if (!this._focused) { return; } + + if (this._handler.keyup(e)) { + // Suppress bubbling/default actions + stopEvent(e); + } + }, + + _allKeysUp: function () { + Log.Debug(">> Keyboard.allKeysUp"); + this._handler.releaseAll(); + Log.Debug("<< Keyboard.allKeysUp"); + }, + + // Public methods + + grab: function () { + //Log.Debug(">> Keyboard.grab"); + var c = this._target; + + c.addEventListener('keydown', this._eventHandlers.keydown); + c.addEventListener('keyup', this._eventHandlers.keyup); + c.addEventListener('keypress', this._eventHandlers.keypress); + + // Release (key up) if window loses focus + window.addEventListener('blur', this._eventHandlers.blur); + + //Log.Debug("<< Keyboard.grab"); + }, + + ungrab: function () { + //Log.Debug(">> Keyboard.ungrab"); + var c = this._target; + + c.removeEventListener('keydown', this._eventHandlers.keydown); + c.removeEventListener('keyup', this._eventHandlers.keyup); + c.removeEventListener('keypress', this._eventHandlers.keypress); + window.removeEventListener('blur', this._eventHandlers.blur); + + // Release (key up) all keys that are in a down state + this._allKeysUp(); + + //Log.Debug(">> Keyboard.ungrab"); + }, + + sync: function (e) { + this._handler.syncModifiers(e); + } +}; + +make_properties(Keyboard, [ + ['target', 'wo', 'dom'], // DOM element that captures keyboard input + ['focused', 'rw', 'bool'], // Capture and send key events + + ['onKeyPress', 'rw', 'func'] // Handler for key press/release +]); + +const Mouse = function (defaults) { + this._mouseCaptured = false; + + this._doubleClickTimer = null; + this._lastTouchPos = null; + + // Configuration attributes + set_defaults(this, defaults, { + 'target': document, + 'focused': true, + 'touchButton': 1 + }); + + this._eventHandlers = { + 'mousedown': this._handleMouseDown.bind(this), + 'mouseup': this._handleMouseUp.bind(this), + 'mousemove': this._handleMouseMove.bind(this), + 'mousewheel': this._handleMouseWheel.bind(this), + 'mousedisable': this._handleMouseDisable.bind(this) }; +}; - Util.make_properties(Keyboard, [ - ['target', 'wo', 'dom'], // DOM element that captures keyboard input - ['focused', 'rw', 'bool'], // Capture and send key events +Mouse.prototype = { + // private methods + _captureMouse: function () { + // capturing the mouse ensures we get the mouseup event + setCapture(this._target); - ['onKeyPress', 'rw', 'func'] // Handler for key press/release - ]); -})(); + // some browsers give us mouseup events regardless, + // so if we never captured the mouse, we can disregard the event + this._mouseCaptured = true; + }, -export var Mouse; - -(function () { - Mouse = function (defaults) { - this._mouseCaptured = false; + _releaseMouse: function () { + releaseCapture(); + this._mouseCaptured = false; + }, + _resetDoubleClickTimer: function () { this._doubleClickTimer = null; - this._lastTouchPos = null; + }, - // Configuration attributes - Util.set_defaults(this, defaults, { - 'target': document, - 'focused': true, - 'touchButton': 1 - }); + _handleMouseButton: function (e, down) { + if (!this._focused) { return; } - this._eventHandlers = { - 'mousedown': this._handleMouseDown.bind(this), - 'mouseup': this._handleMouseUp.bind(this), - 'mousemove': this._handleMouseMove.bind(this), - 'mousewheel': this._handleMouseWheel.bind(this), - 'mousedisable': this._handleMouseDisable.bind(this) - }; - }; - - Mouse.prototype = { - // private methods - _captureMouse: function () { - // capturing the mouse ensures we get the mouseup event - Util.setCapture(this._target); - - // some browsers give us mouseup events regardless, - // so if we never captured the mouse, we can disregard the event - this._mouseCaptured = true; - }, - - _releaseMouse: function () { - Util.releaseCapture(); - this._mouseCaptured = false; - }, - - _resetDoubleClickTimer: function () { - this._doubleClickTimer = null; - }, - - _handleMouseButton: function (e, down) { - if (!this._focused) { return; } - - if (this._notify) { - this._notify(e); - } - - var pos = this._getMousePosition(e); - - var bmask; - if (e.touches || e.changedTouches) { - // Touch device - - // When two touches occur within 500 ms of each other and are - // close enough together a double click is triggered. - if (down == 1) { - if (this._doubleClickTimer === null) { - this._lastTouchPos = pos; - } else { - clearTimeout(this._doubleClickTimer); - - // When the distance between the two touches is small enough - // force the position of the latter touch to the position of - // the first. - - var xs = this._lastTouchPos.x - pos.x; - var ys = this._lastTouchPos.y - pos.y; - var d = Math.sqrt((xs * xs) + (ys * ys)); - - // The goal is to trigger on a certain physical width, the - // devicePixelRatio brings us a bit closer but is not optimal. - var threshold = 20 * (window.devicePixelRatio || 1); - if (d < threshold) { - pos = this._lastTouchPos; - } - } - this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); - } - bmask = this._touchButton; - // If bmask is set - } else if (e.which) { - /* everything except IE */ - bmask = 1 << e.button; - } else { - /* IE including 9 */ - bmask = (e.button & 0x1) + // Left - (e.button & 0x2) * 2 + // Right - (e.button & 0x4) / 2; // Middle - } - - if (this._onMouseButton) { - Util.Debug("onMouseButton " + (down ? "down" : "up") + - ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); - this._onMouseButton(pos.x, pos.y, down, bmask); - } - Util.stopEvent(e); - }, - - _handleMouseDown: function (e) { - this._captureMouse(); - this._handleMouseButton(e, 1); - }, - - _handleMouseUp: function (e) { - if (!this._mouseCaptured) { return; } - - this._handleMouseButton(e, 0); - this._releaseMouse(); - }, - - _handleMouseWheel: function (e) { - if (!this._focused) { return; } - - if (this._notify) { - this._notify(e); - } - - var pos = this._getMousePosition(e); - - if (this._onMouseButton) { - if (e.deltaX < 0) { - this._onMouseButton(pos.x, pos.y, 1, 1 << 5); - this._onMouseButton(pos.x, pos.y, 0, 1 << 5); - } else if (e.deltaX > 0) { - this._onMouseButton(pos.x, pos.y, 1, 1 << 6); - this._onMouseButton(pos.x, pos.y, 0, 1 << 6); - } - - if (e.deltaY < 0) { - this._onMouseButton(pos.x, pos.y, 1, 1 << 3); - this._onMouseButton(pos.x, pos.y, 0, 1 << 3); - } else if (e.deltaY > 0) { - this._onMouseButton(pos.x, pos.y, 1, 1 << 4); - this._onMouseButton(pos.x, pos.y, 0, 1 << 4); - } - } - - Util.stopEvent(e); - }, - - _handleMouseMove: function (e) { - if (! this._focused) { return; } - - if (this._notify) { - this._notify(e); - } - - var pos = this._getMousePosition(e); - if (this._onMouseMove) { - this._onMouseMove(pos.x, pos.y); - } - Util.stopEvent(e); - }, - - _handleMouseDisable: function (e) { - if (!this._focused) { return; } - - /* - * Stop propagation if inside canvas area - * Note: This is only needed for the 'click' event as it fails - * to fire properly for the target element so we have - * to listen on the document element instead. - */ - if (e.target == this._target) { - //Util.Debug("mouse event disabled"); - Util.stopEvent(e); - } - }, - - // Return coordinates relative to target - _getMousePosition: function(e) { - e = Util.getPointerEvent(e); - var bounds = this._target.getBoundingClientRect(); - var x, y; - // Clip to target bounds - if (e.clientX < bounds.left) { - x = 0; - } else if (e.clientX >= bounds.right) { - x = bounds.width - 1; - } else { - x = e.clientX - bounds.left; - } - if (e.clientY < bounds.top) { - y = 0; - } else if (e.clientY >= bounds.bottom) { - y = bounds.height - 1; - } else { - y = e.clientY - bounds.top; - } - return {x:x, y:y}; - }, - - - // Public methods - grab: function () { - var c = this._target; - - if (Util.isTouchDevice) { - c.addEventListener('touchstart', this._eventHandlers.mousedown); - window.addEventListener('touchend', this._eventHandlers.mouseup); - c.addEventListener('touchend', this._eventHandlers.mouseup); - c.addEventListener('touchmove', this._eventHandlers.mousemove); - } - c.addEventListener('mousedown', this._eventHandlers.mousedown); - window.addEventListener('mouseup', this._eventHandlers.mouseup); - c.addEventListener('mouseup', this._eventHandlers.mouseup); - c.addEventListener('mousemove', this._eventHandlers.mousemove); - c.addEventListener('wheel', this._eventHandlers.mousewheel); - - /* Prevent middle-click pasting (see above for why we bind to document) */ - document.addEventListener('click', this._eventHandlers.mousedisable); - - /* preventDefault() on mousedown doesn't stop this event for some - reason so we have to explicitly block it */ - c.addEventListener('contextmenu', this._eventHandlers.mousedisable); - }, - - ungrab: function () { - var c = this._target; - - if (Util.isTouchDevice) { - c.removeEventListener('touchstart', this._eventHandlers.mousedown); - window.removeEventListener('touchend', this._eventHandlers.mouseup); - c.removeEventListener('touchend', this._eventHandlers.mouseup); - c.removeEventListener('touchmove', this._eventHandlers.mousemove); - } - c.removeEventListener('mousedown', this._eventHandlers.mousedown); - window.removeEventListener('mouseup', this._eventHandlers.mouseup); - c.removeEventListener('mouseup', this._eventHandlers.mouseup); - c.removeEventListener('mousemove', this._eventHandlers.mousemove); - c.removeEventListener('wheel', this._eventHandlers.mousewheel); - - document.removeEventListener('click', this._eventHandlers.mousedisable); - - c.removeEventListener('contextmenu', this._eventHandlers.mousedisable); + if (this._notify) { + this._notify(e); } - }; - Util.make_properties(Mouse, [ - ['target', 'ro', 'dom'], // DOM element that captures mouse input - ['notify', 'ro', 'func'], // Function to call to notify whenever a mouse event is received - ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement + var pos = this._getMousePosition(e); - ['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) - ]); -})(); + var bmask; + if (e.touches || e.changedTouches) { + // Touch device + + // When two touches occur within 500 ms of each other and are + // close enough together a double click is triggered. + if (down == 1) { + if (this._doubleClickTimer === null) { + this._lastTouchPos = pos; + } else { + clearTimeout(this._doubleClickTimer); + + // When the distance between the two touches is small enough + // force the position of the latter touch to the position of + // the first. + + var xs = this._lastTouchPos.x - pos.x; + var ys = this._lastTouchPos.y - pos.y; + var d = Math.sqrt((xs * xs) + (ys * ys)); + + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + var threshold = 20 * (window.devicePixelRatio || 1); + if (d < threshold) { + pos = this._lastTouchPos; + } + } + this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); + } + bmask = this._touchButton; + // If bmask is set + } else if (e.which) { + /* everything except IE */ + bmask = 1 << e.button; + } else { + /* IE including 9 */ + bmask = (e.button & 0x1) + // Left + (e.button & 0x2) * 2 + // Right + (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); + } + stopEvent(e); + }, + + _handleMouseDown: function (e) { + this._captureMouse(); + this._handleMouseButton(e, 1); + }, + + _handleMouseUp: function (e) { + if (!this._mouseCaptured) { return; } + + this._handleMouseButton(e, 0); + this._releaseMouse(); + }, + + _handleMouseWheel: function (e) { + if (!this._focused) { return; } + + if (this._notify) { + this._notify(e); + } + + var pos = this._getMousePosition(e); + + if (this._onMouseButton) { + if (e.deltaX < 0) { + this._onMouseButton(pos.x, pos.y, 1, 1 << 5); + this._onMouseButton(pos.x, pos.y, 0, 1 << 5); + } else if (e.deltaX > 0) { + this._onMouseButton(pos.x, pos.y, 1, 1 << 6); + this._onMouseButton(pos.x, pos.y, 0, 1 << 6); + } + + if (e.deltaY < 0) { + this._onMouseButton(pos.x, pos.y, 1, 1 << 3); + this._onMouseButton(pos.x, pos.y, 0, 1 << 3); + } else if (e.deltaY > 0) { + this._onMouseButton(pos.x, pos.y, 1, 1 << 4); + this._onMouseButton(pos.x, pos.y, 0, 1 << 4); + } + } + + stopEvent(e); + }, + + _handleMouseMove: function (e) { + if (! this._focused) { return; } + + if (this._notify) { + this._notify(e); + } + + var pos = this._getMousePosition(e); + if (this._onMouseMove) { + this._onMouseMove(pos.x, pos.y); + } + stopEvent(e); + }, + + _handleMouseDisable: function (e) { + if (!this._focused) { return; } + + /* + * Stop propagation if inside canvas area + * Note: This is only needed for the 'click' event as it fails + * to fire properly for the target element so we have + * to listen on the document element instead. + */ + if (e.target == this._target) { + stopEvent(e); + } + }, + + // Return coordinates relative to target + _getMousePosition: function(e) { + e = getPointerEvent(e); + var bounds = this._target.getBoundingClientRect(); + var x, y; + // Clip to target bounds + if (e.clientX < bounds.left) { + x = 0; + } else if (e.clientX >= bounds.right) { + x = bounds.width - 1; + } else { + x = e.clientX - bounds.left; + } + if (e.clientY < bounds.top) { + y = 0; + } else if (e.clientY >= bounds.bottom) { + y = bounds.height - 1; + } else { + y = e.clientY - bounds.top; + } + return {x:x, y:y}; + }, + + // Public methods + grab: function () { + var c = this._target; + + if (isTouchDevice) { + c.addEventListener('touchstart', this._eventHandlers.mousedown); + window.addEventListener('touchend', this._eventHandlers.mouseup); + c.addEventListener('touchend', this._eventHandlers.mouseup); + c.addEventListener('touchmove', this._eventHandlers.mousemove); + } + c.addEventListener('mousedown', this._eventHandlers.mousedown); + window.addEventListener('mouseup', this._eventHandlers.mouseup); + c.addEventListener('mouseup', this._eventHandlers.mouseup); + c.addEventListener('mousemove', this._eventHandlers.mousemove); + c.addEventListener('wheel', this._eventHandlers.mousewheel); + + /* Prevent middle-click pasting (see above for why we bind to document) */ + document.addEventListener('click', this._eventHandlers.mousedisable); + + /* preventDefault() on mousedown doesn't stop this event for some + reason so we have to explicitly block it */ + c.addEventListener('contextmenu', this._eventHandlers.mousedisable); + }, + + ungrab: function () { + var c = this._target; + + if (isTouchDevice) { + c.removeEventListener('touchstart', this._eventHandlers.mousedown); + window.removeEventListener('touchend', this._eventHandlers.mouseup); + c.removeEventListener('touchend', this._eventHandlers.mouseup); + c.removeEventListener('touchmove', this._eventHandlers.mousemove); + } + c.removeEventListener('mousedown', this._eventHandlers.mousedown); + window.removeEventListener('mouseup', this._eventHandlers.mouseup); + c.removeEventListener('mouseup', this._eventHandlers.mouseup); + c.removeEventListener('mousemove', this._eventHandlers.mousemove); + c.removeEventListener('wheel', this._eventHandlers.mousewheel); + + document.removeEventListener('click', this._eventHandlers.mousedisable); + + c.removeEventListener('contextmenu', this._eventHandlers.mousedisable); + } +}; + +make_properties(Mouse, [ + ['target', 'ro', 'dom'], // DOM element that captures mouse input + ['notify', 'ro', 'func'], // Function to call to notify whenever a mouse event is received + ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement + + ['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) +]); + +export { Keyboard, Mouse }; diff --git a/core/input/keysym.js b/core/input/keysym.js index 15cc1bc7..f3a247fd 100644 --- a/core/input/keysym.js +++ b/core/input/keysym.js @@ -1,4 +1,4 @@ -var KeyTable = { +export default { XK_VoidSymbol: 0xffffff, /* Void symbol */ XK_BackSpace: 0xff08, /* Back space, back char */ @@ -378,5 +378,3 @@ var KeyTable = { XK_thorn: 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */ XK_ydiaeresis: 0x00ff, /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */ }; - -export default KeyTable; diff --git a/core/input/keysymdef.js b/core/input/keysymdef.js index e618c167..3d9cee39 100644 --- a/core/input/keysymdef.js +++ b/core/input/keysymdef.js @@ -3,22 +3,17 @@ // How this file was generated: // node /Users/jalf/dev/mi/novnc/utils/parse.js /opt/X11/include/X11/keysymdef.h -var keysyms = (function(){ - "use strict"; - var keynames = null; - var codepoints = {"32":32,"33":33,"34":34,"35":35,"36":36,"37":37,"38":38,"39":39,"40":40,"41":41,"42":42,"43":43,"44":44,"45":45,"46":46,"47":47,"48":48,"49":49,"50":50,"51":51,"52":52,"53":53,"54":54,"55":55,"56":56,"57":57,"58":58,"59":59,"60":60,"61":61,"62":62,"63":63,"64":64,"65":65,"66":66,"67":67,"68":68,"69":69,"70":70,"71":71,"72":72,"73":73,"74":74,"75":75,"76":76,"77":77,"78":78,"79":79,"80":80,"81":81,"82":82,"83":83,"84":84,"85":85,"86":86,"87":87,"88":88,"89":89,"90":90,"91":91,"92":92,"93":93,"94":94,"95":95,"96":96,"97":97,"98":98,"99":99,"100":100,"101":101,"102":102,"103":103,"104":104,"105":105,"106":106,"107":107,"108":108,"109":109,"110":110,"111":111,"112":112,"113":113,"114":114,"115":115,"116":116,"117":117,"118":118,"119":119,"120":120,"121":121,"122":122,"123":123,"124":124,"125":125,"126":126,"160":160,"161":161,"162":162,"163":163,"164":164,"165":165,"166":166,"167":167,"168":168,"169":169,"170":170,"171":171,"172":172,"173":173,"174":174,"175":175,"176":176,"177":177,"178":178,"179":179,"180":180,"181":181,"182":182,"183":183,"184":184,"185":185,"186":186,"187":187,"188":188,"189":189,"190":190,"191":191,"192":192,"193":193,"194":194,"195":195,"196":196,"197":197,"198":198,"199":199,"200":200,"201":201,"202":202,"203":203,"204":204,"205":205,"206":206,"207":207,"208":208,"209":209,"210":210,"211":211,"212":212,"213":213,"214":214,"215":215,"216":216,"217":217,"218":218,"219":219,"220":220,"221":221,"222":222,"223":223,"224":224,"225":225,"226":226,"227":227,"228":228,"229":229,"230":230,"231":231,"232":232,"233":233,"234":234,"235":235,"236":236,"237":237,"238":238,"239":239,"240":240,"241":241,"242":242,"243":243,"244":244,"245":245,"246":246,"247":247,"248":248,"249":249,"250":250,"251":251,"252":252,"253":253,"254":254,"255":255,"256":960,"257":992,"258":451,"259":483,"260":417,"261":433,"262":454,"263":486,"264":710,"265":742,"266":709,"267":741,"268":456,"269":488,"270":463,"271":495,"272":464,"273":496,"274":938,"275":954,"278":972,"279":1004,"280":458,"281":490,"282":460,"283":492,"284":728,"285":760,"286":683,"287":699,"288":725,"289":757,"290":939,"291":955,"292":678,"293":694,"294":673,"295":689,"296":933,"297":949,"298":975,"299":1007,"300":16777516,"301":16777517,"302":967,"303":999,"304":681,"305":697,"308":684,"309":700,"310":979,"311":1011,"312":930,"313":453,"314":485,"315":934,"316":950,"317":421,"318":437,"321":419,"322":435,"323":465,"324":497,"325":977,"326":1009,"327":466,"328":498,"330":957,"331":959,"332":978,"333":1010,"336":469,"337":501,"338":5052,"339":5053,"340":448,"341":480,"342":931,"343":947,"344":472,"345":504,"346":422,"347":438,"348":734,"349":766,"350":426,"351":442,"352":425,"353":441,"354":478,"355":510,"356":427,"357":443,"358":940,"359":956,"360":989,"361":1021,"362":990,"363":1022,"364":733,"365":765,"366":473,"367":505,"368":475,"369":507,"370":985,"371":1017,"372":16777588,"373":16777589,"374":16777590,"375":16777591,"376":5054,"377":428,"378":444,"379":431,"380":447,"381":430,"382":446,"399":16777615,"402":2294,"415":16777631,"416":16777632,"417":16777633,"431":16777647,"432":16777648,"437":16777653,"438":16777654,"439":16777655,"466":16777681,"486":16777702,"487":16777703,"601":16777817,"629":16777845,"658":16777874,"711":439,"728":418,"729":511,"731":434,"733":445,"901":1966,"902":1953,"904":1954,"905":1955,"906":1956,"908":1959,"910":1960,"911":1963,"912":1974,"913":1985,"914":1986,"915":1987,"916":1988,"917":1989,"918":1990,"919":1991,"920":1992,"921":1993,"922":1994,"923":1995,"924":1996,"925":1997,"926":1998,"927":1999,"928":2000,"929":2001,"931":2002,"932":2004,"933":2005,"934":2006,"935":2007,"936":2008,"937":2009,"938":1957,"939":1961,"940":1969,"941":1970,"942":1971,"943":1972,"944":1978,"945":2017,"946":2018,"947":2019,"948":2020,"949":2021,"950":2022,"951":2023,"952":2024,"953":2025,"954":2026,"955":2027,"956":2028,"957":2029,"958":2030,"959":2031,"960":2032,"961":2033,"962":2035,"963":2034,"964":2036,"965":2037,"966":2038,"967":2039,"968":2040,"969":2041,"970":1973,"971":1977,"972":1975,"973":1976,"974":1979,"1025":1715,"1026":1713,"1027":1714,"1028":1716,"1029":1717,"1030":1718,"1031":1719,"1032":1720,"1033":1721,"1034":1722,"1035":1723,"1036":1724,"1038":1726,"1039":1727,"1040":1761,"1041":1762,"1042":1783,"1043":1767,"1044":1764,"1045":1765,"1046":1782,"1047":1786,"1048":1769,"1049":1770,"1050":1771,"1051":1772,"1052":1773,"1053":1774,"1054":1775,"1055":1776,"1056":1778,"1057":1779,"1058":1780,"1059":1781,"1060":1766,"1061":1768,"1062":1763,"1063":1790,"1064":1787,"1065":1789,"1066":1791,"1067":1785,"1068":1784,"1069":1788,"1070":1760,"1071":1777,"1072":1729,"1073":1730,"1074":1751,"1075":1735,"1076":1732,"1077":1733,"1078":1750,"1079":1754,"1080":1737,"1081":1738,"1082":1739,"1083":1740,"1084":1741,"1085":1742,"1086":1743,"1087":1744,"1088":1746,"1089":1747,"1090":1748,"1091":1749,"1092":1734,"1093":1736,"1094":1731,"1095":1758,"1096":1755,"1097":1757,"1098":1759,"1099":1753,"1100":1752,"1101":1756,"1102":1728,"1103":1745,"1105":1699,"1106":1697,"1107":1698,"1108":1700,"1109":1701,"1110":1702,"1111":1703,"1112":1704,"1113":1705,"1114":1706,"1115":1707,"1116":1708,"1118":1710,"1119":1711,"1168":1725,"1169":1709,"1170":16778386,"1171":16778387,"1174":16778390,"1175":16778391,"1178":16778394,"1179":16778395,"1180":16778396,"1181":16778397,"1186":16778402,"1187":16778403,"1198":16778414,"1199":16778415,"1200":16778416,"1201":16778417,"1202":16778418,"1203":16778419,"1206":16778422,"1207":16778423,"1208":16778424,"1209":16778425,"1210":16778426,"1211":16778427,"1240":16778456,"1241":16778457,"1250":16778466,"1251":16778467,"1256":16778472,"1257":16778473,"1262":16778478,"1263":16778479,"1329":16778545,"1330":16778546,"1331":16778547,"1332":16778548,"1333":16778549,"1334":16778550,"1335":16778551,"1336":16778552,"1337":16778553,"1338":16778554,"1339":16778555,"1340":16778556,"1341":16778557,"1342":16778558,"1343":16778559,"1344":16778560,"1345":16778561,"1346":16778562,"1347":16778563,"1348":16778564,"1349":16778565,"1350":16778566,"1351":16778567,"1352":16778568,"1353":16778569,"1354":16778570,"1355":16778571,"1356":16778572,"1357":16778573,"1358":16778574,"1359":16778575,"1360":16778576,"1361":16778577,"1362":16778578,"1363":16778579,"1364":16778580,"1365":16778581,"1366":16778582,"1370":16778586,"1371":16778587,"1372":16778588,"1373":16778589,"1374":16778590,"1377":16778593,"1378":16778594,"1379":16778595,"1380":16778596,"1381":16778597,"1382":16778598,"1383":16778599,"1384":16778600,"1385":16778601,"1386":16778602,"1387":16778603,"1388":16778604,"1389":16778605,"1390":16778606,"1391":16778607,"1392":16778608,"1393":16778609,"1394":16778610,"1395":16778611,"1396":16778612,"1397":16778613,"1398":16778614,"1399":16778615,"1400":16778616,"1401":16778617,"1402":16778618,"1403":16778619,"1404":16778620,"1405":16778621,"1406":16778622,"1407":16778623,"1408":16778624,"1409":16778625,"1410":16778626,"1411":16778627,"1412":16778628,"1413":16778629,"1414":16778630,"1415":16778631,"1417":16778633,"1418":16778634,"1488":3296,"1489":3297,"1490":3298,"1491":3299,"1492":3300,"1493":3301,"1494":3302,"1495":3303,"1496":3304,"1497":3305,"1498":3306,"1499":3307,"1500":3308,"1501":3309,"1502":3310,"1503":3311,"1504":3312,"1505":3313,"1506":3314,"1507":3315,"1508":3316,"1509":3317,"1510":3318,"1511":3319,"1512":3320,"1513":3321,"1514":3322,"1548":1452,"1563":1467,"1567":1471,"1569":1473,"1570":1474,"1571":1475,"1572":1476,"1573":1477,"1574":1478,"1575":1479,"1576":1480,"1577":1481,"1578":1482,"1579":1483,"1580":1484,"1581":1485,"1582":1486,"1583":1487,"1584":1488,"1585":1489,"1586":1490,"1587":1491,"1588":1492,"1589":1493,"1590":1494,"1591":1495,"1592":1496,"1593":1497,"1594":1498,"1600":1504,"1601":1505,"1602":1506,"1603":1507,"1604":1508,"1605":1509,"1606":1510,"1607":1511,"1608":1512,"1609":1513,"1610":1514,"1611":1515,"1612":1516,"1613":1517,"1614":1518,"1615":1519,"1616":1520,"1617":1521,"1618":1522,"1619":16778835,"1620":16778836,"1621":16778837,"1632":16778848,"1633":16778849,"1634":16778850,"1635":16778851,"1636":16778852,"1637":16778853,"1638":16778854,"1639":16778855,"1640":16778856,"1641":16778857,"1642":16778858,"1648":16778864,"1657":16778873,"1662":16778878,"1670":16778886,"1672":16778888,"1681":16778897,"1688":16778904,"1700":16778916,"1705":16778921,"1711":16778927,"1722":16778938,"1726":16778942,"1729":16778945,"1740":16778956,"1746":16778962,"1748":16778964,"1776":16778992,"1777":16778993,"1778":16778994,"1779":16778995,"1780":16778996,"1781":16778997,"1782":16778998,"1783":16778999,"1784":16779000,"1785":16779001,"3458":16780674,"3459":16780675,"3461":16780677,"3462":16780678,"3463":16780679,"3464":16780680,"3465":16780681,"3466":16780682,"3467":16780683,"3468":16780684,"3469":16780685,"3470":16780686,"3471":16780687,"3472":16780688,"3473":16780689,"3474":16780690,"3475":16780691,"3476":16780692,"3477":16780693,"3478":16780694,"3482":16780698,"3483":16780699,"3484":16780700,"3485":16780701,"3486":16780702,"3487":16780703,"3488":16780704,"3489":16780705,"3490":16780706,"3491":16780707,"3492":16780708,"3493":16780709,"3494":16780710,"3495":16780711,"3496":16780712,"3497":16780713,"3498":16780714,"3499":16780715,"3500":16780716,"3501":16780717,"3502":16780718,"3503":16780719,"3504":16780720,"3505":16780721,"3507":16780723,"3508":16780724,"3509":16780725,"3510":16780726,"3511":16780727,"3512":16780728,"3513":16780729,"3514":16780730,"3515":16780731,"3517":16780733,"3520":16780736,"3521":16780737,"3522":16780738,"3523":16780739,"3524":16780740,"3525":16780741,"3526":16780742,"3530":16780746,"3535":16780751,"3536":16780752,"3537":16780753,"3538":16780754,"3539":16780755,"3540":16780756,"3542":16780758,"3544":16780760,"3545":16780761,"3546":16780762,"3547":16780763,"3548":16780764,"3549":16780765,"3550":16780766,"3551":16780767,"3570":16780786,"3571":16780787,"3572":16780788,"3585":3489,"3586":3490,"3587":3491,"3588":3492,"3589":3493,"3590":3494,"3591":3495,"3592":3496,"3593":3497,"3594":3498,"3595":3499,"3596":3500,"3597":3501,"3598":3502,"3599":3503,"3600":3504,"3601":3505,"3602":3506,"3603":3507,"3604":3508,"3605":3509,"3606":3510,"3607":3511,"3608":3512,"3609":3513,"3610":3514,"3611":3515,"3612":3516,"3613":3517,"3614":3518,"3615":3519,"3616":3520,"3617":3521,"3618":3522,"3619":3523,"3620":3524,"3621":3525,"3622":3526,"3623":3527,"3624":3528,"3625":3529,"3626":3530,"3627":3531,"3628":3532,"3629":3533,"3630":3534,"3631":3535,"3632":3536,"3633":3537,"3634":3538,"3635":3539,"3636":3540,"3637":3541,"3638":3542,"3639":3543,"3640":3544,"3641":3545,"3642":3546,"3647":3551,"3648":3552,"3649":3553,"3650":3554,"3651":3555,"3652":3556,"3653":3557,"3654":3558,"3655":3559,"3656":3560,"3657":3561,"3658":3562,"3659":3563,"3660":3564,"3661":3565,"3664":3568,"3665":3569,"3666":3570,"3667":3571,"3668":3572,"3669":3573,"3670":3574,"3671":3575,"3672":3576,"3673":3577,"4304":16781520,"4305":16781521,"4306":16781522,"4307":16781523,"4308":16781524,"4309":16781525,"4310":16781526,"4311":16781527,"4312":16781528,"4313":16781529,"4314":16781530,"4315":16781531,"4316":16781532,"4317":16781533,"4318":16781534,"4319":16781535,"4320":16781536,"4321":16781537,"4322":16781538,"4323":16781539,"4324":16781540,"4325":16781541,"4326":16781542,"4327":16781543,"4328":16781544,"4329":16781545,"4330":16781546,"4331":16781547,"4332":16781548,"4333":16781549,"4334":16781550,"4335":16781551,"4336":16781552,"4337":16781553,"4338":16781554,"4339":16781555,"4340":16781556,"4341":16781557,"4342":16781558,"7682":16784898,"7683":16784899,"7690":16784906,"7691":16784907,"7710":16784926,"7711":16784927,"7734":16784950,"7735":16784951,"7744":16784960,"7745":16784961,"7766":16784982,"7767":16784983,"7776":16784992,"7777":16784993,"7786":16785002,"7787":16785003,"7808":16785024,"7809":16785025,"7810":16785026,"7811":16785027,"7812":16785028,"7813":16785029,"7818":16785034,"7819":16785035,"7840":16785056,"7841":16785057,"7842":16785058,"7843":16785059,"7844":16785060,"7845":16785061,"7846":16785062,"7847":16785063,"7848":16785064,"7849":16785065,"7850":16785066,"7851":16785067,"7852":16785068,"7853":16785069,"7854":16785070,"7855":16785071,"7856":16785072,"7857":16785073,"7858":16785074,"7859":16785075,"7860":16785076,"7861":16785077,"7862":16785078,"7863":16785079,"7864":16785080,"7865":16785081,"7866":16785082,"7867":16785083,"7868":16785084,"7869":16785085,"7870":16785086,"7871":16785087,"7872":16785088,"7873":16785089,"7874":16785090,"7875":16785091,"7876":16785092,"7877":16785093,"7878":16785094,"7879":16785095,"7880":16785096,"7881":16785097,"7882":16785098,"7883":16785099,"7884":16785100,"7885":16785101,"7886":16785102,"7887":16785103,"7888":16785104,"7889":16785105,"7890":16785106,"7891":16785107,"7892":16785108,"7893":16785109,"7894":16785110,"7895":16785111,"7896":16785112,"7897":16785113,"7898":16785114,"7899":16785115,"7900":16785116,"7901":16785117,"7902":16785118,"7903":16785119,"7904":16785120,"7905":16785121,"7906":16785122,"7907":16785123,"7908":16785124,"7909":16785125,"7910":16785126,"7911":16785127,"7912":16785128,"7913":16785129,"7914":16785130,"7915":16785131,"7916":16785132,"7917":16785133,"7918":16785134,"7919":16785135,"7920":16785136,"7921":16785137,"7922":16785138,"7923":16785139,"7924":16785140,"7925":16785141,"7926":16785142,"7927":16785143,"7928":16785144,"7929":16785145,"8194":2722,"8195":2721,"8196":2723,"8197":2724,"8199":2725,"8200":2726,"8201":2727,"8202":2728,"8210":2747,"8211":2730,"8212":2729,"8213":1967,"8215":3295,"8216":2768,"8217":2769,"8218":2813,"8220":2770,"8221":2771,"8222":2814,"8224":2801,"8225":2802,"8226":2790,"8229":2735,"8230":2734,"8240":2773,"8242":2774,"8243":2775,"8248":2812,"8254":1150,"8304":16785520,"8308":16785524,"8309":16785525,"8310":16785526,"8311":16785527,"8312":16785528,"8313":16785529,"8320":16785536,"8321":16785537,"8322":16785538,"8323":16785539,"8324":16785540,"8325":16785541,"8326":16785542,"8327":16785543,"8328":16785544,"8329":16785545,"8352":16785568,"8353":16785569,"8354":16785570,"8355":16785571,"8356":16785572,"8357":16785573,"8358":16785574,"8359":16785575,"8360":16785576,"8361":3839,"8362":16785578,"8363":16785579,"8364":8364,"8453":2744,"8470":1712,"8471":2811,"8478":2772,"8482":2761,"8531":2736,"8532":2737,"8533":2738,"8534":2739,"8535":2740,"8536":2741,"8537":2742,"8538":2743,"8539":2755,"8540":2756,"8541":2757,"8542":2758,"8592":2299,"8593":2300,"8594":2301,"8595":2302,"8658":2254,"8660":2253,"8706":2287,"8709":16785925,"8711":2245,"8712":16785928,"8713":16785929,"8715":16785931,"8728":3018,"8730":2262,"8731":16785947,"8732":16785948,"8733":2241,"8734":2242,"8743":2270,"8744":2271,"8745":2268,"8746":2269,"8747":2239,"8748":16785964,"8749":16785965,"8756":2240,"8757":16785973,"8764":2248,"8771":2249,"8773":16785992,"8775":16785991,"8800":2237,"8801":2255,"8802":16786018,"8803":16786019,"8804":2236,"8805":2238,"8834":2266,"8835":2267,"8866":3068,"8867":3036,"8868":3010,"8869":3022,"8968":3027,"8970":3012,"8981":2810,"8992":2212,"8993":2213,"9109":3020,"9115":2219,"9117":2220,"9118":2221,"9120":2222,"9121":2215,"9123":2216,"9124":2217,"9126":2218,"9128":2223,"9132":2224,"9143":2209,"9146":2543,"9147":2544,"9148":2546,"9149":2547,"9225":2530,"9226":2533,"9227":2537,"9228":2531,"9229":2532,"9251":2732,"9252":2536,"9472":2211,"9474":2214,"9484":2210,"9488":2539,"9492":2541,"9496":2538,"9500":2548,"9508":2549,"9516":2551,"9524":2550,"9532":2542,"9618":2529,"9642":2791,"9643":2785,"9644":2779,"9645":2786,"9646":2783,"9647":2767,"9650":2792,"9651":2787,"9654":2781,"9655":2765,"9660":2793,"9661":2788,"9664":2780,"9665":2764,"9670":2528,"9675":2766,"9679":2782,"9702":2784,"9734":2789,"9742":2809,"9747":2762,"9756":2794,"9758":2795,"9792":2808,"9794":2807,"9827":2796,"9829":2798,"9830":2797,"9837":2806,"9839":2805,"10003":2803,"10007":2804,"10013":2777,"10016":2800,"10216":2748,"10217":2750,"10240":16787456,"10241":16787457,"10242":16787458,"10243":16787459,"10244":16787460,"10245":16787461,"10246":16787462,"10247":16787463,"10248":16787464,"10249":16787465,"10250":16787466,"10251":16787467,"10252":16787468,"10253":16787469,"10254":16787470,"10255":16787471,"10256":16787472,"10257":16787473,"10258":16787474,"10259":16787475,"10260":16787476,"10261":16787477,"10262":16787478,"10263":16787479,"10264":16787480,"10265":16787481,"10266":16787482,"10267":16787483,"10268":16787484,"10269":16787485,"10270":16787486,"10271":16787487,"10272":16787488,"10273":16787489,"10274":16787490,"10275":16787491,"10276":16787492,"10277":16787493,"10278":16787494,"10279":16787495,"10280":16787496,"10281":16787497,"10282":16787498,"10283":16787499,"10284":16787500,"10285":16787501,"10286":16787502,"10287":16787503,"10288":16787504,"10289":16787505,"10290":16787506,"10291":16787507,"10292":16787508,"10293":16787509,"10294":16787510,"10295":16787511,"10296":16787512,"10297":16787513,"10298":16787514,"10299":16787515,"10300":16787516,"10301":16787517,"10302":16787518,"10303":16787519,"10304":16787520,"10305":16787521,"10306":16787522,"10307":16787523,"10308":16787524,"10309":16787525,"10310":16787526,"10311":16787527,"10312":16787528,"10313":16787529,"10314":16787530,"10315":16787531,"10316":16787532,"10317":16787533,"10318":16787534,"10319":16787535,"10320":16787536,"10321":16787537,"10322":16787538,"10323":16787539,"10324":16787540,"10325":16787541,"10326":16787542,"10327":16787543,"10328":16787544,"10329":16787545,"10330":16787546,"10331":16787547,"10332":16787548,"10333":16787549,"10334":16787550,"10335":16787551,"10336":16787552,"10337":16787553,"10338":16787554,"10339":16787555,"10340":16787556,"10341":16787557,"10342":16787558,"10343":16787559,"10344":16787560,"10345":16787561,"10346":16787562,"10347":16787563,"10348":16787564,"10349":16787565,"10350":16787566,"10351":16787567,"10352":16787568,"10353":16787569,"10354":16787570,"10355":16787571,"10356":16787572,"10357":16787573,"10358":16787574,"10359":16787575,"10360":16787576,"10361":16787577,"10362":16787578,"10363":16787579,"10364":16787580,"10365":16787581,"10366":16787582,"10367":16787583,"10368":16787584,"10369":16787585,"10370":16787586,"10371":16787587,"10372":16787588,"10373":16787589,"10374":16787590,"10375":16787591,"10376":16787592,"10377":16787593,"10378":16787594,"10379":16787595,"10380":16787596,"10381":16787597,"10382":16787598,"10383":16787599,"10384":16787600,"10385":16787601,"10386":16787602,"10387":16787603,"10388":16787604,"10389":16787605,"10390":16787606,"10391":16787607,"10392":16787608,"10393":16787609,"10394":16787610,"10395":16787611,"10396":16787612,"10397":16787613,"10398":16787614,"10399":16787615,"10400":16787616,"10401":16787617,"10402":16787618,"10403":16787619,"10404":16787620,"10405":16787621,"10406":16787622,"10407":16787623,"10408":16787624,"10409":16787625,"10410":16787626,"10411":16787627,"10412":16787628,"10413":16787629,"10414":16787630,"10415":16787631,"10416":16787632,"10417":16787633,"10418":16787634,"10419":16787635,"10420":16787636,"10421":16787637,"10422":16787638,"10423":16787639,"10424":16787640,"10425":16787641,"10426":16787642,"10427":16787643,"10428":16787644,"10429":16787645,"10430":16787646,"10431":16787647,"10432":16787648,"10433":16787649,"10434":16787650,"10435":16787651,"10436":16787652,"10437":16787653,"10438":16787654,"10439":16787655,"10440":16787656,"10441":16787657,"10442":16787658,"10443":16787659,"10444":16787660,"10445":16787661,"10446":16787662,"10447":16787663,"10448":16787664,"10449":16787665,"10450":16787666,"10451":16787667,"10452":16787668,"10453":16787669,"10454":16787670,"10455":16787671,"10456":16787672,"10457":16787673,"10458":16787674,"10459":16787675,"10460":16787676,"10461":16787677,"10462":16787678,"10463":16787679,"10464":16787680,"10465":16787681,"10466":16787682,"10467":16787683,"10468":16787684,"10469":16787685,"10470":16787686,"10471":16787687,"10472":16787688,"10473":16787689,"10474":16787690,"10475":16787691,"10476":16787692,"10477":16787693,"10478":16787694,"10479":16787695,"10480":16787696,"10481":16787697,"10482":16787698,"10483":16787699,"10484":16787700,"10485":16787701,"10486":16787702,"10487":16787703,"10488":16787704,"10489":16787705,"10490":16787706,"10491":16787707,"10492":16787708,"10493":16787709,"10494":16787710,"10495":16787711,"12289":1188,"12290":1185,"12300":1186,"12301":1187,"12443":1246,"12444":1247,"12449":1191,"12450":1201,"12451":1192,"12452":1202,"12453":1193,"12454":1203,"12455":1194,"12456":1204,"12457":1195,"12458":1205,"12459":1206,"12461":1207,"12463":1208,"12465":1209,"12467":1210,"12469":1211,"12471":1212,"12473":1213,"12475":1214,"12477":1215,"12479":1216,"12481":1217,"12483":1199,"12484":1218,"12486":1219,"12488":1220,"12490":1221,"12491":1222,"12492":1223,"12493":1224,"12494":1225,"12495":1226,"12498":1227,"12501":1228,"12504":1229,"12507":1230,"12510":1231,"12511":1232,"12512":1233,"12513":1234,"12514":1235,"12515":1196,"12516":1236,"12517":1197,"12518":1237,"12519":1198,"12520":1238,"12521":1239,"12522":1240,"12523":1241,"12524":1242,"12525":1243,"12527":1244,"12530":1190,"12531":1245,"12539":1189,"12540":1200}; +var keynames = null; +var codepoints = {"32":32,"33":33,"34":34,"35":35,"36":36,"37":37,"38":38,"39":39,"40":40,"41":41,"42":42,"43":43,"44":44,"45":45,"46":46,"47":47,"48":48,"49":49,"50":50,"51":51,"52":52,"53":53,"54":54,"55":55,"56":56,"57":57,"58":58,"59":59,"60":60,"61":61,"62":62,"63":63,"64":64,"65":65,"66":66,"67":67,"68":68,"69":69,"70":70,"71":71,"72":72,"73":73,"74":74,"75":75,"76":76,"77":77,"78":78,"79":79,"80":80,"81":81,"82":82,"83":83,"84":84,"85":85,"86":86,"87":87,"88":88,"89":89,"90":90,"91":91,"92":92,"93":93,"94":94,"95":95,"96":96,"97":97,"98":98,"99":99,"100":100,"101":101,"102":102,"103":103,"104":104,"105":105,"106":106,"107":107,"108":108,"109":109,"110":110,"111":111,"112":112,"113":113,"114":114,"115":115,"116":116,"117":117,"118":118,"119":119,"120":120,"121":121,"122":122,"123":123,"124":124,"125":125,"126":126,"160":160,"161":161,"162":162,"163":163,"164":164,"165":165,"166":166,"167":167,"168":168,"169":169,"170":170,"171":171,"172":172,"173":173,"174":174,"175":175,"176":176,"177":177,"178":178,"179":179,"180":180,"181":181,"182":182,"183":183,"184":184,"185":185,"186":186,"187":187,"188":188,"189":189,"190":190,"191":191,"192":192,"193":193,"194":194,"195":195,"196":196,"197":197,"198":198,"199":199,"200":200,"201":201,"202":202,"203":203,"204":204,"205":205,"206":206,"207":207,"208":208,"209":209,"210":210,"211":211,"212":212,"213":213,"214":214,"215":215,"216":216,"217":217,"218":218,"219":219,"220":220,"221":221,"222":222,"223":223,"224":224,"225":225,"226":226,"227":227,"228":228,"229":229,"230":230,"231":231,"232":232,"233":233,"234":234,"235":235,"236":236,"237":237,"238":238,"239":239,"240":240,"241":241,"242":242,"243":243,"244":244,"245":245,"246":246,"247":247,"248":248,"249":249,"250":250,"251":251,"252":252,"253":253,"254":254,"255":255,"256":960,"257":992,"258":451,"259":483,"260":417,"261":433,"262":454,"263":486,"264":710,"265":742,"266":709,"267":741,"268":456,"269":488,"270":463,"271":495,"272":464,"273":496,"274":938,"275":954,"278":972,"279":1004,"280":458,"281":490,"282":460,"283":492,"284":728,"285":760,"286":683,"287":699,"288":725,"289":757,"290":939,"291":955,"292":678,"293":694,"294":673,"295":689,"296":933,"297":949,"298":975,"299":1007,"300":16777516,"301":16777517,"302":967,"303":999,"304":681,"305":697,"308":684,"309":700,"310":979,"311":1011,"312":930,"313":453,"314":485,"315":934,"316":950,"317":421,"318":437,"321":419,"322":435,"323":465,"324":497,"325":977,"326":1009,"327":466,"328":498,"330":957,"331":959,"332":978,"333":1010,"336":469,"337":501,"338":5052,"339":5053,"340":448,"341":480,"342":931,"343":947,"344":472,"345":504,"346":422,"347":438,"348":734,"349":766,"350":426,"351":442,"352":425,"353":441,"354":478,"355":510,"356":427,"357":443,"358":940,"359":956,"360":989,"361":1021,"362":990,"363":1022,"364":733,"365":765,"366":473,"367":505,"368":475,"369":507,"370":985,"371":1017,"372":16777588,"373":16777589,"374":16777590,"375":16777591,"376":5054,"377":428,"378":444,"379":431,"380":447,"381":430,"382":446,"399":16777615,"402":2294,"415":16777631,"416":16777632,"417":16777633,"431":16777647,"432":16777648,"437":16777653,"438":16777654,"439":16777655,"466":16777681,"486":16777702,"487":16777703,"601":16777817,"629":16777845,"658":16777874,"711":439,"728":418,"729":511,"731":434,"733":445,"901":1966,"902":1953,"904":1954,"905":1955,"906":1956,"908":1959,"910":1960,"911":1963,"912":1974,"913":1985,"914":1986,"915":1987,"916":1988,"917":1989,"918":1990,"919":1991,"920":1992,"921":1993,"922":1994,"923":1995,"924":1996,"925":1997,"926":1998,"927":1999,"928":2000,"929":2001,"931":2002,"932":2004,"933":2005,"934":2006,"935":2007,"936":2008,"937":2009,"938":1957,"939":1961,"940":1969,"941":1970,"942":1971,"943":1972,"944":1978,"945":2017,"946":2018,"947":2019,"948":2020,"949":2021,"950":2022,"951":2023,"952":2024,"953":2025,"954":2026,"955":2027,"956":2028,"957":2029,"958":2030,"959":2031,"960":2032,"961":2033,"962":2035,"963":2034,"964":2036,"965":2037,"966":2038,"967":2039,"968":2040,"969":2041,"970":1973,"971":1977,"972":1975,"973":1976,"974":1979,"1025":1715,"1026":1713,"1027":1714,"1028":1716,"1029":1717,"1030":1718,"1031":1719,"1032":1720,"1033":1721,"1034":1722,"1035":1723,"1036":1724,"1038":1726,"1039":1727,"1040":1761,"1041":1762,"1042":1783,"1043":1767,"1044":1764,"1045":1765,"1046":1782,"1047":1786,"1048":1769,"1049":1770,"1050":1771,"1051":1772,"1052":1773,"1053":1774,"1054":1775,"1055":1776,"1056":1778,"1057":1779,"1058":1780,"1059":1781,"1060":1766,"1061":1768,"1062":1763,"1063":1790,"1064":1787,"1065":1789,"1066":1791,"1067":1785,"1068":1784,"1069":1788,"1070":1760,"1071":1777,"1072":1729,"1073":1730,"1074":1751,"1075":1735,"1076":1732,"1077":1733,"1078":1750,"1079":1754,"1080":1737,"1081":1738,"1082":1739,"1083":1740,"1084":1741,"1085":1742,"1086":1743,"1087":1744,"1088":1746,"1089":1747,"1090":1748,"1091":1749,"1092":1734,"1093":1736,"1094":1731,"1095":1758,"1096":1755,"1097":1757,"1098":1759,"1099":1753,"1100":1752,"1101":1756,"1102":1728,"1103":1745,"1105":1699,"1106":1697,"1107":1698,"1108":1700,"1109":1701,"1110":1702,"1111":1703,"1112":1704,"1113":1705,"1114":1706,"1115":1707,"1116":1708,"1118":1710,"1119":1711,"1168":1725,"1169":1709,"1170":16778386,"1171":16778387,"1174":16778390,"1175":16778391,"1178":16778394,"1179":16778395,"1180":16778396,"1181":16778397,"1186":16778402,"1187":16778403,"1198":16778414,"1199":16778415,"1200":16778416,"1201":16778417,"1202":16778418,"1203":16778419,"1206":16778422,"1207":16778423,"1208":16778424,"1209":16778425,"1210":16778426,"1211":16778427,"1240":16778456,"1241":16778457,"1250":16778466,"1251":16778467,"1256":16778472,"1257":16778473,"1262":16778478,"1263":16778479,"1329":16778545,"1330":16778546,"1331":16778547,"1332":16778548,"1333":16778549,"1334":16778550,"1335":16778551,"1336":16778552,"1337":16778553,"1338":16778554,"1339":16778555,"1340":16778556,"1341":16778557,"1342":16778558,"1343":16778559,"1344":16778560,"1345":16778561,"1346":16778562,"1347":16778563,"1348":16778564,"1349":16778565,"1350":16778566,"1351":16778567,"1352":16778568,"1353":16778569,"1354":16778570,"1355":16778571,"1356":16778572,"1357":16778573,"1358":16778574,"1359":16778575,"1360":16778576,"1361":16778577,"1362":16778578,"1363":16778579,"1364":16778580,"1365":16778581,"1366":16778582,"1370":16778586,"1371":16778587,"1372":16778588,"1373":16778589,"1374":16778590,"1377":16778593,"1378":16778594,"1379":16778595,"1380":16778596,"1381":16778597,"1382":16778598,"1383":16778599,"1384":16778600,"1385":16778601,"1386":16778602,"1387":16778603,"1388":16778604,"1389":16778605,"1390":16778606,"1391":16778607,"1392":16778608,"1393":16778609,"1394":16778610,"1395":16778611,"1396":16778612,"1397":16778613,"1398":16778614,"1399":16778615,"1400":16778616,"1401":16778617,"1402":16778618,"1403":16778619,"1404":16778620,"1405":16778621,"1406":16778622,"1407":16778623,"1408":16778624,"1409":16778625,"1410":16778626,"1411":16778627,"1412":16778628,"1413":16778629,"1414":16778630,"1415":16778631,"1417":16778633,"1418":16778634,"1488":3296,"1489":3297,"1490":3298,"1491":3299,"1492":3300,"1493":3301,"1494":3302,"1495":3303,"1496":3304,"1497":3305,"1498":3306,"1499":3307,"1500":3308,"1501":3309,"1502":3310,"1503":3311,"1504":3312,"1505":3313,"1506":3314,"1507":3315,"1508":3316,"1509":3317,"1510":3318,"1511":3319,"1512":3320,"1513":3321,"1514":3322,"1548":1452,"1563":1467,"1567":1471,"1569":1473,"1570":1474,"1571":1475,"1572":1476,"1573":1477,"1574":1478,"1575":1479,"1576":1480,"1577":1481,"1578":1482,"1579":1483,"1580":1484,"1581":1485,"1582":1486,"1583":1487,"1584":1488,"1585":1489,"1586":1490,"1587":1491,"1588":1492,"1589":1493,"1590":1494,"1591":1495,"1592":1496,"1593":1497,"1594":1498,"1600":1504,"1601":1505,"1602":1506,"1603":1507,"1604":1508,"1605":1509,"1606":1510,"1607":1511,"1608":1512,"1609":1513,"1610":1514,"1611":1515,"1612":1516,"1613":1517,"1614":1518,"1615":1519,"1616":1520,"1617":1521,"1618":1522,"1619":16778835,"1620":16778836,"1621":16778837,"1632":16778848,"1633":16778849,"1634":16778850,"1635":16778851,"1636":16778852,"1637":16778853,"1638":16778854,"1639":16778855,"1640":16778856,"1641":16778857,"1642":16778858,"1648":16778864,"1657":16778873,"1662":16778878,"1670":16778886,"1672":16778888,"1681":16778897,"1688":16778904,"1700":16778916,"1705":16778921,"1711":16778927,"1722":16778938,"1726":16778942,"1729":16778945,"1740":16778956,"1746":16778962,"1748":16778964,"1776":16778992,"1777":16778993,"1778":16778994,"1779":16778995,"1780":16778996,"1781":16778997,"1782":16778998,"1783":16778999,"1784":16779000,"1785":16779001,"3458":16780674,"3459":16780675,"3461":16780677,"3462":16780678,"3463":16780679,"3464":16780680,"3465":16780681,"3466":16780682,"3467":16780683,"3468":16780684,"3469":16780685,"3470":16780686,"3471":16780687,"3472":16780688,"3473":16780689,"3474":16780690,"3475":16780691,"3476":16780692,"3477":16780693,"3478":16780694,"3482":16780698,"3483":16780699,"3484":16780700,"3485":16780701,"3486":16780702,"3487":16780703,"3488":16780704,"3489":16780705,"3490":16780706,"3491":16780707,"3492":16780708,"3493":16780709,"3494":16780710,"3495":16780711,"3496":16780712,"3497":16780713,"3498":16780714,"3499":16780715,"3500":16780716,"3501":16780717,"3502":16780718,"3503":16780719,"3504":16780720,"3505":16780721,"3507":16780723,"3508":16780724,"3509":16780725,"3510":16780726,"3511":16780727,"3512":16780728,"3513":16780729,"3514":16780730,"3515":16780731,"3517":16780733,"3520":16780736,"3521":16780737,"3522":16780738,"3523":16780739,"3524":16780740,"3525":16780741,"3526":16780742,"3530":16780746,"3535":16780751,"3536":16780752,"3537":16780753,"3538":16780754,"3539":16780755,"3540":16780756,"3542":16780758,"3544":16780760,"3545":16780761,"3546":16780762,"3547":16780763,"3548":16780764,"3549":16780765,"3550":16780766,"3551":16780767,"3570":16780786,"3571":16780787,"3572":16780788,"3585":3489,"3586":3490,"3587":3491,"3588":3492,"3589":3493,"3590":3494,"3591":3495,"3592":3496,"3593":3497,"3594":3498,"3595":3499,"3596":3500,"3597":3501,"3598":3502,"3599":3503,"3600":3504,"3601":3505,"3602":3506,"3603":3507,"3604":3508,"3605":3509,"3606":3510,"3607":3511,"3608":3512,"3609":3513,"3610":3514,"3611":3515,"3612":3516,"3613":3517,"3614":3518,"3615":3519,"3616":3520,"3617":3521,"3618":3522,"3619":3523,"3620":3524,"3621":3525,"3622":3526,"3623":3527,"3624":3528,"3625":3529,"3626":3530,"3627":3531,"3628":3532,"3629":3533,"3630":3534,"3631":3535,"3632":3536,"3633":3537,"3634":3538,"3635":3539,"3636":3540,"3637":3541,"3638":3542,"3639":3543,"3640":3544,"3641":3545,"3642":3546,"3647":3551,"3648":3552,"3649":3553,"3650":3554,"3651":3555,"3652":3556,"3653":3557,"3654":3558,"3655":3559,"3656":3560,"3657":3561,"3658":3562,"3659":3563,"3660":3564,"3661":3565,"3664":3568,"3665":3569,"3666":3570,"3667":3571,"3668":3572,"3669":3573,"3670":3574,"3671":3575,"3672":3576,"3673":3577,"4304":16781520,"4305":16781521,"4306":16781522,"4307":16781523,"4308":16781524,"4309":16781525,"4310":16781526,"4311":16781527,"4312":16781528,"4313":16781529,"4314":16781530,"4315":16781531,"4316":16781532,"4317":16781533,"4318":16781534,"4319":16781535,"4320":16781536,"4321":16781537,"4322":16781538,"4323":16781539,"4324":16781540,"4325":16781541,"4326":16781542,"4327":16781543,"4328":16781544,"4329":16781545,"4330":16781546,"4331":16781547,"4332":16781548,"4333":16781549,"4334":16781550,"4335":16781551,"4336":16781552,"4337":16781553,"4338":16781554,"4339":16781555,"4340":16781556,"4341":16781557,"4342":16781558,"7682":16784898,"7683":16784899,"7690":16784906,"7691":16784907,"7710":16784926,"7711":16784927,"7734":16784950,"7735":16784951,"7744":16784960,"7745":16784961,"7766":16784982,"7767":16784983,"7776":16784992,"7777":16784993,"7786":16785002,"7787":16785003,"7808":16785024,"7809":16785025,"7810":16785026,"7811":16785027,"7812":16785028,"7813":16785029,"7818":16785034,"7819":16785035,"7840":16785056,"7841":16785057,"7842":16785058,"7843":16785059,"7844":16785060,"7845":16785061,"7846":16785062,"7847":16785063,"7848":16785064,"7849":16785065,"7850":16785066,"7851":16785067,"7852":16785068,"7853":16785069,"7854":16785070,"7855":16785071,"7856":16785072,"7857":16785073,"7858":16785074,"7859":16785075,"7860":16785076,"7861":16785077,"7862":16785078,"7863":16785079,"7864":16785080,"7865":16785081,"7866":16785082,"7867":16785083,"7868":16785084,"7869":16785085,"7870":16785086,"7871":16785087,"7872":16785088,"7873":16785089,"7874":16785090,"7875":16785091,"7876":16785092,"7877":16785093,"7878":16785094,"7879":16785095,"7880":16785096,"7881":16785097,"7882":16785098,"7883":16785099,"7884":16785100,"7885":16785101,"7886":16785102,"7887":16785103,"7888":16785104,"7889":16785105,"7890":16785106,"7891":16785107,"7892":16785108,"7893":16785109,"7894":16785110,"7895":16785111,"7896":16785112,"7897":16785113,"7898":16785114,"7899":16785115,"7900":16785116,"7901":16785117,"7902":16785118,"7903":16785119,"7904":16785120,"7905":16785121,"7906":16785122,"7907":16785123,"7908":16785124,"7909":16785125,"7910":16785126,"7911":16785127,"7912":16785128,"7913":16785129,"7914":16785130,"7915":16785131,"7916":16785132,"7917":16785133,"7918":16785134,"7919":16785135,"7920":16785136,"7921":16785137,"7922":16785138,"7923":16785139,"7924":16785140,"7925":16785141,"7926":16785142,"7927":16785143,"7928":16785144,"7929":16785145,"8194":2722,"8195":2721,"8196":2723,"8197":2724,"8199":2725,"8200":2726,"8201":2727,"8202":2728,"8210":2747,"8211":2730,"8212":2729,"8213":1967,"8215":3295,"8216":2768,"8217":2769,"8218":2813,"8220":2770,"8221":2771,"8222":2814,"8224":2801,"8225":2802,"8226":2790,"8229":2735,"8230":2734,"8240":2773,"8242":2774,"8243":2775,"8248":2812,"8254":1150,"8304":16785520,"8308":16785524,"8309":16785525,"8310":16785526,"8311":16785527,"8312":16785528,"8313":16785529,"8320":16785536,"8321":16785537,"8322":16785538,"8323":16785539,"8324":16785540,"8325":16785541,"8326":16785542,"8327":16785543,"8328":16785544,"8329":16785545,"8352":16785568,"8353":16785569,"8354":16785570,"8355":16785571,"8356":16785572,"8357":16785573,"8358":16785574,"8359":16785575,"8360":16785576,"8361":3839,"8362":16785578,"8363":16785579,"8364":8364,"8453":2744,"8470":1712,"8471":2811,"8478":2772,"8482":2761,"8531":2736,"8532":2737,"8533":2738,"8534":2739,"8535":2740,"8536":2741,"8537":2742,"8538":2743,"8539":2755,"8540":2756,"8541":2757,"8542":2758,"8592":2299,"8593":2300,"8594":2301,"8595":2302,"8658":2254,"8660":2253,"8706":2287,"8709":16785925,"8711":2245,"8712":16785928,"8713":16785929,"8715":16785931,"8728":3018,"8730":2262,"8731":16785947,"8732":16785948,"8733":2241,"8734":2242,"8743":2270,"8744":2271,"8745":2268,"8746":2269,"8747":2239,"8748":16785964,"8749":16785965,"8756":2240,"8757":16785973,"8764":2248,"8771":2249,"8773":16785992,"8775":16785991,"8800":2237,"8801":2255,"8802":16786018,"8803":16786019,"8804":2236,"8805":2238,"8834":2266,"8835":2267,"8866":3068,"8867":3036,"8868":3010,"8869":3022,"8968":3027,"8970":3012,"8981":2810,"8992":2212,"8993":2213,"9109":3020,"9115":2219,"9117":2220,"9118":2221,"9120":2222,"9121":2215,"9123":2216,"9124":2217,"9126":2218,"9128":2223,"9132":2224,"9143":2209,"9146":2543,"9147":2544,"9148":2546,"9149":2547,"9225":2530,"9226":2533,"9227":2537,"9228":2531,"9229":2532,"9251":2732,"9252":2536,"9472":2211,"9474":2214,"9484":2210,"9488":2539,"9492":2541,"9496":2538,"9500":2548,"9508":2549,"9516":2551,"9524":2550,"9532":2542,"9618":2529,"9642":2791,"9643":2785,"9644":2779,"9645":2786,"9646":2783,"9647":2767,"9650":2792,"9651":2787,"9654":2781,"9655":2765,"9660":2793,"9661":2788,"9664":2780,"9665":2764,"9670":2528,"9675":2766,"9679":2782,"9702":2784,"9734":2789,"9742":2809,"9747":2762,"9756":2794,"9758":2795,"9792":2808,"9794":2807,"9827":2796,"9829":2798,"9830":2797,"9837":2806,"9839":2805,"10003":2803,"10007":2804,"10013":2777,"10016":2800,"10216":2748,"10217":2750,"10240":16787456,"10241":16787457,"10242":16787458,"10243":16787459,"10244":16787460,"10245":16787461,"10246":16787462,"10247":16787463,"10248":16787464,"10249":16787465,"10250":16787466,"10251":16787467,"10252":16787468,"10253":16787469,"10254":16787470,"10255":16787471,"10256":16787472,"10257":16787473,"10258":16787474,"10259":16787475,"10260":16787476,"10261":16787477,"10262":16787478,"10263":16787479,"10264":16787480,"10265":16787481,"10266":16787482,"10267":16787483,"10268":16787484,"10269":16787485,"10270":16787486,"10271":16787487,"10272":16787488,"10273":16787489,"10274":16787490,"10275":16787491,"10276":16787492,"10277":16787493,"10278":16787494,"10279":16787495,"10280":16787496,"10281":16787497,"10282":16787498,"10283":16787499,"10284":16787500,"10285":16787501,"10286":16787502,"10287":16787503,"10288":16787504,"10289":16787505,"10290":16787506,"10291":16787507,"10292":16787508,"10293":16787509,"10294":16787510,"10295":16787511,"10296":16787512,"10297":16787513,"10298":16787514,"10299":16787515,"10300":16787516,"10301":16787517,"10302":16787518,"10303":16787519,"10304":16787520,"10305":16787521,"10306":16787522,"10307":16787523,"10308":16787524,"10309":16787525,"10310":16787526,"10311":16787527,"10312":16787528,"10313":16787529,"10314":16787530,"10315":16787531,"10316":16787532,"10317":16787533,"10318":16787534,"10319":16787535,"10320":16787536,"10321":16787537,"10322":16787538,"10323":16787539,"10324":16787540,"10325":16787541,"10326":16787542,"10327":16787543,"10328":16787544,"10329":16787545,"10330":16787546,"10331":16787547,"10332":16787548,"10333":16787549,"10334":16787550,"10335":16787551,"10336":16787552,"10337":16787553,"10338":16787554,"10339":16787555,"10340":16787556,"10341":16787557,"10342":16787558,"10343":16787559,"10344":16787560,"10345":16787561,"10346":16787562,"10347":16787563,"10348":16787564,"10349":16787565,"10350":16787566,"10351":16787567,"10352":16787568,"10353":16787569,"10354":16787570,"10355":16787571,"10356":16787572,"10357":16787573,"10358":16787574,"10359":16787575,"10360":16787576,"10361":16787577,"10362":16787578,"10363":16787579,"10364":16787580,"10365":16787581,"10366":16787582,"10367":16787583,"10368":16787584,"10369":16787585,"10370":16787586,"10371":16787587,"10372":16787588,"10373":16787589,"10374":16787590,"10375":16787591,"10376":16787592,"10377":16787593,"10378":16787594,"10379":16787595,"10380":16787596,"10381":16787597,"10382":16787598,"10383":16787599,"10384":16787600,"10385":16787601,"10386":16787602,"10387":16787603,"10388":16787604,"10389":16787605,"10390":16787606,"10391":16787607,"10392":16787608,"10393":16787609,"10394":16787610,"10395":16787611,"10396":16787612,"10397":16787613,"10398":16787614,"10399":16787615,"10400":16787616,"10401":16787617,"10402":16787618,"10403":16787619,"10404":16787620,"10405":16787621,"10406":16787622,"10407":16787623,"10408":16787624,"10409":16787625,"10410":16787626,"10411":16787627,"10412":16787628,"10413":16787629,"10414":16787630,"10415":16787631,"10416":16787632,"10417":16787633,"10418":16787634,"10419":16787635,"10420":16787636,"10421":16787637,"10422":16787638,"10423":16787639,"10424":16787640,"10425":16787641,"10426":16787642,"10427":16787643,"10428":16787644,"10429":16787645,"10430":16787646,"10431":16787647,"10432":16787648,"10433":16787649,"10434":16787650,"10435":16787651,"10436":16787652,"10437":16787653,"10438":16787654,"10439":16787655,"10440":16787656,"10441":16787657,"10442":16787658,"10443":16787659,"10444":16787660,"10445":16787661,"10446":16787662,"10447":16787663,"10448":16787664,"10449":16787665,"10450":16787666,"10451":16787667,"10452":16787668,"10453":16787669,"10454":16787670,"10455":16787671,"10456":16787672,"10457":16787673,"10458":16787674,"10459":16787675,"10460":16787676,"10461":16787677,"10462":16787678,"10463":16787679,"10464":16787680,"10465":16787681,"10466":16787682,"10467":16787683,"10468":16787684,"10469":16787685,"10470":16787686,"10471":16787687,"10472":16787688,"10473":16787689,"10474":16787690,"10475":16787691,"10476":16787692,"10477":16787693,"10478":16787694,"10479":16787695,"10480":16787696,"10481":16787697,"10482":16787698,"10483":16787699,"10484":16787700,"10485":16787701,"10486":16787702,"10487":16787703,"10488":16787704,"10489":16787705,"10490":16787706,"10491":16787707,"10492":16787708,"10493":16787709,"10494":16787710,"10495":16787711,"12289":1188,"12290":1185,"12300":1186,"12301":1187,"12443":1246,"12444":1247,"12449":1191,"12450":1201,"12451":1192,"12452":1202,"12453":1193,"12454":1203,"12455":1194,"12456":1204,"12457":1195,"12458":1205,"12459":1206,"12461":1207,"12463":1208,"12465":1209,"12467":1210,"12469":1211,"12471":1212,"12473":1213,"12475":1214,"12477":1215,"12479":1216,"12481":1217,"12483":1199,"12484":1218,"12486":1219,"12488":1220,"12490":1221,"12491":1222,"12492":1223,"12493":1224,"12494":1225,"12495":1226,"12498":1227,"12501":1228,"12504":1229,"12507":1230,"12510":1231,"12511":1232,"12512":1233,"12513":1234,"12514":1235,"12515":1196,"12516":1236,"12517":1197,"12518":1237,"12519":1198,"12520":1238,"12521":1239,"12522":1240,"12523":1241,"12524":1242,"12525":1243,"12527":1244,"12530":1190,"12531":1245,"12539":1189,"12540":1200}; - function lookup(k) { return k ? {keysym: k, keyname: keynames ? keynames[k] : k} : undefined; } - return { - fromUnicode : function(u) { - var keysym = codepoints[u]; - if (keysym === undefined) { - keysym = 0x01000000 | u; - } - return lookup(keysym); - }, - lookup : lookup - }; -})(); - -export default keysyms +function lookup(k) { return k ? {keysym: k, keyname: keynames ? keynames[k] : k} : undefined; } +export default { + fromUnicode : function(u) { + var keysym = codepoints[u]; + if (keysym === undefined) { + keysym = 0x01000000 | u; + } + return lookup(keysym); + }, + lookup : lookup +}; diff --git a/core/input/util.js b/core/input/util.js index f34a9abf..b1e63711 100644 --- a/core/input/util.js +++ b/core/input/util.js @@ -1,293 +1,277 @@ import KeyTable from "./keysym.js"; import keysyms from "./keysymdef.js"; +export function substituteCodepoint(cp) { + // Any Unicode code points which do not have corresponding keysym entries + // can be swapped out for another code point by adding them to this table + var substitutions = { + // {S,s} with comma below -> {S,s} with cedilla + 0x218 : 0x15e, + 0x219 : 0x15f, + // {T,t} with comma below -> {T,t} with cedilla + 0x21a : 0x162, + 0x21b : 0x163 + }; -var KeyboardUtil = {}; + var sub = substitutions[cp]; + return sub ? sub : cp; +} -(function() { - "use strict"; +function isMac() { + return navigator && !!(/mac/i).exec(navigator.platform); +} +function isWindows() { + return navigator && !!(/win/i).exec(navigator.platform); +} +function isLinux() { + return navigator && !!(/linux/i).exec(navigator.platform); +} - function substituteCodepoint(cp) { - // Any Unicode code points which do not have corresponding keysym entries - // can be swapped out for another code point by adding them to this table - var substitutions = { - // {S,s} with comma below -> {S,s} with cedilla - 0x218 : 0x15e, - 0x219 : 0x15f, - // {T,t} with comma below -> {T,t} with cedilla - 0x21a : 0x162, - 0x21b : 0x163 - }; - - var sub = substitutions[cp]; - return sub ? sub : cp; - } - - function isMac() { - return navigator && !!(/mac/i).exec(navigator.platform); - } - function isWindows() { - return navigator && !!(/win/i).exec(navigator.platform); - } - function isLinux() { - return navigator && !!(/linux/i).exec(navigator.platform); - } - - // Return true if a modifier which is not the specified char modifier (and is not shift) is down - function hasShortcutModifier(charModifier, currentModifiers) { - var mods = {}; - for (var key in currentModifiers) { - if (parseInt(key) !== KeyTable.XK_Shift_L) { - mods[key] = currentModifiers[key]; - } +// Return true if a modifier which is not the specified char modifier (and is not shift) is down +export function hasShortcutModifier(charModifier, currentModifiers) { + var mods = {}; + for (var key in currentModifiers) { + if (parseInt(key) !== KeyTable.XK_Shift_L) { + mods[key] = currentModifiers[key]; } + } - var sum = 0; - for (var k in currentModifiers) { - if (mods[k]) { - ++sum; - } + var sum = 0; + for (var k in currentModifiers) { + if (mods[k]) { + ++sum; } - if (hasCharModifier(charModifier, mods)) { - return sum > charModifier.length; + } + if (hasCharModifier(charModifier, mods)) { + return sum > charModifier.length; + } + else { + return sum > 0; + } +} + +// Return true if the specified char modifier is currently down +export function hasCharModifier(charModifier, currentModifiers) { + if (charModifier.length === 0) { return false; } + + for (var i = 0; i < charModifier.length; ++i) { + if (!currentModifiers[charModifier[i]]) { + return false; + } + } + return true; +} + +// Helper object tracking modifier key state +// and generates fake key events to compensate if it gets out of sync +export function ModifierSync(charModifier) { + if (!charModifier) { + if (isMac()) { + // on Mac, Option (AKA Alt) is used as a char modifier + charModifier = [KeyTable.XK_Alt_L]; + } + else if (isWindows()) { + // on Windows, Ctrl+Alt is used as a char modifier + charModifier = [KeyTable.XK_Alt_L, KeyTable.XK_Control_L]; + } + else if (isLinux()) { + // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier + charModifier = [KeyTable.XK_ISO_Level3_Shift]; } else { - return sum > 0; + charModifier = []; } } - // Return true if the specified char modifier is currently down - function hasCharModifier(charModifier, currentModifiers) { - if (charModifier.length === 0) { return false; } + var state = {}; + state[KeyTable.XK_Control_L] = false; + state[KeyTable.XK_Alt_L] = false; + state[KeyTable.XK_ISO_Level3_Shift] = false; + state[KeyTable.XK_Shift_L] = false; + state[KeyTable.XK_Meta_L] = false; - for (var i = 0; i < charModifier.length; ++i) { - if (!currentModifiers[charModifier[i]]) { - return false; - } + function sync(evt, keysym) { + var result = []; + function syncKey(keysym) { + return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; } - return true; + + if (evt.ctrlKey !== undefined && + evt.ctrlKey !== state[KeyTable.XK_Control_L] && keysym !== KeyTable.XK_Control_L) { + state[KeyTable.XK_Control_L] = evt.ctrlKey; + result.push(syncKey(KeyTable.XK_Control_L)); + } + if (evt.altKey !== undefined && + evt.altKey !== state[KeyTable.XK_Alt_L] && keysym !== KeyTable.XK_Alt_L) { + state[KeyTable.XK_Alt_L] = evt.altKey; + result.push(syncKey(KeyTable.XK_Alt_L)); + } + if (evt.altGraphKey !== undefined && + evt.altGraphKey !== state[KeyTable.XK_ISO_Level3_Shift] && keysym !== KeyTable.XK_ISO_Level3_Shift) { + state[KeyTable.XK_ISO_Level3_Shift] = evt.altGraphKey; + result.push(syncKey(KeyTable.XK_ISO_Level3_Shift)); + } + if (evt.shiftKey !== undefined && + evt.shiftKey !== state[KeyTable.XK_Shift_L] && keysym !== KeyTable.XK_Shift_L) { + state[KeyTable.XK_Shift_L] = evt.shiftKey; + result.push(syncKey(KeyTable.XK_Shift_L)); + } + if (evt.metaKey !== undefined && + evt.metaKey !== state[KeyTable.XK_Meta_L] && keysym !== KeyTable.XK_Meta_L) { + state[KeyTable.XK_Meta_L] = evt.metaKey; + result.push(syncKey(KeyTable.XK_Meta_L)); + } + return result; + } + function syncKeyEvent(evt, down) { + var obj = getKeysym(evt); + var keysym = obj ? obj.keysym : null; + + // first, apply the event itself, if relevant + if (keysym !== null && state[keysym] !== undefined) { + state[keysym] = down; + } + return sync(evt, keysym); } - // Helper object tracking modifier key state - // and generates fake key events to compensate if it gets out of sync - function ModifierSync(charModifier) { - if (!charModifier) { - if (isMac()) { - // on Mac, Option (AKA Alt) is used as a char modifier - charModifier = [KeyTable.XK_Alt_L]; - } - else if (isWindows()) { - // on Windows, Ctrl+Alt is used as a char modifier - charModifier = [KeyTable.XK_Alt_L, KeyTable.XK_Control_L]; - } - else if (isLinux()) { - // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier - charModifier = [KeyTable.XK_ISO_Level3_Shift]; - } - else { - charModifier = []; - } - } + return { + // sync on the appropriate keyboard event + keydown: function(evt) { return syncKeyEvent(evt, true);}, + keyup: function(evt) { return syncKeyEvent(evt, false);}, + // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway + syncAny: function(evt) { return sync(evt);}, - var state = {}; - state[KeyTable.XK_Control_L] = false; - state[KeyTable.XK_Alt_L] = false; - state[KeyTable.XK_ISO_Level3_Shift] = false; - state[KeyTable.XK_Shift_L] = false; - state[KeyTable.XK_Meta_L] = false; + // is a shortcut modifier down? + hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); }, + // if a char modifier is down, return the keys it consists of, otherwise return null + activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; } + }; +} - function sync(evt, keysym) { - var result = []; - function syncKey(keysym) { - return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; - } - - if (evt.ctrlKey !== undefined && - evt.ctrlKey !== state[KeyTable.XK_Control_L] && keysym !== KeyTable.XK_Control_L) { - state[KeyTable.XK_Control_L] = evt.ctrlKey; - result.push(syncKey(KeyTable.XK_Control_L)); - } - if (evt.altKey !== undefined && - evt.altKey !== state[KeyTable.XK_Alt_L] && keysym !== KeyTable.XK_Alt_L) { - state[KeyTable.XK_Alt_L] = evt.altKey; - result.push(syncKey(KeyTable.XK_Alt_L)); - } - if (evt.altGraphKey !== undefined && - evt.altGraphKey !== state[KeyTable.XK_ISO_Level3_Shift] && keysym !== KeyTable.XK_ISO_Level3_Shift) { - state[KeyTable.XK_ISO_Level3_Shift] = evt.altGraphKey; - result.push(syncKey(KeyTable.XK_ISO_Level3_Shift)); - } - if (evt.shiftKey !== undefined && - evt.shiftKey !== state[KeyTable.XK_Shift_L] && keysym !== KeyTable.XK_Shift_L) { - state[KeyTable.XK_Shift_L] = evt.shiftKey; - result.push(syncKey(KeyTable.XK_Shift_L)); - } - if (evt.metaKey !== undefined && - evt.metaKey !== state[KeyTable.XK_Meta_L] && keysym !== KeyTable.XK_Meta_L) { - state[KeyTable.XK_Meta_L] = evt.metaKey; - result.push(syncKey(KeyTable.XK_Meta_L)); - } - return result; - } - function syncKeyEvent(evt, down) { - var obj = getKeysym(evt); - var keysym = obj ? obj.keysym : null; - - // first, apply the event itself, if relevant - if (keysym !== null && state[keysym] !== undefined) { - state[keysym] = down; - } - return sync(evt, keysym); - } - - return { - // sync on the appropriate keyboard event - keydown: function(evt) { return syncKeyEvent(evt, true);}, - keyup: function(evt) { return syncKeyEvent(evt, false);}, - // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway - syncAny: function(evt) { return sync(evt);}, - - // is a shortcut modifier down? - hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); }, - // if a char modifier is down, return the keys it consists of, otherwise return null - activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; } - }; +// Get a key ID from a keyboard event +// May be a string or an integer depending on the available properties +export function getKey(evt){ + if ('keyCode' in evt && 'key' in evt) { + return evt.key + ':' + evt.keyCode; } - - // Get a key ID from a keyboard event - // May be a string or an integer depending on the available properties - function getKey(evt){ - if ('keyCode' in evt && 'key' in evt) { - return evt.key + ':' + evt.keyCode; - } - else if ('keyCode' in evt) { - return evt.keyCode; - } - else { - return evt.key; - } + else if ('keyCode' in evt) { + return evt.keyCode; } + else { + return evt.key; + } +} - // Get the most reliable keysym value we can get from a key event - // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which - function getKeysym(evt){ - var codepoint; - if (evt.char && evt.char.length === 1) { - codepoint = evt.char.charCodeAt(); - } - else if (evt.charCode) { - codepoint = evt.charCode; - } - else if (evt.keyCode && evt.type === 'keypress') { - // IE10 stores the char code as keyCode, and has no other useful properties - codepoint = evt.keyCode; - } - if (codepoint) { - return keysyms.fromUnicode(substituteCodepoint(codepoint)); - } - // we could check evt.key here. - // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list, - // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key - // so we don't *need* it yet - if (evt.keyCode) { - return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey)); - } - if (evt.which) { - return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey)); - } +// Get the most reliable keysym value we can get from a key event +// if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which +export function getKeysym(evt){ + var codepoint; + if (evt.char && evt.char.length === 1) { + codepoint = evt.char.charCodeAt(); + } + else if (evt.charCode) { + codepoint = evt.charCode; + } + else if (evt.keyCode && evt.type === 'keypress') { + // IE10 stores the char code as keyCode, and has no other useful properties + codepoint = evt.keyCode; + } + if (codepoint) { + return keysyms.fromUnicode(substituteCodepoint(codepoint)); + } + // we could check evt.key here. + // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list, + // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key + // so we don't *need* it yet + if (evt.keyCode) { + return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey)); + } + if (evt.which) { + return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey)); + } + return null; +} + +// Given a keycode, try to predict which keysym it might be. +// If the keycode is unknown, null is returned. +export function keysymFromKeyCode(keycode, shiftPressed) { + if (typeof(keycode) !== 'number') { return null; } - - // Given a keycode, try to predict which keysym it might be. - // If the keycode is unknown, null is returned. - function keysymFromKeyCode(keycode, shiftPressed) { - if (typeof(keycode) !== 'number') { - return null; - } - // won't be accurate for azerty - if (keycode >= 0x30 && keycode <= 0x39) { - return keycode; // digit - } - if (keycode >= 0x41 && keycode <= 0x5a) { - // remap to lowercase unless shift is down - return shiftPressed ? keycode : keycode + 32; // A-Z - } - if (keycode >= 0x60 && keycode <= 0x69) { - return KeyTable.XK_KP_0 + (keycode - 0x60); // numpad 0-9 - } - - switch(keycode) { - case 0x20: return KeyTable.XK_space; - case 0x6a: return KeyTable.XK_KP_Multiply; - case 0x6b: return KeyTable.XK_KP_Add; - case 0x6c: return KeyTable.XK_KP_Separator; - case 0x6d: return KeyTable.XK_KP_Subtract; - case 0x6e: return KeyTable.XK_KP_Decimal; - case 0x6f: return KeyTable.XK_KP_Divide; - case 0xbb: return KeyTable.XK_plus; - case 0xbc: return KeyTable.XK_comma; - case 0xbd: return KeyTable.XK_minus; - case 0xbe: return KeyTable.XK_period; - } - - return nonCharacterKey({keyCode: keycode}); + // won't be accurate for azerty + if (keycode >= 0x30 && keycode <= 0x39) { + return keycode; // digit + } + if (keycode >= 0x41 && keycode <= 0x5a) { + // remap to lowercase unless shift is down + return shiftPressed ? keycode : keycode + 32; // A-Z + } + if (keycode >= 0x60 && keycode <= 0x69) { + return KeyTable.XK_KP_0 + (keycode - 0x60); // numpad 0-9 } - // if the key is a known non-character key (any key which doesn't generate character data) - // return its keysym value. Otherwise return null - function nonCharacterKey(evt) { - // evt.key not implemented yet - if (!evt.keyCode) { return null; } - var keycode = evt.keyCode; - - if (keycode >= 0x70 && keycode <= 0x87) { - return KeyTable.XK_F1 + keycode - 0x70; // F1-F24 - } - switch (keycode) { - - case 8 : return KeyTable.XK_BackSpace; - case 13 : return KeyTable.XK_Return; - - case 9 : return KeyTable.XK_Tab; - - case 27 : return KeyTable.XK_Escape; - case 46 : return KeyTable.XK_Delete; - - case 36 : return KeyTable.XK_Home; - case 35 : return KeyTable.XK_End; - case 33 : return KeyTable.XK_Page_Up; - case 34 : return KeyTable.XK_Page_Down; - case 45 : return KeyTable.XK_Insert; - - case 37 : return KeyTable.XK_Left; - case 38 : return KeyTable.XK_Up; - case 39 : return KeyTable.XK_Right; - case 40 : return KeyTable.XK_Down; - - case 16 : return KeyTable.XK_Shift_L; - case 17 : return KeyTable.XK_Control_L; - case 18 : return KeyTable.XK_Alt_L; // also: Option-key on Mac - - case 224 : return KeyTable.XK_Meta_L; - case 225 : return KeyTable.XK_ISO_Level3_Shift; // AltGr - case 91 : return KeyTable.XK_Super_L; // also: Windows-key - case 92 : return KeyTable.XK_Super_R; // also: Windows-key - case 93 : return KeyTable.XK_Menu; // also: Windows-Menu, Command on Mac - default: return null; - } + switch(keycode) { + case 0x20: return KeyTable.XK_space; + case 0x6a: return KeyTable.XK_KP_Multiply; + case 0x6b: return KeyTable.XK_KP_Add; + case 0x6c: return KeyTable.XK_KP_Separator; + case 0x6d: return KeyTable.XK_KP_Subtract; + case 0x6e: return KeyTable.XK_KP_Decimal; + case 0x6f: return KeyTable.XK_KP_Divide; + case 0xbb: return KeyTable.XK_plus; + case 0xbc: return KeyTable.XK_comma; + case 0xbd: return KeyTable.XK_minus; + case 0xbe: return KeyTable.XK_period; } - KeyboardUtil.hasShortcutModifier = hasShortcutModifier; - KeyboardUtil.hasCharModifier = hasCharModifier; - KeyboardUtil.ModifierSync = ModifierSync; - KeyboardUtil.getKey = getKey; - KeyboardUtil.getKeysym = getKeysym; - KeyboardUtil.keysymFromKeyCode = keysymFromKeyCode; - KeyboardUtil.nonCharacterKey = nonCharacterKey; - KeyboardUtil.substituteCodepoint = substituteCodepoint; -})(); + return nonCharacterKey({keyCode: keycode}); +} -KeyboardUtil.QEMUKeyEventDecoder = function(modifierState, next) { +// if the key is a known non-character key (any key which doesn't generate character data) +// return its keysym value. Otherwise return null +export function nonCharacterKey(evt) { + // evt.key not implemented yet + if (!evt.keyCode) { return null; } + var keycode = evt.keyCode; + + if (keycode >= 0x70 && keycode <= 0x87) { + return KeyTable.XK_F1 + keycode - 0x70; // F1-F24 + } + switch (keycode) { + + case 8 : return KeyTable.XK_BackSpace; + case 13 : return KeyTable.XK_Return; + + case 9 : return KeyTable.XK_Tab; + + case 27 : return KeyTable.XK_Escape; + case 46 : return KeyTable.XK_Delete; + + case 36 : return KeyTable.XK_Home; + case 35 : return KeyTable.XK_End; + case 33 : return KeyTable.XK_Page_Up; + case 34 : return KeyTable.XK_Page_Down; + case 45 : return KeyTable.XK_Insert; + + case 37 : return KeyTable.XK_Left; + case 38 : return KeyTable.XK_Up; + case 39 : return KeyTable.XK_Right; + case 40 : return KeyTable.XK_Down; + + case 16 : return KeyTable.XK_Shift_L; + case 17 : return KeyTable.XK_Control_L; + case 18 : return KeyTable.XK_Alt_L; // also: Option-key on Mac + + case 224 : return KeyTable.XK_Meta_L; + case 225 : return KeyTable.XK_ISO_Level3_Shift; // AltGr + case 91 : return KeyTable.XK_Super_L; // also: Windows-key + case 92 : return KeyTable.XK_Super_R; // also: Windows-key + case 93 : return KeyTable.XK_Menu; // also: Windows-Menu, Command on Mac + default: return null; + } +} + +export function QEMUKeyEventDecoder (modifierState, next) { "use strict"; function sendAll(evts) { @@ -333,7 +317,7 @@ KeyboardUtil.QEMUKeyEventDecoder = function(modifierState, next) { var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; - var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!KeyboardUtil.nonCharacterKey(evt)); + var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!nonCharacterKey(evt)); next(result); return suppress; @@ -357,7 +341,7 @@ KeyboardUtil.QEMUKeyEventDecoder = function(modifierState, next) { }; }; -KeyboardUtil.TrackQEMUKeyState = function(next) { +export function TrackQEMUKeyState (next) { "use strict"; var state = []; @@ -425,7 +409,7 @@ KeyboardUtil.TrackQEMUKeyState = function(next) { // - marks each event with an 'escape' property if a modifier was down which should be "escaped" // - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown // This information is collected into an object which is passed to the next() function. (one call per event) -KeyboardUtil.KeyEventDecoder = function(modifierState, next) { +export function KeyEventDecoder (modifierState, next) { "use strict"; function sendAll(evts) { for (var i = 0; i < evts.length; ++i) { @@ -434,18 +418,18 @@ KeyboardUtil.KeyEventDecoder = function(modifierState, next) { } function process(evt, type) { var result = {type: type}; - var keyId = KeyboardUtil.getKey(evt); + var keyId = getKey(evt); if (keyId) { result.keyId = keyId; } - var keysym = KeyboardUtil.getKeysym(evt); + var keysym = getKeysym(evt); var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress? // "special" keys like enter, tab or backspace don't send keypress events, // and some browsers don't send keypresses at all if a modifier is down - if (keysym && (type !== 'keydown' || KeyboardUtil.nonCharacterKey(evt) || hasModifier)) { + if (keysym && (type !== 'keydown' || nonCharacterKey(evt) || hasModifier)) { result.keysym = keysym; } @@ -454,11 +438,11 @@ KeyboardUtil.KeyEventDecoder = function(modifierState, next) { // Should we prevent the browser from handling the event? // Doing so on a keydown (in most browsers) prevents keypress from being generated // so only do that if we have to. - var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!KeyboardUtil.nonCharacterKey(evt)); + var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!nonCharacterKey(evt)); // If a char modifier is down on a keydown, we need to insert a stall, // so VerifyCharModifier knows to wait and see if a keypress is comnig - var stall = type === 'keydown' && modifierState.activeCharModifier() && !KeyboardUtil.nonCharacterKey(evt); + var stall = type === 'keydown' && modifierState.activeCharModifier() && !nonCharacterKey(evt); // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt) var active = modifierState.activeCharModifier(); @@ -512,7 +496,7 @@ KeyboardUtil.KeyEventDecoder = function(modifierState, next) { // so when used with the '2' key, Ctrl-Alt counts as a char modifier (and should be escaped), but when used with 'D', it does not. // The only way we can distinguish these cases is to wait and see if a keypress event arrives // When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two -KeyboardUtil.VerifyCharModifier = function(next) { +export function VerifyCharModifier (next) { "use strict"; var queue = []; var timer = null; @@ -569,7 +553,7 @@ KeyboardUtil.VerifyCharModifier = function(next) { // in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars // key repeat events should be merged into a single entry. // Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess -KeyboardUtil.TrackKeyState = function(next) { +export function TrackKeyState (next) { "use strict"; var state = []; @@ -653,7 +637,7 @@ KeyboardUtil.TrackKeyState = function(next) { // Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @), // then the modifier must be "undone" before sending the @, and "redone" afterwards. -KeyboardUtil.EscapeModifiers = function(next) { +export function EscapeModifiers (next) { "use strict"; return function(evt) { if (evt.type !== 'keydown' || evt.escape === undefined) { @@ -674,5 +658,3 @@ KeyboardUtil.EscapeModifiers = function(next) { /* jshint shadow: false */ }; }; - -export default KeyboardUtil; diff --git a/core/input/xtscancodes.js b/core/input/xtscancodes.js index 611a80be..43ea51b7 100644 --- a/core/input/xtscancodes.js +++ b/core/input/xtscancodes.js @@ -1,4 +1,4 @@ -var XtScancode = { +export default { "Escape": 0x0001, "Digit1": 0x0002, "Digit2": 0x0003, @@ -147,5 +147,3 @@ var XtScancode = { "LaunchMail": 0xE06C, "MediaSelect": 0xE06D, }; - -export default XtScancode diff --git a/core/rfb.js b/core/rfb.js index 97667b43..55ca2275 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -10,7 +10,10 @@ * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) */ -import Util from "./util.js"; +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 Display from "./display.js"; import { Keyboard, Mouse } from "./input/devices.js"; import Websock from "./websock.js"; @@ -143,7 +146,7 @@ export default function RFB(defaults) { this._qemuExtKeyEventSupported = false; // set the default value on user-facing properties - Util.set_defaults(this, defaults, { + set_defaults(this, defaults, { 'target': 'null', // VNC display rendering Canvas object 'focusContainer': document, // DOM element that captures keyboard input 'encrypt': false, // Use TLS/SSL/wss encryption @@ -172,7 +175,7 @@ export default function RFB(defaults) { }); // main setup - Util.Debug(">> RFB.constructor"); + Log.Debug(">> RFB.constructor"); // populate encHandlers with bound versions Object.keys(RFB.encodingHandlers).forEach(function (encName) { @@ -192,7 +195,7 @@ export default function RFB(defaults) { this._display = new Display({target: this._target, onFlush: this._onFlush.bind(this)}); } catch (exc) { - Util.Error("Display exception: " + exc); + Log.Error("Display exception: " + exc); throw exc; } @@ -210,13 +213,13 @@ export default function RFB(defaults) { if ((this._rfb_connection_state === 'connecting') && (this._rfb_init_state === '')) { this._rfb_init_state = 'ProtocolVersion'; - Util.Debug("Starting VNC handshake"); + Log.Debug("Starting VNC handshake"); } else { this._fail("Unexpected server connection"); } }.bind(this)); this._sock.on('close', function (e) { - Util.Warn("WebSocket on-close event"); + Log.Warn("WebSocket on-close event"); var msg = ""; if (e.code) { msg = " (code: " + e.code; @@ -249,1868 +252,1853 @@ export default function RFB(defaults) { this._sock.off('close'); }.bind(this)); this._sock.on('error', function (e) { - Util.Warn("WebSocket on-error event"); + Log.Warn("WebSocket on-error event"); }); this._init_vars(); this._cleanup(); var rmode = this._display.get_render_mode(); - Util.Info("Using native WebSockets, render mode: " + rmode); + Log.Info("Using native WebSockets, render mode: " + rmode); - Util.Debug("<< RFB.constructor"); + Log.Debug("<< RFB.constructor"); }; -(function() { - var _ = Util.Localisation.get; +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 : ""; - 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 : ""; + if (!this._rfb_host || !this._rfb_port) { + return this._fail( + _("Must set host and port")); + } - if (!this._rfb_host || !this._rfb_port) { - return this._fail( - _("Must set host and port")); - } + this._rfb_init_state = ''; + this._updateConnectionState('connecting'); + return true; + }, - this._rfb_init_state = ''; - this._updateConnectionState('connecting'); - return true; - }, + disconnect: function () { + this._updateConnectionState('disconnecting'); + this._sock.off('error'); + this._sock.off('message'); + this._sock.off('open'); + }, - disconnect: function () { - this._updateConnectionState('disconnecting'); - this._sock.off('error'); - this._sock.off('message'); - this._sock.off('open'); - }, + sendPassword: function (passwd) { + this._rfb_password = passwd; + setTimeout(this._init_msg.bind(this), 0); + }, - sendPassword: function (passwd) { - this._rfb_password = passwd; - setTimeout(this._init_msg.bind(this), 0); - }, + sendCtrlAltDel: function () { + if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } + Log.Info("Sending Ctrl-Alt-Del"); - sendCtrlAltDel: function () { - if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } - Util.Info("Sending Ctrl-Alt-Del"); + RFB.messages.keyEvent(this._sock, KeyTable.XK_Control_L, 1); + RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_L, 1); + RFB.messages.keyEvent(this._sock, KeyTable.XK_Delete, 1); + RFB.messages.keyEvent(this._sock, KeyTable.XK_Delete, 0); + RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_L, 0); + RFB.messages.keyEvent(this._sock, KeyTable.XK_Control_L, 0); + return true; + }, - RFB.messages.keyEvent(this._sock, KeyTable.XK_Control_L, 1); - RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_L, 1); - RFB.messages.keyEvent(this._sock, KeyTable.XK_Delete, 1); - RFB.messages.keyEvent(this._sock, KeyTable.XK_Delete, 0); - RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_L, 0); - RFB.messages.keyEvent(this._sock, KeyTable.XK_Control_L, 0); - 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; + }, - xvpOp: function (ver, op) { - if (this._rfb_xvp_ver < ver) { return false; } - Util.Info("Sending XVP operation " + op + " (version " + ver + ")"); - this._sock.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op)); - return true; - }, + xvpShutdown: function () { + return this.xvpOp(1, 2); + }, - xvpShutdown: function () { - return this.xvpOp(1, 2); - }, + xvpReboot: function () { + return this.xvpOp(1, 3); + }, - xvpReboot: function () { - return this.xvpOp(1, 3); - }, + xvpReset: function () { + return this.xvpOp(1, 4); + }, - xvpReset: function () { - return 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, down) { + if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } + if (typeof down !== 'undefined') { + Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym); + RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); + } else { + Log.Info("Sending keysym (down + up): " + keysym); + RFB.messages.keyEvent(this._sock, keysym, 1); + RFB.messages.keyEvent(this._sock, keysym, 0); + } + return true; + }, - // Send a key press. If 'down' is not specified then send a down key - // followed by an up key. - sendKey: function (keysym, down) { - if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } - if (typeof down !== 'undefined') { - Util.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym); - RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); - } else { - Util.Info("Sending keysym (down + up): " + keysym); - RFB.messages.keyEvent(this._sock, keysym, 1); - RFB.messages.keyEvent(this._sock, keysym, 0); - } - return true; - }, - - clipboardPasteFrom: function (text) { - if (this._rfb_connection_state !== 'connected' || this._view_only) { - return; - } - RFB.messages.clientCutText(this._sock, text); - }, - - // 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; - } - - if (this._supportsSetDesktopSize) { - RFB.messages.setDesktopSize(this._sock, width, height, - this._screen_id, this._screen_flags); - this._sock.flush(); - return true; - } else { - return false; - } - }, - - - // Private methods - - _connect: function () { - Util.Debug(">> RFB.connect"); - this._init_vars(); - - var uri; - if (typeof UsingSocketIO !== 'undefined') { - uri = 'http'; - } else { - uri = this._encrypt ? 'wss' : 'ws'; - } - - uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path; - Util.Info("connecting to " + uri); - - try { - // WebSocket.onopen transitions to the RFB init states - this._sock.open(uri, this._wsProtocols); - } catch (e) { - if (e.name === 'SyntaxError') { - this._fail("Invalid host or port value given", e); - } else { - this._fail("Error while connecting", e); - } - } - - Util.Debug("<< RFB.connect"); - }, - - _disconnect: function () { - Util.Debug(">> RFB.disconnect"); - this._cleanup(); - this._sock.close(); - this._print_stats(); - Util.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 i; - for (i = 0; i < this._encodings.length; i++) { - this._encStats[this._encodings[i][1]][0] = 0; - } - - for (i = 0; i < 4; i++) { - this._FBU.zlibs[i] = new Inflator(); - } - }, - - _print_stats: function () { - Util.Info("Encoding stats for this connection:"); - var i, s; - for (i = 0; i < this._encodings.length; i++) { - s = this._encStats[this._encodings[i][1]]; - if (s[0] + s[1] > 0) { - Util.Info(" " + this._encodings[i][0] + ": " + s[0] + " rects"); - } - } - - Util.Info("Encoding stats since page load:"); - for (i = 0; i < this._encodings.length; i++) { - s = this._encStats[this._encodings[i][1]]; - Util.Info(" " + this._encodings[i][0] + ": " + s[1] + " rects"); - } - }, - - _cleanup: function () { - if (!this._view_only) { this._keyboard.ungrab(); } - if (!this._view_only) { this._mouse.ungrab(); } - this._display.defaultCursor(); - if (Util.get_logging() !== 'debug') { - // Show noVNC logo on load and when disconnected, unless in - // debug mode - this._display.clear(); - } - }, - - /* - * Connection states: - * connecting - * connected - * disconnecting - * disconnected - permanent state - */ - _updateConnectionState: function (state) { - var oldstate = this._rfb_connection_state; - - if (state === oldstate) { - Util.Debug("Already in state '" + state + "', ignoring"); - return; - } - - // The 'disconnected' state is permanent for each RFB object - if (oldstate === 'disconnected') { - Util.Error("Tried changing state of a disconnected RFB object"); - return; - } - - // Ensure proper transitions before doing anything - switch (state) { - case 'connected': - if (oldstate !== 'connecting') { - Util.Error("Bad transition to connected state, " + - "previous connection state: " + oldstate); - return; - } - break; - - case 'disconnected': - if (oldstate !== 'disconnecting') { - Util.Error("Bad transition to disconnected state, " + - "previous connection state: " + oldstate); - return; - } - break; - - case 'connecting': - if (oldstate !== '') { - Util.Error("Bad transition to connecting state, " + - "previous connection state: " + oldstate); - return; - } - break; - - case 'disconnecting': - if (oldstate !== 'connected' && oldstate !== 'connecting') { - Util.Error("Bad transition to disconnecting state, " + - "previous connection state: " + oldstate); - return; - } - break; - - default: - Util.Error("Unknown connection state: " + state); - return; - } - - // State change actions - - this._rfb_connection_state = state; - this._onUpdateState(this, state, oldstate); - - var smsg = "New state '" + state + "', was '" + oldstate + "'."; - Util.Debug(smsg); - - if (this._disconnTimer && state !== 'disconnecting') { - Util.Debug("Clearing disconnect timer"); - clearTimeout(this._disconnTimer); - this._disconnTimer = null; - - // make sure we don't get a double event - this._sock.off('close'); - } - - switch (state) { - case 'disconnected': - // Call onDisconnected callback after onUpdateState 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); - } else { - // No reason means clean disconnect - this._onDisconnected(this); - } - break; - - case 'connecting': - this._connect(); - break; - - case 'disconnecting': - this._disconnect(); - - this._disconnTimer = setTimeout(function () { - this._rfb_disconnect_reason = _("Disconnect timeout"); - this._updateConnectionState('disconnected'); - }.bind(this), this._disconnectTimeout * 1000); - break; - } - }, - - /* Print errors and disconnect - * - * The optional parameter 'details' is used for information that - * should be logged but not sent to the user interface. - */ - _fail: function (msg, details) { - var fullmsg = msg; - if (typeof details !== 'undefined') { - fullmsg = msg + " (" + details + ")"; - } - switch (this._rfb_connection_state) { - case 'disconnecting': - Util.Error("Failed when disconnecting: " + fullmsg); - break; - case 'connected': - Util.Error("Failed while connected: " + fullmsg); - break; - case 'connecting': - Util.Error("Failed when connecting: " + fullmsg); - break; - default: - Util.Error("RFB failure: " + fullmsg); - break; - } - this._rfb_disconnect_reason = msg; //This is sent to the UI - - // Transition to disconnected without waiting for socket to close - this._updateConnectionState('disconnecting'); - this._updateConnectionState('disconnected'); + clipboardPasteFrom: function (text) { + if (this._rfb_connection_state !== 'connected' || this._view_only) { return; } + RFB.messages.clientCutText(this._sock, text); + }, + // 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; - }, + } - /* - * 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) { - switch (level) { - case 'normal': - case 'warn': - case 'error': - Util.Debug("Notification[" + level + "]:" + msg); - break; - default: - Util.Error("Invalid notification level: " + level); - 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 (options) { - this._onNotification(this, msg, level, options); + + // 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 + ':' + this._rfb_port + '/' + this._rfb_path; + Log.Info("connecting to " + uri); + + try { + // WebSocket.onopen transitions to the RFB init states + this._sock.open(uri, this._wsProtocols); + } catch (e) { + if (e.name === 'SyntaxError') { + this._fail("Invalid host or port value given", e); } else { - this._onNotification(this, msg, level); + this._fail("Error while connecting", e); } - }, + } - _handle_message: function () { - if (this._sock.rQlen() === 0) { - Util.Warn("handle_message called on an empty receive queue"); + Log.Debug("<< RFB.connect"); + }, + + _disconnect: function () { + Log.Debug(">> RFB.disconnect"); + this._cleanup(); + this._sock.close(); + this._print_stats(); + 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 i; + for (i = 0; i < this._encodings.length; i++) { + this._encStats[this._encodings[i][1]][0] = 0; + } + + for (i = 0; i < 4; i++) { + this._FBU.zlibs[i] = new Inflator(); + } + }, + + _print_stats: function () { + Log.Info("Encoding stats for this connection:"); + var i, s; + for (i = 0; i < this._encodings.length; i++) { + s = this._encStats[this._encodings[i][1]]; + if (s[0] + s[1] > 0) { + Log.Info(" " + this._encodings[i][0] + ": " + s[0] + " rects"); + } + } + + Log.Info("Encoding stats since page load:"); + for (i = 0; i < this._encodings.length; i++) { + s = this._encStats[this._encodings[i][1]]; + Log.Info(" " + this._encodings[i][0] + ": " + s[1] + " rects"); + } + }, + + _cleanup: function () { + if (!this._view_only) { this._keyboard.ungrab(); } + if (!this._view_only) { this._mouse.ungrab(); } + this._display.defaultCursor(); + if (Log.get_logging() !== 'debug') { + // Show noVNC logo on load and when disconnected, unless in + // debug mode + this._display.clear(); + } + }, + + /* + * Connection states: + * connecting + * connected + * disconnecting + * disconnected - permanent state + */ + _updateConnectionState: function (state) { + var oldstate = this._rfb_connection_state; + + if (state === oldstate) { + Log.Debug("Already in state '" + state + "', ignoring"); + return; + } + + // The 'disconnected' state is permanent for each RFB object + if (oldstate === 'disconnected') { + Log.Error("Tried changing state of a disconnected RFB object"); + return; + } + + // Ensure proper transitions before doing anything + switch (state) { + case 'connected': + if (oldstate !== 'connecting') { + Log.Error("Bad transition to connected state, " + + "previous connection state: " + oldstate); + return; + } + break; + + case 'disconnected': + if (oldstate !== 'disconnecting') { + Log.Error("Bad transition to disconnected state, " + + "previous connection state: " + oldstate); + return; + } + break; + + case 'connecting': + if (oldstate !== '') { + Log.Error("Bad transition to connecting state, " + + "previous connection state: " + oldstate); + return; + } + break; + + case 'disconnecting': + if (oldstate !== 'connected' && oldstate !== 'connecting') { + Log.Error("Bad transition to disconnecting state, " + + "previous connection state: " + oldstate); + return; + } + break; + + default: + Log.Error("Unknown connection state: " + state); return; - } + } - switch (this._rfb_connection_state) { - case 'disconnected': - Util.Error("Got data while disconnected"); - break; - case 'connected': - while (true) { - if (this._flushing) { - break; - } - if (!this._normal_msg()) { - break; - } - if (this._sock.rQlen() === 0) { - break; - } - } - break; - default: - this._init_msg(); - break; - } - }, + // State change actions - _handleKeyPress: function (keyevent) { - if (this._view_only) { return; } // View only, skip keyboard, events + this._rfb_connection_state = state; + this._onUpdateState(this, state, oldstate); - var down = (keyevent.type == 'keydown'); - if (this._qemuExtKeyEventSupported) { - var scancode = XtScancode[keyevent.code]; - if (scancode) { - var keysym = keyevent.keysym; - RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode); + var smsg = "New state '" + state + "', was '" + oldstate + "'."; + Log.Debug(smsg); + + if (this._disconnTimer && state !== 'disconnecting') { + Log.Debug("Clearing disconnect timer"); + clearTimeout(this._disconnTimer); + this._disconnTimer = null; + + // make sure we don't get a double event + this._sock.off('close'); + } + + switch (state) { + case 'disconnected': + // Call onDisconnected callback after onUpdateState 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); } else { - Util.Error('Unable to find a xt scancode for code = ' + keyevent.code); + // No reason means clean disconnect + this._onDisconnected(this); } - } else { - keysym = keyevent.keysym.keysym; - RFB.messages.keyEvent(this._sock, keysym, down); - } - }, + break; - _handleMouseButton: function (x, y, down, bmask) { - if (down) { - this._mouse_buttonMask |= bmask; - } else { - this._mouse_buttonMask ^= bmask; - } + case 'connecting': + this._connect(); + break; - if (this._viewportDrag) { - if (down && !this._viewportDragging) { - this._viewportDragging = true; - this._viewportDragPos = {'x': x, 'y': y}; + case 'disconnecting': + this._disconnect(); - // Skip sending mouse events - return; - } else { - this._viewportDragging = false; + this._disconnTimer = setTimeout(function () { + this._rfb_disconnect_reason = _("Disconnect timeout"); + this._updateConnectionState('disconnected'); + }.bind(this), this._disconnectTimeout * 1000); + break; + } + }, - // 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) { - RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask); + /* Print errors and disconnect + * + * The optional parameter 'details' is used for information that + * should be logged but not sent to the user interface. + */ + _fail: function (msg, details) { + var fullmsg = msg; + if (typeof details !== 'undefined') { + fullmsg = msg + " (" + details + ")"; + } + switch (this._rfb_connection_state) { + case 'disconnecting': + Log.Error("Failed when disconnecting: " + fullmsg); + break; + case 'connected': + Log.Error("Failed while connected: " + fullmsg); + break; + case 'connecting': + Log.Error("Failed when connecting: " + fullmsg); + break; + default: + Log.Error("RFB failure: " + fullmsg); + break; + } + this._rfb_disconnect_reason = msg; //This is sent to the UI + + // Transition to disconnected without waiting for socket to close + this._updateConnectionState('disconnecting'); + this._updateConnectionState('disconnected'); + + return false; + }, + + /* + * 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) { + switch (level) { + case 'normal': + case 'warn': + case 'error': + Log.Debug("Notification[" + level + "]:" + msg); + break; + default: + Log.Error("Invalid notification level: " + level); + return; + } + + if (options) { + this._onNotification(this, msg, level, options); + } else { + this._onNotification(this, msg, level); + } + }, + + _handle_message: function () { + if (this._sock.rQlen() === 0) { + Log.Warn("handle_message called on an empty receive queue"); + return; + } + + switch (this._rfb_connection_state) { + case 'disconnected': + Log.Error("Got data while disconnected"); + break; + case 'connected': + while (true) { + if (this._flushing) { + break; + } + if (!this._normal_msg()) { + break; + } + if (this._sock.rQlen() === 0) { + break; } - this._viewportHasMoved = false; } + break; + default: + this._init_msg(); + break; + } + }, + + _handleKeyPress: function (keyevent) { + if (this._view_only) { return; } // View only, skip keyboard, events + + var down = (keyevent.type == 'keydown'); + if (this._qemuExtKeyEventSupported) { + var scancode = XtScancode[keyevent.code]; + if (scancode) { + var keysym = keyevent.keysym; + RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode); + } else { + Log.Error('Unable to find a xt scancode for code = ' + keyevent.code); } + } else { + keysym = keyevent.keysym.keysym; + RFB.messages.keyEvent(this._sock, keysym, down); + } + }, - if (this._view_only) { return; } // View only, skip mouse events + _handleMouseButton: function (x, y, down, bmask) { + if (down) { + this._mouse_buttonMask |= bmask; + } else { + this._mouse_buttonMask ^= bmask; + } - if (this._rfb_connection_state !== 'connected') { return; } - RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); - }, - - _handleMouseMove: function (x, y) { - if (this._viewportDragging) { - var deltaX = this._viewportDragPos.x - x; - var deltaY = this._viewportDragPos.y - y; - - // The goal is to trigger on a certain physical width, the - // devicePixelRatio brings us a bit closer but is not optimal. - var dragThreshold = 10 * (window.devicePixelRatio || 1); - - if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || - Math.abs(deltaY) > dragThreshold)) { - this._viewportHasMoved = true; - - this._viewportDragPos = {'x': x, 'y': y}; - this._display.viewportChangePos(deltaX, deltaY); - } + if (this._viewportDrag) { + if (down && !this._viewportDragging) { + this._viewportDragging = true; + this._viewportDragPos = {'x': x, 'y': y}; // Skip sending mouse events return; - } - - if (this._view_only) { 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); - }, - - // Message Handlers - - _negotiate_protocol_version: function () { - if (this._sock.rQlen() < 12) { - return this._fail("Error while negotiating with server", - "Incomplete protocol version"); - } - - var sversion = this._sock.rQshiftStr(12).substr(4, 7); - Util.Info("Server ProtocolVersion: " + sversion); - var is_repeater = 0; - switch (sversion) { - case "000.000": // UltraVNC repeater - is_repeater = 1; - break; - case "003.003": - case "003.006": // UltraVNC - case "003.889": // Apple Remote Desktop - this._rfb_version = 3.3; - break; - case "003.007": - this._rfb_version = 3.7; - break; - case "003.008": - case "004.000": // Intel AMT KVM - case "004.001": // RealVNC 4.6 - case "005.000": // RealVNC 5.3 - this._rfb_version = 3.8; - break; - default: - return this._fail("Unsupported server", - "Invalid server version: " + sversion); - } - - if (is_repeater) { - var repeaterID = this._repeaterID; - while (repeaterID.length < 250) { - repeaterID += "\0"; - } - this._sock.send_string(repeaterID); - return true; - } - - if (this._rfb_version > this._rfb_max_version) { - this._rfb_version = this._rfb_max_version; - } - - var cversion = "00" + parseInt(this._rfb_version, 10) + - ".00" + ((this._rfb_version * 10) % 10); - this._sock.send_string("RFB " + cversion + "\n"); - Util.Debug('Sent ProtocolVersion: ' + cversion); - - this._rfb_init_state = 'Security'; - }, - - _negotiate_security: function () { - if (this._rfb_version >= 3.7) { - // Server sends supported list, client decides - var num_types = this._sock.rQshift8(); - if (this._sock.rQwait("security type", num_types, 1)) { return false; } - - if (num_types === 0) { - var strlen = this._sock.rQshift32(); - var reason = this._sock.rQshiftStr(strlen); - return this._fail("Error while negotiating with server", - "Security failure: " + reason); - } - - var types = this._sock.rQshiftBytes(num_types); - Util.Debug("Server security types: " + types); - - // Polyfill since IE and PhantomJS doesn't have - // TypedArray.includes() - function includes(item, array) { - for (var i = 0; i < array.length; i++) { - if (array[i] === item) { - return true; - } - } - return false; - } - - // Look for each auth in preferred order - this._rfb_auth_scheme = 0; - if (includes(1, types)) { - this._rfb_auth_scheme = 1; // None - } else if (includes(22, types)) { - this._rfb_auth_scheme = 22; // XVP - } else if (includes(16, types)) { - this._rfb_auth_scheme = 16; // Tight - } else if (includes(2, types)) { - this._rfb_auth_scheme = 2; // VNC Auth - } else { - return this._fail("Unsupported server", - "Unsupported security types: " + types); - } - - this._sock.send([this._rfb_auth_scheme]); } else { - // Server decides - if (this._sock.rQwait("security scheme", 4)) { return false; } - this._rfb_auth_scheme = this._sock.rQshift32(); - } + this._viewportDragging = false; - this._rfb_init_state = 'Authentication'; - Util.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme); - - return this._init_msg(); // jump to authentication - }, - - // 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); - return false; - } - - var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) + - String.fromCharCode(xvp_auth[1].length) + - xvp_auth[0] + - xvp_auth[1]; - 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(); - }, - - _negotiate_std_vnc_auth: function () { - if (this._rfb_password.length === 0) { - this._onPasswordRequired(this); - return false; - } - - if (this._sock.rQwait("auth challenge", 16)) { 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); - this._sock.send(response); - this._rfb_init_state = "SecurityResult"; - return true; - }, - - _negotiate_tight_tunnels: function (numTunnels) { - var clientSupportedTunnelTypes = { - 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } - }; - var serverSupportedTunnelTypes = {}; - // receive tunnel capabilities - for (var i = 0; i < numTunnels; i++) { - var cap_code = this._sock.rQshift32(); - var cap_vendor = this._sock.rQshiftStr(4); - var cap_signature = this._sock.rQshiftStr(8); - serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; - } - - // choose the notunnel type - if (serverSupportedTunnelTypes[0]) { - if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || - serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { - return this._fail("Unsupported server", - "Client's tunnel type had the incorrect " + - "vendor or signature"); + // 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) { + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask); } - this._sock.send([0, 0, 0, 0]); // use NOTUNNEL - return false; // wait until we receive the sub auth count to continue + this._viewportHasMoved = false; + } + } + + if (this._view_only) { 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); + }, + + _handleMouseMove: function (x, y) { + if (this._viewportDragging) { + var deltaX = this._viewportDragPos.x - x; + var deltaY = this._viewportDragPos.y - y; + + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + var dragThreshold = 10 * (window.devicePixelRatio || 1); + + if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || + Math.abs(deltaY) > dragThreshold)) { + this._viewportHasMoved = true; + + this._viewportDragPos = {'x': x, 'y': y}; + this._display.viewportChangePos(deltaX, deltaY); + } + + // Skip sending mouse events + return; + } + + if (this._view_only) { 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); + }, + + // Message Handlers + + _negotiate_protocol_version: function () { + if (this._sock.rQlen() < 12) { + return this._fail("Error while negotiating with server", + "Incomplete protocol version"); + } + + var sversion = this._sock.rQshiftStr(12).substr(4, 7); + Log.Info("Server ProtocolVersion: " + sversion); + var is_repeater = 0; + switch (sversion) { + case "000.000": // UltraVNC repeater + is_repeater = 1; + break; + case "003.003": + case "003.006": // UltraVNC + case "003.889": // Apple Remote Desktop + this._rfb_version = 3.3; + break; + case "003.007": + this._rfb_version = 3.7; + break; + case "003.008": + case "004.000": // Intel AMT KVM + case "004.001": // RealVNC 4.6 + case "005.000": // RealVNC 5.3 + this._rfb_version = 3.8; + break; + default: + return this._fail("Unsupported server", + "Invalid server version: " + sversion); + } + + if (is_repeater) { + var repeaterID = this._repeaterID; + while (repeaterID.length < 250) { + repeaterID += "\0"; + } + this._sock.send_string(repeaterID); + return true; + } + + if (this._rfb_version > this._rfb_max_version) { + this._rfb_version = this._rfb_max_version; + } + + var cversion = "00" + parseInt(this._rfb_version, 10) + + ".00" + ((this._rfb_version * 10) % 10); + this._sock.send_string("RFB " + cversion + "\n"); + Log.Debug('Sent ProtocolVersion: ' + cversion); + + this._rfb_init_state = 'Security'; + }, + + _negotiate_security: function () { + // Polyfill since IE and PhantomJS doesn't have + // TypedArray.includes() + function includes(item, array) { + for (var i = 0; i < array.length; i++) { + if (array[i] === item) { + return true; + } + } + return false; + } + + if (this._rfb_version >= 3.7) { + // Server sends supported list, client decides + var num_types = this._sock.rQshift8(); + if (this._sock.rQwait("security type", num_types, 1)) { return false; } + + if (num_types === 0) { + var strlen = this._sock.rQshift32(); + var reason = this._sock.rQshiftStr(strlen); + return this._fail("Error while negotiating with server", + "Security failure: " + reason); + } + + var types = this._sock.rQshiftBytes(num_types); + Log.Debug("Server security types: " + types); + + // Look for each auth in preferred order + this._rfb_auth_scheme = 0; + if (includes(1, types)) { + this._rfb_auth_scheme = 1; // None + } else if (includes(22, types)) { + this._rfb_auth_scheme = 22; // XVP + } else if (includes(16, types)) { + this._rfb_auth_scheme = 16; // Tight + } else if (includes(2, types)) { + this._rfb_auth_scheme = 2; // VNC Auth } else { return this._fail("Unsupported server", - "Server wanted tunnels, but doesn't support " + - "the notunnel type"); - } - }, - - _negotiate_tight_auth: function () { - if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation - if (this._sock.rQwait("num tunnels", 4)) { return false; } - var numTunnels = this._sock.rQshift32(); - if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } - - this._rfb_tightvnc = true; - - if (numTunnels > 0) { - this._negotiate_tight_tunnels(numTunnels); - return false; // wait until we receive the sub auth to continue - } + "Unsupported security types: " + types); } - // second pass, do the sub-auth negotiation - if (this._sock.rQwait("sub auth count", 4)) { return false; } - var subAuthCount = this._sock.rQshift32(); - if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected - this._rfb_init_state = 'SecurityResult'; - return true; + this._sock.send([this._rfb_auth_scheme]); + } else { + // Server decides + if (this._sock.rQwait("security scheme", 4)) { return false; } + this._rfb_auth_scheme = this._sock.rQshift32(); + } + + this._rfb_init_state = 'Authentication'; + Log.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme); + + return this._init_msg(); // jump to authentication + }, + + // 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); + return false; + } + + var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) + + String.fromCharCode(xvp_auth[1].length) + + xvp_auth[0] + + xvp_auth[1]; + 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(); + }, + + _negotiate_std_vnc_auth: function () { + if (this._rfb_password.length === 0) { + this._onPasswordRequired(this); + return false; + } + + if (this._sock.rQwait("auth challenge", 16)) { 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); + this._sock.send(response); + this._rfb_init_state = "SecurityResult"; + return true; + }, + + _negotiate_tight_tunnels: function (numTunnels) { + var clientSupportedTunnelTypes = { + 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } + }; + var serverSupportedTunnelTypes = {}; + // receive tunnel capabilities + for (var i = 0; i < numTunnels; i++) { + var cap_code = this._sock.rQshift32(); + var cap_vendor = this._sock.rQshiftStr(4); + var cap_signature = this._sock.rQshiftStr(8); + serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; + } + + // choose the notunnel type + if (serverSupportedTunnelTypes[0]) { + if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || + serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { + return this._fail("Unsupported server", + "Client's tunnel type had the incorrect " + + "vendor or signature"); } - - if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } - - var clientSupportedTypes = { - 'STDVNOAUTH__': 1, - 'STDVVNCAUTH_': 2 - }; - - var serverSupportedTypes = []; - - for (var i = 0; i < subAuthCount; i++) { - var capNum = this._sock.rQshift32(); - var capabilities = this._sock.rQshiftStr(12); - serverSupportedTypes.push(capabilities); - } - - for (var authType in clientSupportedTypes) { - if (serverSupportedTypes.indexOf(authType) != -1) { - this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); - - switch (authType) { - case 'STDVNOAUTH__': // no auth - this._rfb_init_state = 'SecurityResult'; - return true; - case 'STDVVNCAUTH_': // VNC auth - this._rfb_auth_scheme = 2; - return this._init_msg(); - default: - return this._fail("Unsupported server", - "Unsupported tiny auth scheme: " + - authType); - } - } - } - + this._sock.send([0, 0, 0, 0]); // use NOTUNNEL + return false; // wait until we receive the sub auth count to continue + } else { return this._fail("Unsupported server", - "No supported sub-auth types!"); - }, + "Server wanted tunnels, but doesn't support " + + "the notunnel type"); + } + }, - _negotiate_authentication: function () { - switch (this._rfb_auth_scheme) { - case 0: // connection failed - if (this._sock.rQwait("auth reason", 4)) { return false; } - var strlen = this._sock.rQshift32(); - var reason = this._sock.rQshiftStr(strlen); - return this._fail("Authentication failure", reason); + _negotiate_tight_auth: function () { + if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation + if (this._sock.rQwait("num tunnels", 4)) { return false; } + var numTunnels = this._sock.rQshift32(); + if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } - case 1: // no auth - if (this._rfb_version >= 3.8) { + this._rfb_tightvnc = true; + + if (numTunnels > 0) { + this._negotiate_tight_tunnels(numTunnels); + return false; // wait until we receive the sub auth to continue + } + } + + // second pass, do the sub-auth negotiation + if (this._sock.rQwait("sub auth count", 4)) { return false; } + var subAuthCount = this._sock.rQshift32(); + if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected + this._rfb_init_state = 'SecurityResult'; + return true; + } + + if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } + + var clientSupportedTypes = { + 'STDVNOAUTH__': 1, + 'STDVVNCAUTH_': 2 + }; + + var serverSupportedTypes = []; + + for (var i = 0; i < subAuthCount; i++) { + var capNum = this._sock.rQshift32(); + var capabilities = this._sock.rQshiftStr(12); + serverSupportedTypes.push(capabilities); + } + + for (var authType in clientSupportedTypes) { + if (serverSupportedTypes.indexOf(authType) != -1) { + this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); + + switch (authType) { + case 'STDVNOAUTH__': // no auth this._rfb_init_state = 'SecurityResult'; return true; - } - this._rfb_init_state = 'ClientInitialisation'; - return this._init_msg(); - - case 22: // XVP auth - return this._negotiate_xvp_auth(); - - case 2: // VNC authentication - return this._negotiate_std_vnc_auth(); - - case 16: // TightVNC Security Type - return this._negotiate_tight_auth(); - - default: - return this._fail("Unsupported server", - "Unsupported auth scheme: " + - this._rfb_auth_scheme); + case 'STDVVNCAUTH_': // VNC auth + this._rfb_auth_scheme = 2; + return this._init_msg(); + default: + return this._fail("Unsupported server", + "Unsupported tiny auth scheme: " + + authType); + } } - }, + } - _handle_security_result: function () { - if (this._sock.rQwait('VNC auth response ', 4)) { return false; } - switch (this._sock.rQshift32()) { - case 0: // OK - this._rfb_init_state = 'ClientInitialisation'; - Util.Debug('Authentication OK'); - return this._init_msg(); - case 1: // failed - if (this._rfb_version >= 3.8) { - var length = this._sock.rQshift32(); - if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; } - var reason = this._sock.rQshiftStr(length); - return this._fail("Authentication failure", reason); - } else { - return this._fail("Authentication failure"); - } - return false; - case 2: - return this._fail("Too many authentication attempts"); - default: - return this._fail("Unsupported server", - "Unknown SecurityResult"); - } - }, + return this._fail("Unsupported server", + "No supported sub-auth types!"); + }, - _negotiate_server_init: function () { - if (this._sock.rQwait("server initialization", 24)) { return false; } + _negotiate_authentication: function () { + switch (this._rfb_auth_scheme) { + case 0: // connection failed + if (this._sock.rQwait("auth reason", 4)) { return false; } + var strlen = this._sock.rQshift32(); + var reason = this._sock.rQshiftStr(strlen); + return this._fail("Authentication failure", reason); - /* Screen size */ - this._fb_width = this._sock.rQshift16(); - this._fb_height = this._sock.rQshift16(); - this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); + case 1: // no auth + if (this._rfb_version >= 3.8) { + this._rfb_init_state = 'SecurityResult'; + return true; + } + this._rfb_init_state = 'ClientInitialisation'; + return this._init_msg(); - /* PIXEL_FORMAT */ - var bpp = this._sock.rQshift8(); - var depth = this._sock.rQshift8(); - var big_endian = this._sock.rQshift8(); - var true_color = this._sock.rQshift8(); + case 22: // XVP auth + return this._negotiate_xvp_auth(); - var red_max = this._sock.rQshift16(); - var green_max = this._sock.rQshift16(); - var blue_max = this._sock.rQshift16(); - var red_shift = this._sock.rQshift8(); - var green_shift = this._sock.rQshift8(); - var blue_shift = this._sock.rQshift8(); - this._sock.rQskipBytes(3); // padding + case 2: // VNC authentication + return this._negotiate_std_vnc_auth(); - // NB(directxman12): we don't want to call any callbacks or print messages until - // *after* we're past the point where we could backtrack + case 16: // TightVNC Security Type + return this._negotiate_tight_auth(); - /* Connection name/title */ - var name_length = this._sock.rQshift32(); - if (this._sock.rQwait('server init name', name_length, 24)) { return false; } - this._fb_name = Util.decodeUTF8(this._sock.rQshiftStr(name_length)); + default: + return this._fail("Unsupported server", + "Unsupported auth scheme: " + + this._rfb_auth_scheme); + } + }, - if (this._rfb_tightvnc) { - if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } - // In TightVNC mode, ServerInit message is extended - var numServerMessages = this._sock.rQshift16(); - var numClientMessages = this._sock.rQshift16(); - var numEncodings = this._sock.rQshift16(); - this._sock.rQskipBytes(2); // padding + _handle_security_result: function () { + if (this._sock.rQwait('VNC auth response ', 4)) { return false; } + switch (this._sock.rQshift32()) { + case 0: // OK + this._rfb_init_state = 'ClientInitialisation'; + Log.Debug('Authentication OK'); + return this._init_msg(); + case 1: // failed + if (this._rfb_version >= 3.8) { + var length = this._sock.rQshift32(); + if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; } + var reason = this._sock.rQshiftStr(length); + return this._fail("Authentication failure", reason); + } else { + return this._fail("Authentication failure"); + } + return false; + case 2: + return this._fail("Too many authentication attempts"); + default: + return this._fail("Unsupported server", + "Unknown SecurityResult"); + } + }, - var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; - if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } + _negotiate_server_init: function () { + if (this._sock.rQwait("server initialization", 24)) { return false; } - // we don't actually do anything with the capability information that TIGHT sends, - // so we just skip the all of this. + /* Screen size */ + this._fb_width = this._sock.rQshift16(); + this._fb_height = this._sock.rQshift16(); + this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); - // TIGHT server message capabilities - this._sock.rQskipBytes(16 * numServerMessages); + /* PIXEL_FORMAT */ + var bpp = this._sock.rQshift8(); + var depth = this._sock.rQshift8(); + var big_endian = this._sock.rQshift8(); + var true_color = this._sock.rQshift8(); - // TIGHT client message capabilities - this._sock.rQskipBytes(16 * numClientMessages); + var red_max = this._sock.rQshift16(); + var green_max = this._sock.rQshift16(); + var blue_max = this._sock.rQshift16(); + var red_shift = this._sock.rQshift8(); + var green_shift = this._sock.rQshift8(); + var blue_shift = this._sock.rQshift8(); + this._sock.rQskipBytes(3); // padding - // TIGHT encoding capabilities - this._sock.rQskipBytes(16 * numEncodings); - } + // NB(directxman12): we don't want to call any callbacks or print messages until + // *after* we're past the point where we could backtrack - // NB(directxman12): these are down here so that we don't run them multiple times - // if we backtrack - Util.Info("Screen: " + this._fb_width + "x" + this._fb_height + - ", bpp: " + bpp + ", depth: " + depth + - ", big_endian: " + big_endian + - ", true_color: " + true_color + - ", red_max: " + red_max + - ", green_max: " + green_max + - ", blue_max: " + blue_max + - ", red_shift: " + red_shift + - ", green_shift: " + green_shift + - ", blue_shift: " + blue_shift); + /* Connection name/title */ + var name_length = this._sock.rQshift32(); + if (this._sock.rQwait('server init name', name_length, 24)) { return false; } + this._fb_name = decodeUTF8(this._sock.rQshiftStr(name_length)); - if (big_endian !== 0) { - Util.Warn("Server native endian is not little endian"); - } + if (this._rfb_tightvnc) { + if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } + // In TightVNC mode, ServerInit message is extended + var numServerMessages = this._sock.rQshift16(); + var numClientMessages = this._sock.rQshift16(); + var numEncodings = this._sock.rQshift16(); + this._sock.rQskipBytes(2); // padding - if (red_shift !== 16) { - Util.Warn("Server native red-shift is not 16"); - } + var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; + if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } - if (blue_shift !== 0) { - Util.Warn("Server native blue-shift is not 0"); - } + // we don't actually do anything with the capability information that TIGHT sends, + // so we just skip the all of this. - // we're past the point where we could backtrack, so it's safe to call this - this._onDesktopName(this, this._fb_name); + // TIGHT server message capabilities + this._sock.rQskipBytes(16 * numServerMessages); - if (this._true_color && this._fb_name === "Intel(r) AMT KVM") { - Util.Warn("Intel AMT KVM only supports 8/16 bit depths. Disabling true color"); - this._true_color = false; - } + // TIGHT client message capabilities + this._sock.rQskipBytes(16 * numClientMessages); - this._display.set_true_color(this._true_color); - this._display.resize(this._fb_width, this._fb_height); - this._onFBResize(this, this._fb_width, this._fb_height); + // TIGHT encoding capabilities + this._sock.rQskipBytes(16 * numEncodings); + } - if (!this._view_only) { this._keyboard.grab(); } - if (!this._view_only) { this._mouse.grab(); } + // NB(directxman12): these are down here so that we don't run them multiple times + // if we backtrack + Log.Info("Screen: " + this._fb_width + "x" + this._fb_height + + ", bpp: " + bpp + ", depth: " + depth + + ", big_endian: " + big_endian + + ", true_color: " + true_color + + ", red_max: " + red_max + + ", green_max: " + green_max + + ", blue_max: " + blue_max + + ", red_shift: " + red_shift + + ", green_shift: " + green_shift + + ", blue_shift: " + blue_shift); - if (this._true_color) { - this._fb_Bpp = 4; - this._fb_depth = 3; - } else { - this._fb_Bpp = 1; - this._fb_depth = 1; - } + if (big_endian !== 0) { + Log.Warn("Server native endian is not little endian"); + } - RFB.messages.pixelFormat(this._sock, this._fb_Bpp, this._fb_depth, this._true_color); - RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._true_color); - RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height); + if (red_shift !== 16) { + Log.Warn("Server native red-shift is not 16"); + } - this._timing.fbu_rt_start = (new Date()).getTime(); - this._timing.pixels = 0; + if (blue_shift !== 0) { + Log.Warn("Server native blue-shift is not 0"); + } - this._updateConnectionState('connected'); - return true; - }, + // we're past the point where we could backtrack, so it's safe to call this + this._onDesktopName(this, this._fb_name); - /* RFB protocol initialization states: - * ProtocolVersion - * Security - * Authentication - * SecurityResult - * ClientInitialization - not triggered by server message - * ServerInitialization + if (this._true_color && this._fb_name === "Intel(r) AMT KVM") { + Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Disabling true color"); + this._true_color = false; + } + + this._display.set_true_color(this._true_color); + this._display.resize(this._fb_width, this._fb_height); + this._onFBResize(this, this._fb_width, this._fb_height); + + if (!this._view_only) { this._keyboard.grab(); } + if (!this._view_only) { this._mouse.grab(); } + + if (this._true_color) { + this._fb_Bpp = 4; + this._fb_depth = 3; + } else { + this._fb_Bpp = 1; + this._fb_depth = 1; + } + + RFB.messages.pixelFormat(this._sock, this._fb_Bpp, this._fb_depth, this._true_color); + RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._true_color); + RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height); + + this._timing.fbu_rt_start = (new Date()).getTime(); + this._timing.pixels = 0; + + this._updateConnectionState('connected'); + return true; + }, + + /* RFB protocol initialization states: + * ProtocolVersion + * Security + * Authentication + * SecurityResult + * ClientInitialization - not triggered by server message + * ServerInitialization + */ + _init_msg: function () { + switch (this._rfb_init_state) { + case 'ProtocolVersion': + return this._negotiate_protocol_version(); + + case 'Security': + return this._negotiate_security(); + + case 'Authentication': + return this._negotiate_authentication(); + + case 'SecurityResult': + return this._handle_security_result(); + + case 'ClientInitialisation': + this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation + this._rfb_init_state = 'ServerInitialisation'; + return true; + + case 'ServerInitialisation': + return this._negotiate_server_init(); + + default: + return this._fail("Internal error", "Unknown init state: " + + this._rfb_init_state); + } + }, + + _handle_set_colour_map_msg: function () { + Log.Debug("SetColorMapEntries"); + this._sock.rQskip8(); // Padding + + var first_colour = this._sock.rQshift16(); + var num_colours = this._sock.rQshift16(); + if (this._sock.rQwait('SetColorMapEntries', num_colours * 6, 6)) { return false; } + + for (var c = 0; c < num_colours; c++) { + var red = parseInt(this._sock.rQshift16() / 256, 10); + var green = parseInt(this._sock.rQshift16() / 256, 10); + var blue = parseInt(this._sock.rQshift16() / 256, 10); + this._display.set_colourMap([blue, green, red], first_colour + c); + } + Log.Debug("colourMap: " + this._display.get_colourMap()); + Log.Info("Registered " + num_colours + " colourMap entries"); + + return true; + }, + + _handle_server_cut_text: function () { + Log.Debug("ServerCutText"); + if (this._view_only) { return true; } + + if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; } + this._sock.rQskipBytes(3); // Padding + var length = this._sock.rQshift32(); + if (this._sock.rQwait("ServerCutText", length, 8)) { return false; } + + var text = this._sock.rQshiftStr(length); + this._onClipboard(this, text); + + return true; + }, + + _handle_server_fence_msg: function() { + if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; } + this._sock.rQskipBytes(3); // Padding + var flags = this._sock.rQshift32(); + var length = this._sock.rQshift8(); + + if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; } + + if (length > 64) { + Log.Warn("Bad payload length (" + length + ") in fence response"); + length = 64; + } + + var payload = this._sock.rQshiftStr(length); + + this._supportsFence = true; + + /* + * Fence flags + * + * (1<<0) - BlockBefore + * (1<<1) - BlockAfter + * (1<<2) - SyncNext + * (1<<31) - Request */ - _init_msg: function () { - switch (this._rfb_init_state) { - case 'ProtocolVersion': - return this._negotiate_protocol_version(); - case 'Security': - return this._negotiate_security(); + if (!(flags & (1<<31))) { + return this._fail("Internal error", + "Unexpected fence response"); + } - case 'Authentication': - return this._negotiate_authentication(); + // Filter out unsupported flags + // FIXME: support syncNext + flags &= (1<<0) | (1<<1); - case 'SecurityResult': - return this._handle_security_result(); + // BlockBefore and BlockAfter are automatically handled by + // the fact that we process each incoming message + // synchronuosly. + RFB.messages.clientFence(this._sock, flags, payload); - case 'ClientInitialisation': - this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation - this._rfb_init_state = 'ServerInitialisation'; - return true; + return true; + }, - case 'ServerInitialisation': - return this._negotiate_server_init(); + _handle_xvp_msg: function () { + if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; } + this._sock.rQskip8(); // Padding + var xvp_ver = this._sock.rQshift8(); + var xvp_msg = this._sock.rQshift8(); - default: - return this._fail("Internal error", "Unknown init state: " + - this._rfb_init_state); - } - }, + switch (xvp_msg) { + case 0: // XVP_FAIL + Log.Error("Operation Failed"); + this._notification("XVP Operation Failed", 'error'); + break; + 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); + break; + default: + this._fail("Unexpected server message", + "Illegal server XVP message " + xvp_msg); + break; + } - _handle_set_colour_map_msg: function () { - Util.Debug("SetColorMapEntries"); + return true; + }, + + _normal_msg: function () { + var msg_type; + + if (this._FBU.rects > 0) { + msg_type = 0; + } else { + msg_type = this._sock.rQshift8(); + } + + switch (msg_type) { + case 0: // FramebufferUpdate + var ret = this._framebufferUpdate(); + if (ret && !this._enabledContinuousUpdates) { + RFB.messages.fbUpdateRequest(this._sock, true, 0, 0, + this._fb_width, this._fb_height); + } + return ret; + + case 1: // SetColorMapEntries + return this._handle_set_colour_map_msg(); + + case 2: // Bell + Log.Debug("Bell"); + this._onBell(this); + return true; + + case 3: // ServerCutText + return this._handle_server_cut_text(); + + case 150: // EndOfContinuousUpdates + var first = !(this._supportsContinuousUpdates); + this._supportsContinuousUpdates = true; + this._enabledContinuousUpdates = false; + if (first) { + this._enabledContinuousUpdates = true; + this._updateContinuousUpdates(); + Log.Info("Enabling continuous updates."); + } else { + // FIXME: We need to send a framebufferupdaterequest here + // if we add support for turning off continuous updates + } + return true; + + case 248: // ServerFence + return this._handle_server_fence_msg(); + + case 250: // XVP + return this._handle_xvp_msg(); + + default: + this._fail("Unexpected server message", "Type:" + msg_type); + Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); + return true; + } + }, + + _onFlush: function() { + this._flushing = false; + // Resume processing + if (this._sock.rQlen() > 0) { + this._handle_message(); + } + }, + + _framebufferUpdate: function () { + var ret = true; + var now; + + if (this._FBU.rects === 0) { + if (this._sock.rQwait("FBU header", 3, 1)) { return false; } this._sock.rQskip8(); // Padding - - var first_colour = this._sock.rQshift16(); - var num_colours = this._sock.rQshift16(); - if (this._sock.rQwait('SetColorMapEntries', num_colours * 6, 6)) { return false; } - - for (var c = 0; c < num_colours; c++) { - var red = parseInt(this._sock.rQshift16() / 256, 10); - var green = parseInt(this._sock.rQshift16() / 256, 10); - var blue = parseInt(this._sock.rQshift16() / 256, 10); - this._display.set_colourMap([blue, green, red], first_colour + c); - } - Util.Debug("colourMap: " + this._display.get_colourMap()); - Util.Info("Registered " + num_colours + " colourMap entries"); - - return true; - }, - - _handle_server_cut_text: function () { - Util.Debug("ServerCutText"); - if (this._view_only) { return true; } - - if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; } - this._sock.rQskipBytes(3); // Padding - var length = this._sock.rQshift32(); - if (this._sock.rQwait("ServerCutText", length, 8)) { return false; } - - var text = this._sock.rQshiftStr(length); - this._onClipboard(this, text); - - return true; - }, - - _handle_server_fence_msg: function() { - if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; } - this._sock.rQskipBytes(3); // Padding - var flags = this._sock.rQshift32(); - var length = this._sock.rQshift8(); - - if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; } - - if (length > 64) { - Util.Warn("Bad payload length (" + length + ") in fence response"); - length = 64; - } - - var payload = this._sock.rQshiftStr(length); - - this._supportsFence = true; - - /* - * Fence flags - * - * (1<<0) - BlockBefore - * (1<<1) - BlockAfter - * (1<<2) - SyncNext - * (1<<31) - Request - */ - - if (!(flags & (1<<31))) { - return this._fail("Internal error", - "Unexpected fence response"); - } - - // Filter out unsupported flags - // FIXME: support syncNext - flags &= (1<<0) | (1<<1); - - // BlockBefore and BlockAfter are automatically handled by - // the fact that we process each incoming message - // synchronuosly. - RFB.messages.clientFence(this._sock, flags, payload); - - return true; - }, - - _handle_xvp_msg: function () { - if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; } - this._sock.rQskip8(); // Padding - var xvp_ver = this._sock.rQshift8(); - var xvp_msg = this._sock.rQshift8(); - - switch (xvp_msg) { - case 0: // XVP_FAIL - Util.Error("Operation Failed"); - this._notification("XVP Operation Failed", 'error'); - break; - case 1: // XVP_INIT - this._rfb_xvp_ver = xvp_ver; - Util.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")"); - this._onXvpInit(this._rfb_xvp_ver); - break; - default: - this._fail("Unexpected server message", - "Illegal server XVP message " + xvp_msg); - break; - } - - return true; - }, - - _normal_msg: function () { - var msg_type; - - if (this._FBU.rects > 0) { - msg_type = 0; - } else { - msg_type = this._sock.rQshift8(); - } - - switch (msg_type) { - case 0: // FramebufferUpdate - var ret = this._framebufferUpdate(); - if (ret && !this._enabledContinuousUpdates) { - RFB.messages.fbUpdateRequest(this._sock, true, 0, 0, - this._fb_width, this._fb_height); - } - return ret; - - case 1: // SetColorMapEntries - return this._handle_set_colour_map_msg(); - - case 2: // Bell - Util.Debug("Bell"); - this._onBell(this); - return true; - - case 3: // ServerCutText - return this._handle_server_cut_text(); - - case 150: // EndOfContinuousUpdates - var first = !(this._supportsContinuousUpdates); - this._supportsContinuousUpdates = true; - this._enabledContinuousUpdates = false; - if (first) { - this._enabledContinuousUpdates = true; - this._updateContinuousUpdates(); - Util.Info("Enabling continuous updates."); - } else { - // FIXME: We need to send a framebufferupdaterequest here - // if we add support for turning off continuous updates - } - return true; - - case 248: // ServerFence - return this._handle_server_fence_msg(); - - case 250: // XVP - return this._handle_xvp_msg(); - - default: - this._fail("Unexpected server message", "Type:" + msg_type); - Util.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); - return true; - } - }, - - _onFlush: function() { - this._flushing = false; - // Resume processing - if (this._sock.rQlen() > 0) { - this._handle_message(); - } - }, - - _framebufferUpdate: function () { - var ret = true; - var now; - - if (this._FBU.rects === 0) { - if (this._sock.rQwait("FBU header", 3, 1)) { return false; } - this._sock.rQskip8(); // Padding - this._FBU.rects = this._sock.rQshift16(); - this._FBU.bytes = 0; - this._timing.cur_fbu = 0; - if (this._timing.fbu_rt_start > 0) { - now = (new Date()).getTime(); - Util.Info("First FBU latency: " + (now - this._timing.fbu_rt_start)); - } - - // Make sure the previous frame is fully rendered first - // to avoid building up an excessive queue - if (this._display.pending()) { - this._flushing = true; - this._display.flush(); - return false; - } - } - - while (this._FBU.rects > 0) { - if (this._rfb_connection_state !== 'connected') { return false; } - - if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; } - if (this._FBU.bytes === 0) { - if (this._sock.rQwait("rect header", 12)) { return false; } - /* New FramebufferUpdate */ - - var hdr = this._sock.rQshiftBytes(12); - this._FBU.x = (hdr[0] << 8) + hdr[1]; - this._FBU.y = (hdr[2] << 8) + hdr[3]; - this._FBU.width = (hdr[4] << 8) + hdr[5]; - this._FBU.height = (hdr[6] << 8) + hdr[7]; - 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': this._encNames[this._FBU.encoding]}); - - if (!this._encNames[this._FBU.encoding]) { - this._fail("Unexpected server message", - "Unsupported encoding " + - this._FBU.encoding); - return false; - } - } - - this._timing.last_fbu = (new Date()).getTime(); - - ret = this._encHandlers[this._FBU.encoding](); - + this._FBU.rects = this._sock.rQshift16(); + this._FBU.bytes = 0; + this._timing.cur_fbu = 0; + if (this._timing.fbu_rt_start > 0) { now = (new Date()).getTime(); - this._timing.cur_fbu += (now - this._timing.last_fbu); - - if (ret) { - this._encStats[this._FBU.encoding][0]++; - this._encStats[this._FBU.encoding][1]++; - this._timing.pixels += this._FBU.width * this._FBU.height; - } - - if (this._timing.pixels >= (this._fb_width * this._fb_height)) { - if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) || - this._timing.fbu_rt_start > 0) { - this._timing.full_fbu_total += this._timing.cur_fbu; - this._timing.full_fbu_cnt++; - Util.Info("Timing of full FBU, curr: " + - this._timing.cur_fbu + ", total: " + - this._timing.full_fbu_total + ", cnt: " + - this._timing.full_fbu_cnt + ", avg: " + - (this._timing.full_fbu_total / this._timing.full_fbu_cnt)); - } - - if (this._timing.fbu_rt_start > 0) { - var fbu_rt_diff = now - this._timing.fbu_rt_start; - this._timing.fbu_rt_total += fbu_rt_diff; - this._timing.fbu_rt_cnt++; - Util.Info("full FBU round-trip, cur: " + - fbu_rt_diff + ", total: " + - this._timing.fbu_rt_total + ", cnt: " + - this._timing.fbu_rt_cnt + ", avg: " + - (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt)); - this._timing.fbu_rt_start = 0; - } - } - - if (!ret) { return ret; } // need more data + Log.Info("First FBU latency: " + (now - this._timing.fbu_rt_start)); } - this._display.flip(); + // Make sure the previous frame is fully rendered first + // to avoid building up an excessive queue + if (this._display.pending()) { + this._flushing = true; + this._display.flush(); + return false; + } + } - this._onFBUComplete(this, + while (this._FBU.rects > 0) { + if (this._rfb_connection_state !== 'connected') { return false; } + + if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; } + if (this._FBU.bytes === 0) { + if (this._sock.rQwait("rect header", 12)) { return false; } + /* New FramebufferUpdate */ + + var hdr = this._sock.rQshiftBytes(12); + this._FBU.x = (hdr[0] << 8) + hdr[1]; + this._FBU.y = (hdr[2] << 8) + hdr[3]; + this._FBU.width = (hdr[4] << 8) + hdr[5]; + this._FBU.height = (hdr[6] << 8) + hdr[7]; + 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': this._encNames[this._FBU.encoding]}); - return true; // We finished this FBU - }, - - _updateContinuousUpdates: function() { - if (!this._enabledContinuousUpdates) { return; } - - RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0, - this._fb_width, this._fb_height); - } - }; - - Util.make_properties(RFB, [ - ['target', 'wo', 'dom'], // VNC display rendering Canvas object - ['focusContainer', 'wo', 'dom'], // DOM element that captures keyboard input - ['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption - ['true_color', 'rw', 'bool'], // Request true color pixel data - ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor - ['shared', 'rw', 'bool'], // Request shared mode - ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard - ['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 { - Util.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") { - RFB.messages.clientEncodings(this._sock, this._encodings, cursor, - this._true_color); - } - }; - - 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; }; - - // Class Methods - RFB.messages = { - keyEvent: function (sock, keysym, down) { - var buff = sock._sQ; - var offset = sock._sQlen; - - buff[offset] = 4; // msg-type - buff[offset + 1] = down; - - buff[offset + 2] = 0; - buff[offset + 3] = 0; - - buff[offset + 4] = (keysym >> 24); - buff[offset + 5] = (keysym >> 16); - buff[offset + 6] = (keysym >> 8); - buff[offset + 7] = keysym; - - sock._sQlen += 8; - sock.flush(); - }, - - QEMUExtendedKeyEvent: function (sock, keysym, down, keycode) { - function getRFBkeycode(xt_scancode) { - var upperByte = (keycode >> 8); - var lowerByte = (keycode & 0x00ff); - if (upperByte === 0xe0 && lowerByte < 0x7f) { - lowerByte = lowerByte | 0x80; - return lowerByte; - } - return xt_scancode; - } - - var buff = sock._sQ; - var offset = sock._sQlen; - - buff[offset] = 255; // msg-type - buff[offset + 1] = 0; // sub msg-type - - buff[offset + 2] = (down >> 8); - buff[offset + 3] = down; - - buff[offset + 4] = (keysym >> 24); - buff[offset + 5] = (keysym >> 16); - buff[offset + 6] = (keysym >> 8); - buff[offset + 7] = keysym; - - var RFBkeycode = getRFBkeycode(keycode); - - buff[offset + 8] = (RFBkeycode >> 24); - buff[offset + 9] = (RFBkeycode >> 16); - buff[offset + 10] = (RFBkeycode >> 8); - buff[offset + 11] = RFBkeycode; - - sock._sQlen += 12; - sock.flush(); - }, - - pointerEvent: function (sock, x, y, mask) { - var buff = sock._sQ; - var offset = sock._sQlen; - - buff[offset] = 5; // msg-type - - buff[offset + 1] = mask; - - buff[offset + 2] = x >> 8; - buff[offset + 3] = x; - - buff[offset + 4] = y >> 8; - buff[offset + 5] = y; - - sock._sQlen += 6; - sock.flush(); - }, - - // TODO(directxman12): make this unicode compatible? - clientCutText: function (sock, text) { - var buff = sock._sQ; - var offset = sock._sQlen; - - buff[offset] = 6; // msg-type - - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding - - var n = text.length; - - buff[offset + 4] = n >> 24; - buff[offset + 5] = n >> 16; - buff[offset + 6] = n >> 8; - buff[offset + 7] = n; - - for (var i = 0; i < n; i++) { - buff[offset + 8 + i] = text.charCodeAt(i); - } - - sock._sQlen += 8 + n; - sock.flush(); - }, - - setDesktopSize: function (sock, width, height, id, flags) { - var buff = sock._sQ; - var offset = sock._sQlen; - - buff[offset] = 251; // msg-type - buff[offset + 1] = 0; // padding - buff[offset + 2] = width >> 8; // width - buff[offset + 3] = width; - buff[offset + 4] = height >> 8; // height - buff[offset + 5] = height; - - buff[offset + 6] = 1; // number-of-screens - buff[offset + 7] = 0; // padding - - // screen array - buff[offset + 8] = id >> 24; // id - buff[offset + 9] = id >> 16; - buff[offset + 10] = id >> 8; - buff[offset + 11] = id; - buff[offset + 12] = 0; // x-position - buff[offset + 13] = 0; - buff[offset + 14] = 0; // y-position - buff[offset + 15] = 0; - buff[offset + 16] = width >> 8; // width - buff[offset + 17] = width; - buff[offset + 18] = height >> 8; // height - buff[offset + 19] = height; - buff[offset + 20] = flags >> 24; // flags - buff[offset + 21] = flags >> 16; - buff[offset + 22] = flags >> 8; - buff[offset + 23] = flags; - - sock._sQlen += 24; - sock.flush(); - }, - - clientFence: function (sock, flags, payload) { - var buff = sock._sQ; - var offset = sock._sQlen; - - buff[offset] = 248; // msg-type - - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding - - buff[offset + 4] = flags >> 24; // flags - buff[offset + 5] = flags >> 16; - buff[offset + 6] = flags >> 8; - buff[offset + 7] = flags; - - var n = payload.length; - - buff[offset + 8] = n; // length - - for (var i = 0; i < n; i++) { - buff[offset + 9 + i] = payload.charCodeAt(i); - } - - sock._sQlen += 9 + n; - sock.flush(); - }, - - enableContinuousUpdates: function (sock, enable, x, y, width, height) { - var buff = sock._sQ; - var offset = sock._sQlen; - - buff[offset] = 150; // msg-type - buff[offset + 1] = enable; // enable-flag - - buff[offset + 2] = x >> 8; // x - buff[offset + 3] = x; - buff[offset + 4] = y >> 8; // y - buff[offset + 5] = y; - buff[offset + 6] = width >> 8; // width - buff[offset + 7] = width; - buff[offset + 8] = height >> 8; // height - buff[offset + 9] = height; - - sock._sQlen += 10; - sock.flush(); - }, - - pixelFormat: function (sock, bpp, depth, true_color) { - var buff = sock._sQ; - var offset = sock._sQlen; - - buff[offset] = 0; // msg-type - - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding - - buff[offset + 4] = bpp * 8; // bits-per-pixel - buff[offset + 5] = depth * 8; // depth - buff[offset + 6] = 0; // little-endian - buff[offset + 7] = true_color ? 1 : 0; // true-color - - buff[offset + 8] = 0; // red-max - buff[offset + 9] = 255; // red-max - - buff[offset + 10] = 0; // green-max - buff[offset + 11] = 255; // green-max - - buff[offset + 12] = 0; // blue-max - buff[offset + 13] = 255; // blue-max - - buff[offset + 14] = 16; // red-shift - buff[offset + 15] = 8; // green-shift - buff[offset + 16] = 0; // blue-shift - - buff[offset + 17] = 0; // padding - buff[offset + 18] = 0; // padding - buff[offset + 19] = 0; // padding - - sock._sQlen += 20; - sock.flush(); - }, - - clientEncodings: function (sock, encodings, local_cursor, true_color) { - var buff = sock._sQ; - var offset = sock._sQlen; - - buff[offset] = 2; // msg-type - buff[offset + 1] = 0; // padding - - // offset + 2 and offset + 3 are encoding count - - var i, j = offset + 4, cnt = 0; - for (i = 0; i < encodings.length; i++) { - if (encodings[i][0] === "Cursor" && !local_cursor) { - Util.Debug("Skipping Cursor pseudo-encoding"); - } else if (encodings[i][0] === "TIGHT" && !true_color) { - // TODO: remove this when we have tight+non-true-color - Util.Warn("Skipping tight as it is only supported with true color"); - } else { - var enc = encodings[i][1]; - buff[j] = enc >> 24; - buff[j + 1] = enc >> 16; - buff[j + 2] = enc >> 8; - buff[j + 3] = enc; - - j += 4; - cnt++; - } - } - - buff[offset + 2] = cnt >> 8; - buff[offset + 3] = cnt; - - sock._sQlen += j - offset; - sock.flush(); - }, - - fbUpdateRequest: function (sock, incremental, x, y, w, h) { - var buff = sock._sQ; - var offset = sock._sQlen; - - if (typeof(x) === "undefined") { x = 0; } - if (typeof(y) === "undefined") { y = 0; } - - buff[offset] = 3; // msg-type - buff[offset + 1] = incremental ? 1 : 0; - - buff[offset + 2] = (x >> 8) & 0xFF; - buff[offset + 3] = x & 0xFF; - - buff[offset + 4] = (y >> 8) & 0xFF; - buff[offset + 5] = y & 0xFF; - - buff[offset + 6] = (w >> 8) & 0xFF; - buff[offset + 7] = w & 0xFF; - - buff[offset + 8] = (h >> 8) & 0xFF; - buff[offset + 9] = h & 0xFF; - - sock._sQlen += 10; - sock.flush(); - } - }; - - RFB.genDES = function (password, challenge) { - var passwd = []; - for (var i = 0; i < password.length; i++) { - passwd.push(password.charCodeAt(i)); - } - return (new DES(passwd)).encrypt(challenge); - }; - - RFB.encodingHandlers = { - RAW: function () { - if (this._FBU.lines === 0) { - this._FBU.lines = this._FBU.height; - } - - this._FBU.bytes = this._FBU.width * this._fb_Bpp; // at least a line - if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; } - var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines); - var curr_height = Math.min(this._FBU.lines, - Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp))); - this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, - curr_height, this._sock.get_rQ(), - this._sock.get_rQi()); - this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp); - this._FBU.lines -= curr_height; - - if (this._FBU.lines > 0) { - this._FBU.bytes = this._FBU.width * this._fb_Bpp; // At least another line - } else { - this._FBU.rects--; - this._FBU.bytes = 0; - } - - return true; - }, - - COPYRECT: function () { - this._FBU.bytes = 4; - if (this._sock.rQwait("COPYRECT", 4)) { return false; } - this._display.copyImage(this._sock.rQshift16(), this._sock.rQshift16(), - this._FBU.x, this._FBU.y, this._FBU.width, - this._FBU.height); - - this._FBU.rects--; - this._FBU.bytes = 0; - return true; - }, - - RRE: function () { - var color; - if (this._FBU.subrects === 0) { - this._FBU.bytes = 4 + this._fb_Bpp; - if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; } - this._FBU.subrects = this._sock.rQshift32(); - color = this._sock.rQshiftBytes(this._fb_Bpp); // Background - this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color); - } - - while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) { - color = this._sock.rQshiftBytes(this._fb_Bpp); - var x = this._sock.rQshift16(); - var y = this._sock.rQshift16(); - var width = this._sock.rQshift16(); - var height = this._sock.rQshift16(); - this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color); - this._FBU.subrects--; - } - - if (this._FBU.subrects > 0) { - var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); - this._FBU.bytes = (this._fb_Bpp + 8) * chunk; - } else { - this._FBU.rects--; - this._FBU.bytes = 0; - } - - return true; - }, - - HEXTILE: function () { - var rQ = this._sock.get_rQ(); - var rQi = this._sock.get_rQi(); - - if (this._FBU.tiles === 0) { - this._FBU.tiles_x = Math.ceil(this._FBU.width / 16); - this._FBU.tiles_y = Math.ceil(this._FBU.height / 16); - this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y; - this._FBU.tiles = this._FBU.total_tiles; - } - - while (this._FBU.tiles > 0) { - this._FBU.bytes = 1; - if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; } - var subencoding = rQ[rQi]; // Peek - if (subencoding > 30) { // Raw + if (!this._encNames[this._FBU.encoding]) { this._fail("Unexpected server message", - "Illegal hextile subencoding: " + subencoding); + "Unsupported encoding " + + this._FBU.encoding); return false; } + } - var subrects = 0; - var curr_tile = this._FBU.total_tiles - this._FBU.tiles; - var tile_x = curr_tile % this._FBU.tiles_x; - var tile_y = Math.floor(curr_tile / this._FBU.tiles_x); - var x = this._FBU.x + tile_x * 16; - var y = this._FBU.y + tile_y * 16; - var w = Math.min(16, (this._FBU.x + this._FBU.width) - x); - var h = Math.min(16, (this._FBU.y + this._FBU.height) - y); + this._timing.last_fbu = (new Date()).getTime(); - // Figure out how much we are expecting - if (subencoding & 0x01) { // Raw - this._FBU.bytes += w * h * this._fb_Bpp; - } else { - if (subencoding & 0x02) { // Background - this._FBU.bytes += this._fb_Bpp; - } - if (subencoding & 0x04) { // Foreground - this._FBU.bytes += this._fb_Bpp; - } - if (subencoding & 0x08) { // AnySubrects - this._FBU.bytes++; // Since we aren't shifting it off - if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; } - subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek - if (subencoding & 0x10) { // SubrectsColoured - this._FBU.bytes += subrects * (this._fb_Bpp + 2); - } else { - this._FBU.bytes += subrects * 2; - } - } + ret = this._encHandlers[this._FBU.encoding](); + + now = (new Date()).getTime(); + this._timing.cur_fbu += (now - this._timing.last_fbu); + + if (ret) { + this._encStats[this._FBU.encoding][0]++; + this._encStats[this._FBU.encoding][1]++; + this._timing.pixels += this._FBU.width * this._FBU.height; + } + + if (this._timing.pixels >= (this._fb_width * this._fb_height)) { + if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) || + this._timing.fbu_rt_start > 0) { + this._timing.full_fbu_total += this._timing.cur_fbu; + this._timing.full_fbu_cnt++; + Log.Info("Timing of full FBU, curr: " + + this._timing.cur_fbu + ", total: " + + this._timing.full_fbu_total + ", cnt: " + + this._timing.full_fbu_cnt + ", avg: " + + (this._timing.full_fbu_total / this._timing.full_fbu_cnt)); } - if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; } + if (this._timing.fbu_rt_start > 0) { + var fbu_rt_diff = now - this._timing.fbu_rt_start; + this._timing.fbu_rt_total += fbu_rt_diff; + this._timing.fbu_rt_cnt++; + Log.Info("full FBU round-trip, cur: " + + fbu_rt_diff + ", total: " + + this._timing.fbu_rt_total + ", cnt: " + + this._timing.fbu_rt_cnt + ", avg: " + + (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt)); + this._timing.fbu_rt_start = 0; + } + } - // We know the encoding and have a whole tile - this._FBU.subencoding = rQ[rQi]; - rQi++; - if (this._FBU.subencoding === 0) { - if (this._FBU.lastsubencoding & 0x01) { - // Weird: ignore blanks are RAW - Util.Debug(" Ignoring blank after RAW"); + if (!ret) { return ret; } // need more data + } + + this._display.flip(); + + this._onFBUComplete(this, + {'x': this._FBU.x, 'y': this._FBU.y, + 'width': this._FBU.width, 'height': this._FBU.height, + 'encoding': this._FBU.encoding, + 'encodingName': this._encNames[this._FBU.encoding]}); + + return true; // We finished this FBU + }, + + _updateContinuousUpdates: function() { + if (!this._enabledContinuousUpdates) { return; } + + RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0, + this._fb_width, this._fb_height); + } +}; + +make_properties(RFB, [ + ['target', 'wo', 'dom'], // VNC display rendering Canvas object + ['focusContainer', 'wo', 'dom'], // DOM element that captures keyboard input + ['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption + ['true_color', 'rw', 'bool'], // Request true color pixel data + ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor + ['shared', 'rw', 'bool'], // Request shared mode + ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard + ['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") { + RFB.messages.clientEncodings(this._sock, this._encodings, cursor, + this._true_color); + } +}; + +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; }; + +// Class Methods +RFB.messages = { + keyEvent: function (sock, keysym, down) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 4; // msg-type + buff[offset + 1] = down; + + buff[offset + 2] = 0; + buff[offset + 3] = 0; + + buff[offset + 4] = (keysym >> 24); + buff[offset + 5] = (keysym >> 16); + buff[offset + 6] = (keysym >> 8); + buff[offset + 7] = keysym; + + sock._sQlen += 8; + sock.flush(); + }, + + QEMUExtendedKeyEvent: function (sock, keysym, down, keycode) { + function getRFBkeycode(xt_scancode) { + var upperByte = (keycode >> 8); + var lowerByte = (keycode & 0x00ff); + if (upperByte === 0xe0 && lowerByte < 0x7f) { + lowerByte = lowerByte | 0x80; + return lowerByte; + } + return xt_scancode; + } + + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 255; // msg-type + buff[offset + 1] = 0; // sub msg-type + + buff[offset + 2] = (down >> 8); + buff[offset + 3] = down; + + buff[offset + 4] = (keysym >> 24); + buff[offset + 5] = (keysym >> 16); + buff[offset + 6] = (keysym >> 8); + buff[offset + 7] = keysym; + + var RFBkeycode = getRFBkeycode(keycode); + + buff[offset + 8] = (RFBkeycode >> 24); + buff[offset + 9] = (RFBkeycode >> 16); + buff[offset + 10] = (RFBkeycode >> 8); + buff[offset + 11] = RFBkeycode; + + sock._sQlen += 12; + sock.flush(); + }, + + pointerEvent: function (sock, x, y, mask) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 5; // msg-type + + buff[offset + 1] = mask; + + buff[offset + 2] = x >> 8; + buff[offset + 3] = x; + + buff[offset + 4] = y >> 8; + buff[offset + 5] = y; + + sock._sQlen += 6; + sock.flush(); + }, + + // TODO(directxman12): make this unicode compatible? + clientCutText: function (sock, text) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 6; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + var n = text.length; + + buff[offset + 4] = n >> 24; + buff[offset + 5] = n >> 16; + buff[offset + 6] = n >> 8; + buff[offset + 7] = n; + + for (var i = 0; i < n; i++) { + buff[offset + 8 + i] = text.charCodeAt(i); + } + + sock._sQlen += 8 + n; + sock.flush(); + }, + + setDesktopSize: function (sock, width, height, id, flags) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 251; // msg-type + buff[offset + 1] = 0; // padding + buff[offset + 2] = width >> 8; // width + buff[offset + 3] = width; + buff[offset + 4] = height >> 8; // height + buff[offset + 5] = height; + + buff[offset + 6] = 1; // number-of-screens + buff[offset + 7] = 0; // padding + + // screen array + buff[offset + 8] = id >> 24; // id + buff[offset + 9] = id >> 16; + buff[offset + 10] = id >> 8; + buff[offset + 11] = id; + buff[offset + 12] = 0; // x-position + buff[offset + 13] = 0; + buff[offset + 14] = 0; // y-position + buff[offset + 15] = 0; + buff[offset + 16] = width >> 8; // width + buff[offset + 17] = width; + buff[offset + 18] = height >> 8; // height + buff[offset + 19] = height; + buff[offset + 20] = flags >> 24; // flags + buff[offset + 21] = flags >> 16; + buff[offset + 22] = flags >> 8; + buff[offset + 23] = flags; + + sock._sQlen += 24; + sock.flush(); + }, + + clientFence: function (sock, flags, payload) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 248; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + buff[offset + 4] = flags >> 24; // flags + buff[offset + 5] = flags >> 16; + buff[offset + 6] = flags >> 8; + buff[offset + 7] = flags; + + var n = payload.length; + + buff[offset + 8] = n; // length + + for (var i = 0; i < n; i++) { + buff[offset + 9 + i] = payload.charCodeAt(i); + } + + sock._sQlen += 9 + n; + sock.flush(); + }, + + enableContinuousUpdates: function (sock, enable, x, y, width, height) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 150; // msg-type + buff[offset + 1] = enable; // enable-flag + + buff[offset + 2] = x >> 8; // x + buff[offset + 3] = x; + buff[offset + 4] = y >> 8; // y + buff[offset + 5] = y; + buff[offset + 6] = width >> 8; // width + buff[offset + 7] = width; + buff[offset + 8] = height >> 8; // height + buff[offset + 9] = height; + + sock._sQlen += 10; + sock.flush(); + }, + + pixelFormat: function (sock, bpp, depth, true_color) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 0; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + buff[offset + 4] = bpp * 8; // bits-per-pixel + buff[offset + 5] = depth * 8; // depth + buff[offset + 6] = 0; // little-endian + buff[offset + 7] = true_color ? 1 : 0; // true-color + + buff[offset + 8] = 0; // red-max + buff[offset + 9] = 255; // red-max + + buff[offset + 10] = 0; // green-max + buff[offset + 11] = 255; // green-max + + buff[offset + 12] = 0; // blue-max + buff[offset + 13] = 255; // blue-max + + buff[offset + 14] = 16; // red-shift + buff[offset + 15] = 8; // green-shift + buff[offset + 16] = 0; // blue-shift + + buff[offset + 17] = 0; // padding + buff[offset + 18] = 0; // padding + buff[offset + 19] = 0; // padding + + sock._sQlen += 20; + sock.flush(); + }, + + clientEncodings: function (sock, encodings, local_cursor, true_color) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 2; // msg-type + buff[offset + 1] = 0; // padding + + // offset + 2 and offset + 3 are encoding count + + var i, j = offset + 4, cnt = 0; + for (i = 0; i < encodings.length; i++) { + if (encodings[i][0] === "Cursor" && !local_cursor) { + Log.Debug("Skipping Cursor pseudo-encoding"); + } else if (encodings[i][0] === "TIGHT" && !true_color) { + // TODO: remove this when we have tight+non-true-color + Log.Warn("Skipping tight as it is only supported with true color"); + } else { + var enc = encodings[i][1]; + buff[j] = enc >> 24; + buff[j + 1] = enc >> 16; + buff[j + 2] = enc >> 8; + buff[j + 3] = enc; + + j += 4; + cnt++; + } + } + + buff[offset + 2] = cnt >> 8; + buff[offset + 3] = cnt; + + sock._sQlen += j - offset; + sock.flush(); + }, + + fbUpdateRequest: function (sock, incremental, x, y, w, h) { + var buff = sock._sQ; + var offset = sock._sQlen; + + if (typeof(x) === "undefined") { x = 0; } + if (typeof(y) === "undefined") { y = 0; } + + buff[offset] = 3; // msg-type + buff[offset + 1] = incremental ? 1 : 0; + + buff[offset + 2] = (x >> 8) & 0xFF; + buff[offset + 3] = x & 0xFF; + + buff[offset + 4] = (y >> 8) & 0xFF; + buff[offset + 5] = y & 0xFF; + + buff[offset + 6] = (w >> 8) & 0xFF; + buff[offset + 7] = w & 0xFF; + + buff[offset + 8] = (h >> 8) & 0xFF; + buff[offset + 9] = h & 0xFF; + + sock._sQlen += 10; + sock.flush(); + } +}; + +RFB.genDES = function (password, challenge) { + var passwd = []; + for (var i = 0; i < password.length; i++) { + passwd.push(password.charCodeAt(i)); + } + return (new DES(passwd)).encrypt(challenge); +}; + +RFB.encodingHandlers = { + RAW: function () { + if (this._FBU.lines === 0) { + this._FBU.lines = this._FBU.height; + } + + this._FBU.bytes = this._FBU.width * this._fb_Bpp; // at least a line + if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; } + var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines); + var curr_height = Math.min(this._FBU.lines, + Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp))); + this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, + curr_height, this._sock.get_rQ(), + this._sock.get_rQi()); + this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp); + this._FBU.lines -= curr_height; + + if (this._FBU.lines > 0) { + this._FBU.bytes = this._FBU.width * this._fb_Bpp; // At least another line + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } + + return true; + }, + + COPYRECT: function () { + this._FBU.bytes = 4; + if (this._sock.rQwait("COPYRECT", 4)) { return false; } + this._display.copyImage(this._sock.rQshift16(), this._sock.rQshift16(), + this._FBU.x, this._FBU.y, this._FBU.width, + this._FBU.height); + + this._FBU.rects--; + this._FBU.bytes = 0; + return true; + }, + + RRE: function () { + var color; + if (this._FBU.subrects === 0) { + this._FBU.bytes = 4 + this._fb_Bpp; + if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; } + this._FBU.subrects = this._sock.rQshift32(); + color = this._sock.rQshiftBytes(this._fb_Bpp); // Background + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color); + } + + while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) { + color = this._sock.rQshiftBytes(this._fb_Bpp); + var x = this._sock.rQshift16(); + var y = this._sock.rQshift16(); + var width = this._sock.rQshift16(); + var height = this._sock.rQshift16(); + this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color); + this._FBU.subrects--; + } + + if (this._FBU.subrects > 0) { + var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); + this._FBU.bytes = (this._fb_Bpp + 8) * chunk; + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } + + return true; + }, + + HEXTILE: function () { + var rQ = this._sock.get_rQ(); + var rQi = this._sock.get_rQi(); + + if (this._FBU.tiles === 0) { + this._FBU.tiles_x = Math.ceil(this._FBU.width / 16); + this._FBU.tiles_y = Math.ceil(this._FBU.height / 16); + this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y; + this._FBU.tiles = this._FBU.total_tiles; + } + + while (this._FBU.tiles > 0) { + this._FBU.bytes = 1; + if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; } + var subencoding = rQ[rQi]; // Peek + if (subencoding > 30) { // Raw + this._fail("Unexpected server message", + "Illegal hextile subencoding: " + subencoding); + return false; + } + + var subrects = 0; + var curr_tile = this._FBU.total_tiles - this._FBU.tiles; + var tile_x = curr_tile % this._FBU.tiles_x; + var tile_y = Math.floor(curr_tile / this._FBU.tiles_x); + var x = this._FBU.x + tile_x * 16; + var y = this._FBU.y + tile_y * 16; + var w = Math.min(16, (this._FBU.x + this._FBU.width) - x); + var h = Math.min(16, (this._FBU.y + this._FBU.height) - y); + + // Figure out how much we are expecting + if (subencoding & 0x01) { // Raw + this._FBU.bytes += w * h * this._fb_Bpp; + } else { + if (subencoding & 0x02) { // Background + this._FBU.bytes += this._fb_Bpp; + } + if (subencoding & 0x04) { // Foreground + this._FBU.bytes += this._fb_Bpp; + } + if (subencoding & 0x08) { // AnySubrects + this._FBU.bytes++; // Since we aren't shifting it off + if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; } + subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek + if (subencoding & 0x10) { // SubrectsColoured + this._FBU.bytes += subrects * (this._fb_Bpp + 2); } else { - this._display.fillRect(x, y, w, h, this._FBU.background); + this._FBU.bytes += subrects * 2; } - } else if (this._FBU.subencoding & 0x01) { // Raw - this._display.blitImage(x, y, w, h, rQ, rQi); - rQi += this._FBU.bytes - 1; + } + } + + if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; } + + // We know the encoding and have a whole tile + this._FBU.subencoding = rQ[rQi]; + rQi++; + if (this._FBU.subencoding === 0) { + if (this._FBU.lastsubencoding & 0x01) { + // Weird: ignore blanks are RAW + Log.Debug(" Ignoring blank after RAW"); } else { - if (this._FBU.subencoding & 0x02) { // Background - if (this._fb_Bpp == 1) { - this._FBU.background = rQ[rQi]; - } else { - // fb_Bpp is 4 - this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - } - rQi += this._fb_Bpp; + this._display.fillRect(x, y, w, h, this._FBU.background); + } + } else if (this._FBU.subencoding & 0x01) { // Raw + this._display.blitImage(x, y, w, h, rQ, rQi); + rQi += this._FBU.bytes - 1; + } else { + if (this._FBU.subencoding & 0x02) { // Background + if (this._fb_Bpp == 1) { + this._FBU.background = rQ[rQi]; + } else { + // fb_Bpp is 4 + this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; } - if (this._FBU.subencoding & 0x04) { // Foreground - if (this._fb_Bpp == 1) { - this._FBU.foreground = rQ[rQi]; - } else { - // this._fb_Bpp is 4 - this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - } - rQi += this._fb_Bpp; + rQi += this._fb_Bpp; + } + if (this._FBU.subencoding & 0x04) { // Foreground + if (this._fb_Bpp == 1) { + this._FBU.foreground = rQ[rQi]; + } else { + // this._fb_Bpp is 4 + this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; } + rQi += this._fb_Bpp; + } - this._display.startTile(x, y, w, h, this._FBU.background); - if (this._FBU.subencoding & 0x08) { // AnySubrects - subrects = rQ[rQi]; - rQi++; + this._display.startTile(x, y, w, h, this._FBU.background); + if (this._FBU.subencoding & 0x08) { // AnySubrects + subrects = rQ[rQi]; + rQi++; - for (var s = 0; s < subrects; s++) { - var color; - if (this._FBU.subencoding & 0x10) { // SubrectsColoured - if (this._fb_Bpp === 1) { - color = rQ[rQi]; - } else { - // _fb_Bpp is 4 - color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - } - rQi += this._fb_Bpp; + for (var s = 0; s < subrects; s++) { + var color; + if (this._FBU.subencoding & 0x10) { // SubrectsColoured + if (this._fb_Bpp === 1) { + color = rQ[rQi]; } else { - color = this._FBU.foreground; + // _fb_Bpp is 4 + color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; } - var xy = rQ[rQi]; - rQi++; - var sx = (xy >> 4); - var sy = (xy & 0x0f); - - var wh = rQ[rQi]; - rQi++; - var sw = (wh >> 4) + 1; - var sh = (wh & 0x0f) + 1; - - this._display.subTile(sx, sy, sw, sh, color); + rQi += this._fb_Bpp; + } else { + color = this._FBU.foreground; } + var xy = rQ[rQi]; + rQi++; + var sx = (xy >> 4); + var sy = (xy & 0x0f); + + var wh = rQ[rQi]; + rQi++; + var sw = (wh >> 4) + 1; + var sh = (wh & 0x0f) + 1; + + this._display.subTile(sx, sy, sw, sh, color); } - this._display.finishTile(); } - this._sock.set_rQi(rQi); - this._FBU.lastsubencoding = this._FBU.subencoding; - this._FBU.bytes = 0; - this._FBU.tiles--; + this._display.finishTile(); } + this._sock.set_rQi(rQi); + this._FBU.lastsubencoding = this._FBU.subencoding; + this._FBU.bytes = 0; + this._FBU.tiles--; + } - if (this._FBU.tiles === 0) { - this._FBU.rects--; - } + if (this._FBU.tiles === 0) { + this._FBU.rects--; + } - return true; - }, + return true; + }, - getTightCLength: function (arr) { - var header = 1, data = 0; - data += arr[0] & 0x7f; - if (arr[0] & 0x80) { + getTightCLength: function (arr) { + var header = 1, data = 0; + data += arr[0] & 0x7f; + if (arr[0] & 0x80) { + header++; + data += (arr[1] & 0x7f) << 7; + if (arr[1] & 0x80) { header++; - data += (arr[1] & 0x7f) << 7; - if (arr[1] & 0x80) { - header++; - data += arr[2] << 14; + data += arr[2] << 14; + } + } + return [header, data]; + }, + + display_tight: function (isTightPNG) { + if (this._fb_depth === 1) { + this._fail("Internal error", + "Tight protocol handler only implements " + + "true color mode"); + } + + this._FBU.bytes = 1; // compression-control byte + if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; } + + var checksum = function (data) { + var sum = 0; + for (var i = 0; i < data.length; i++) { + sum += data[i]; + if (sum > 65536) sum -= 65536; + } + return sum; + }; + + var resetStreams = 0; + var streamId = -1; + var decompress = function (data, expected) { + for (var i = 0; i < 4; i++) { + if ((resetStreams >> i) & 1) { + this._FBU.zlibs[i].reset(); + Log.Info("Reset zlib stream " + i); } } - return [header, data]; - }, - display_tight: function (isTightPNG) { - if (this._fb_depth === 1) { - this._fail("Internal error", - "Tight protocol handler only implements " + - "true color mode"); - } + //var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); + var uncompressed = this._FBU.zlibs[streamId].inflate(data, true, expected); + /*if (uncompressed.status !== 0) { + Log.Error("Invalid data in zlib stream"); + }*/ - this._FBU.bytes = 1; // compression-control byte - if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; } + //return uncompressed.data; + return uncompressed; + }.bind(this); - var checksum = function (data) { - var sum = 0; - for (var i = 0; i < data.length; i++) { - sum += data[i]; - if (sum > 65536) sum -= 65536; - } - return sum; - }; - - var resetStreams = 0; - var streamId = -1; - var decompress = function (data, expected) { - for (var i = 0; i < 4; i++) { - if ((resetStreams >> i) & 1) { - this._FBU.zlibs[i].reset(); - Util.Info("Reset zlib stream " + i); - } - } - - //var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); - var uncompressed = this._FBU.zlibs[streamId].inflate(data, true, expected); - /*if (uncompressed.status !== 0) { - Util.Error("Invalid data in zlib stream"); - }*/ - - //return uncompressed.data; - return uncompressed; - }.bind(this); - - var indexedToRGBX2Color = function (data, palette, width, height) { - // Convert indexed (palette based) image data to RGB - // TODO: reduce number of calculations inside loop - var dest = this._destBuff; - var w = Math.floor((width + 7) / 8); - var w1 = Math.floor(width / 8); - - /*for (var y = 0; y < height; y++) { - var b, x, dp, sp; - var yoffset = y * width; - var ybitoffset = y * w; - var xoffset, targetbyte; - for (x = 0; x < w1; x++) { - xoffset = yoffset + x * 8; - targetbyte = data[ybitoffset + x]; - for (b = 7; b >= 0; b--) { - dp = (xoffset + 7 - b) * 3; - sp = (targetbyte >> b & 1) * 3; - dest[dp] = palette[sp]; - dest[dp + 1] = palette[sp + 1]; - dest[dp + 2] = palette[sp + 2]; - } - } + var indexedToRGBX2Color = function (data, palette, width, height) { + // Convert indexed (palette based) image data to RGB + // TODO: reduce number of calculations inside loop + var dest = this._destBuff; + var w = Math.floor((width + 7) / 8); + var w1 = Math.floor(width / 8); + /*for (var y = 0; y < height; y++) { + var b, x, dp, sp; + var yoffset = y * width; + var ybitoffset = y * w; + var xoffset, targetbyte; + for (x = 0; x < w1; x++) { xoffset = yoffset + x * 8; targetbyte = data[ybitoffset + x]; - for (b = 7; b >= 8 - width % 8; b--) { + for (b = 7; b >= 0; b--) { dp = (xoffset + 7 - b) * 3; sp = (targetbyte >> b & 1) * 3; dest[dp] = palette[sp]; dest[dp + 1] = palette[sp + 1]; dest[dp + 2] = palette[sp + 2]; } - }*/ + } - for (var y = 0; y < height; y++) { - var b, x, dp, sp; - for (x = 0; x < w1; x++) { - for (b = 7; b >= 0; b--) { - dp = (y * width + x * 8 + 7 - b) * 4; - sp = (data[y * w + x] >> b & 1) * 3; - dest[dp] = palette[sp]; - dest[dp + 1] = palette[sp + 1]; - dest[dp + 2] = palette[sp + 2]; - dest[dp + 3] = 255; - } - } + xoffset = yoffset + x * 8; + targetbyte = data[ybitoffset + x]; + for (b = 7; b >= 8 - width % 8; b--) { + dp = (xoffset + 7 - b) * 3; + sp = (targetbyte >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + } + }*/ - for (b = 7; b >= 8 - width % 8; b--) { + for (var y = 0; y < height; y++) { + var b, x, dp, sp; + for (x = 0; x < w1; x++) { + for (b = 7; b >= 0; b--) { dp = (y * width + x * 8 + 7 - b) * 4; sp = (data[y * w + x] >> b & 1) * 3; dest[dp] = palette[sp]; @@ -2120,356 +2108,365 @@ export default function RFB(defaults) { } } - return dest; - }.bind(this); - - var indexedToRGBX = function (data, palette, width, height) { - // Convert indexed (palette based) image data to RGB - var dest = this._destBuff; - var total = width * height * 4; - for (var i = 0, j = 0; i < total; i += 4, j++) { - var sp = data[j] * 3; - dest[i] = palette[sp]; - dest[i + 1] = palette[sp + 1]; - dest[i + 2] = palette[sp + 2]; - dest[i + 3] = 255; + for (b = 7; b >= 8 - width % 8; b--) { + dp = (y * width + x * 8 + 7 - b) * 4; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; } - - return dest; - }.bind(this); - - var rQi = this._sock.get_rQi(); - var rQ = this._sock.rQwhole(); - var cmode, data; - var cl_header, cl_data; - - var handlePalette = function () { - var numColors = rQ[rQi + 2] + 1; - var paletteSize = numColors * this._fb_depth; - this._FBU.bytes += paletteSize; - if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; } - - var bpp = (numColors <= 2) ? 1 : 8; - var rowSize = Math.floor((this._FBU.width * bpp + 7) / 8); - var raw = false; - if (rowSize * this._FBU.height < 12) { - raw = true; - cl_header = 0; - cl_data = rowSize * this._FBU.height; - //clength = [0, rowSize * this._FBU.height]; - } else { - // begin inline getTightCLength (returning two-item arrays is bad for performance with GC) - var cl_offset = rQi + 3 + paletteSize; - cl_header = 1; - cl_data = 0; - cl_data += rQ[cl_offset] & 0x7f; - if (rQ[cl_offset] & 0x80) { - cl_header++; - cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; - if (rQ[cl_offset + 1] & 0x80) { - cl_header++; - cl_data += rQ[cl_offset + 2] << 14; - } - } - // end inline getTightCLength - } - - this._FBU.bytes += cl_header + cl_data; - if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } - - // Shift ctl, filter id, num colors, palette entries, and clength off - this._sock.rQskipBytes(3); - //var palette = this._sock.rQshiftBytes(paletteSize); - this._sock.rQshiftTo(this._paletteBuff, paletteSize); - this._sock.rQskipBytes(cl_header); - - if (raw) { - data = this._sock.rQshiftBytes(cl_data); - } else { - data = decompress(this._sock.rQshiftBytes(cl_data), rowSize * this._FBU.height); - } - - // Convert indexed (palette based) image data to RGB - var rgbx; - if (numColors == 2) { - rgbx = indexedToRGBX2Color(data, this._paletteBuff, this._FBU.width, this._FBU.height); - this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); - } else { - rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height); - this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); - } - - - return true; - }.bind(this); - - var handleCopy = function () { - var raw = false; - var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth; - if (uncompressedSize < 12) { - raw = true; - cl_header = 0; - cl_data = uncompressedSize; - } else { - // begin inline getTightCLength (returning two-item arrays is for peformance with GC) - var cl_offset = rQi + 1; - cl_header = 1; - cl_data = 0; - cl_data += rQ[cl_offset] & 0x7f; - if (rQ[cl_offset] & 0x80) { - cl_header++; - cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; - if (rQ[cl_offset + 1] & 0x80) { - cl_header++; - cl_data += rQ[cl_offset + 2] << 14; - } - } - // end inline getTightCLength - } - this._FBU.bytes = 1 + cl_header + cl_data; - if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } - - // Shift ctl, clength off - this._sock.rQshiftBytes(1 + cl_header); - - if (raw) { - data = this._sock.rQshiftBytes(cl_data); - } else { - data = decompress(this._sock.rQshiftBytes(cl_data), uncompressedSize); - } - - this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false); - - return true; - }.bind(this); - - var ctl = this._sock.rQpeek8(); - - // Keep tight reset bits - resetStreams = ctl & 0xF; - - // Figure out filter - ctl = ctl >> 4; - streamId = ctl & 0x3; - - if (ctl === 0x08) cmode = "fill"; - else if (ctl === 0x09) cmode = "jpeg"; - else if (ctl === 0x0A) cmode = "png"; - else if (ctl & 0x04) cmode = "filter"; - else if (ctl < 0x04) cmode = "copy"; - else return this._fail("Unexpected server message", - "Illegal tight compression received, " + - "ctl: " + ctl); - - if (isTightPNG && (cmode === "filter" || cmode === "copy")) { - return this._fail("Unexpected server message", - "filter/copy received in tightPNG mode"); } - switch (cmode) { - // fill use fb_depth because TPIXELs drop the padding byte - case "fill": // TPIXEL - this._FBU.bytes += this._fb_depth; - break; - case "jpeg": // max clength - this._FBU.bytes += 3; - break; - case "png": // max clength - this._FBU.bytes += 3; - break; - case "filter": // filter id + num colors if palette - this._FBU.bytes += 2; - break; - case "copy": - break; + return dest; + }.bind(this); + + var indexedToRGBX = function (data, palette, width, height) { + // Convert indexed (palette based) image data to RGB + var dest = this._destBuff; + var total = width * height * 4; + for (var i = 0, j = 0; i < total; i += 4, j++) { + var sp = data[j] * 3; + dest[i] = palette[sp]; + dest[i + 1] = palette[sp + 1]; + dest[i + 2] = palette[sp + 2]; + dest[i + 3] = 255; } + return dest; + }.bind(this); + + var rQi = this._sock.get_rQi(); + var rQ = this._sock.rQwhole(); + var cmode, data; + var cl_header, cl_data; + + var handlePalette = function () { + var numColors = rQ[rQi + 2] + 1; + var paletteSize = numColors * this._fb_depth; + this._FBU.bytes += paletteSize; + if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; } + + var bpp = (numColors <= 2) ? 1 : 8; + var rowSize = Math.floor((this._FBU.width * bpp + 7) / 8); + var raw = false; + if (rowSize * this._FBU.height < 12) { + raw = true; + cl_header = 0; + cl_data = rowSize * this._FBU.height; + //clength = [0, rowSize * this._FBU.height]; + } else { + // begin inline getTightCLength (returning two-item arrays is bad for performance with GC) + var cl_offset = rQi + 3 + paletteSize; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } + } + // end inline getTightCLength + } + + this._FBU.bytes += cl_header + cl_data; if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } - // Determine FBU.bytes - switch (cmode) { - case "fill": - // skip ctl byte - this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false); - this._sock.rQskipBytes(4); - break; - case "png": - case "jpeg": - // begin inline getTightCLength (returning two-item arrays is for peformance with GC) - var cl_offset = rQi + 1; - cl_header = 1; - cl_data = 0; - cl_data += rQ[cl_offset] & 0x7f; - if (rQ[cl_offset] & 0x80) { + // Shift ctl, filter id, num colors, palette entries, and clength off + this._sock.rQskipBytes(3); + //var palette = this._sock.rQshiftBytes(paletteSize); + this._sock.rQshiftTo(this._paletteBuff, paletteSize); + this._sock.rQskipBytes(cl_header); + + if (raw) { + data = this._sock.rQshiftBytes(cl_data); + } else { + data = decompress(this._sock.rQshiftBytes(cl_data), rowSize * this._FBU.height); + } + + // Convert indexed (palette based) image data to RGB + var rgbx; + if (numColors == 2) { + rgbx = indexedToRGBX2Color(data, this._paletteBuff, this._FBU.width, this._FBU.height); + this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); + } else { + rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height); + this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); + } + + + return true; + }.bind(this); + + var handleCopy = function () { + var raw = false; + var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth; + if (uncompressedSize < 12) { + raw = true; + cl_header = 0; + cl_data = uncompressedSize; + } else { + // begin inline getTightCLength (returning two-item arrays is for peformance with GC) + var cl_offset = rQi + 1; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { cl_header++; - cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; - if (rQ[cl_offset + 1] & 0x80) { - cl_header++; - cl_data += rQ[cl_offset + 2] << 14; - } + cl_data += rQ[cl_offset + 2] << 14; } - // end inline getTightCLength - this._FBU.bytes = 1 + cl_header + cl_data; // ctl + clength size + jpeg-data - if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } - - // We have everything, render it - this._sock.rQskipBytes(1 + cl_header); // shift off clt + compact length - data = this._sock.rQshiftBytes(cl_data); - this._display.imageRect(this._FBU.x, this._FBU.y, "image/" + cmode, data); - break; - case "filter": - var filterId = rQ[rQi + 1]; - if (filterId === 1) { - if (!handlePalette()) { return false; } - } else { - // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter - // Filter 2, Gradient is valid but not use if jpeg is enabled - this._fail("Unexpected server message", - "Unsupported tight subencoding received, " + - "filter: " + filterId); - } - break; - case "copy": - if (!handleCopy()) { return false; } - break; - } - - - this._FBU.bytes = 0; - this._FBU.rects--; - - return true; - }, - - TIGHT: function () { return this._encHandlers.display_tight(false); }, - TIGHT_PNG: function () { return this._encHandlers.display_tight(true); }, - - last_rect: function () { - this._FBU.rects = 0; - return true; - }, - - handle_FB_resize: function () { - this._fb_width = this._FBU.width; - this._fb_height = this._FBU.height; - 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); - this._timing.fbu_rt_start = (new Date()).getTime(); - this._updateContinuousUpdates(); - - this._FBU.bytes = 0; - this._FBU.rects -= 1; - return true; - }, - - ExtendedDesktopSize: function () { - this._FBU.bytes = 1; - if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } - - this._supportsSetDesktopSize = true; - var number_of_screens = this._sock.rQpeek8(); - - this._FBU.bytes = 4 + (number_of_screens * 16); - if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } - - this._sock.rQskipBytes(1); // number-of-screens - this._sock.rQskipBytes(3); // padding - - for (var i = 0; i < number_of_screens; i += 1) { - // Save the id and flags of the first screen - if (i === 0) { - this._screen_id = this._sock.rQshiftBytes(4); // id - this._sock.rQskipBytes(2); // x-position - this._sock.rQskipBytes(2); // y-position - this._sock.rQskipBytes(2); // width - this._sock.rQskipBytes(2); // height - this._screen_flags = this._sock.rQshiftBytes(4); // flags - } else { - this._sock.rQskipBytes(16); } + // end inline getTightCLength + } + this._FBU.bytes = 1 + cl_header + cl_data; + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Shift ctl, clength off + this._sock.rQshiftBytes(1 + cl_header); + + if (raw) { + data = this._sock.rQshiftBytes(cl_data); + } else { + data = decompress(this._sock.rQshiftBytes(cl_data), uncompressedSize); } - /* - * The x-position indicates the reason for the change: - * - * 0 - server resized on its own - * 1 - this client requested the resize - * 2 - another client requested the resize - */ + this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false); - // We need to handle errors when we requested the resize. - if (this._FBU.x === 1 && this._FBU.y !== 0) { - var msg = ""; - // The y-position indicates the status code from the server - switch (this._FBU.y) { - case 1: - msg = "Resize is administratively prohibited"; - break; - case 2: - msg = "Out of resources"; - break; - case 3: - msg = "Invalid screen layout"; - break; - default: - msg = "Unknown reason"; - break; - } - this._notification("Server did not accept the resize request: " - + msg, 'normal'); - return true; - } - - this._encHandlers.handle_FB_resize(); return true; - }, + }.bind(this); - DesktopSize: function () { - this._encHandlers.handle_FB_resize(); - return true; - }, + var ctl = this._sock.rQpeek8(); - Cursor: function () { - Util.Debug(">> set_cursor"); - var x = this._FBU.x; // hotspot-x - var y = this._FBU.y; // hotspot-y - var w = this._FBU.width; - var h = this._FBU.height; + // Keep tight reset bits + resetStreams = ctl & 0xF; - var pixelslength = w * h * this._fb_Bpp; - var masklength = Math.floor((w + 7) / 8) * h; + // Figure out filter + ctl = ctl >> 4; + streamId = ctl & 0x3; - this._FBU.bytes = pixelslength + masklength; - if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; } + if (ctl === 0x08) cmode = "fill"; + else if (ctl === 0x09) cmode = "jpeg"; + else if (ctl === 0x0A) cmode = "png"; + else if (ctl & 0x04) cmode = "filter"; + else if (ctl < 0x04) cmode = "copy"; + else return this._fail("Unexpected server message", + "Illegal tight compression received, " + + "ctl: " + ctl); - this._display.changeCursor(this._sock.rQshiftBytes(pixelslength), - this._sock.rQshiftBytes(masklength), - x, y, w, h); - - this._FBU.bytes = 0; - this._FBU.rects--; - - Util.Debug("<< set_cursor"); - return true; - }, - - QEMUExtendedKeyEvent: function () { - this._FBU.rects--; - - var keyboardEvent = document.createEvent("keyboardEvent"); - if (keyboardEvent.code !== undefined) { - this._qemuExtKeyEventSupported = true; - this._keyboard.setQEMUVNCKeyboardHandler(); - } - }, - - JPEG_quality_lo: function () { - Util.Error("Server sent jpeg_quality pseudo-encoding"); - }, - - compress_lo: function () { - Util.Error("Server sent compress level pseudo-encoding"); + if (isTightPNG && (cmode === "filter" || cmode === "copy")) { + return this._fail("Unexpected server message", + "filter/copy received in tightPNG mode"); } - }; -})(); + + switch (cmode) { + // fill use fb_depth because TPIXELs drop the padding byte + case "fill": // TPIXEL + this._FBU.bytes += this._fb_depth; + break; + case "jpeg": // max clength + this._FBU.bytes += 3; + break; + case "png": // max clength + this._FBU.bytes += 3; + break; + case "filter": // filter id + num colors if palette + this._FBU.bytes += 2; + break; + case "copy": + break; + } + + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Determine FBU.bytes + switch (cmode) { + case "fill": + // skip ctl byte + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false); + this._sock.rQskipBytes(4); + break; + case "png": + case "jpeg": + // begin inline getTightCLength (returning two-item arrays is for peformance with GC) + var cl_offset = rQi + 1; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } + } + // end inline getTightCLength + this._FBU.bytes = 1 + cl_header + cl_data; // ctl + clength size + jpeg-data + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // We have everything, render it + this._sock.rQskipBytes(1 + cl_header); // shift off clt + compact length + data = this._sock.rQshiftBytes(cl_data); + this._display.imageRect(this._FBU.x, this._FBU.y, "image/" + cmode, data); + break; + case "filter": + var filterId = rQ[rQi + 1]; + if (filterId === 1) { + if (!handlePalette()) { return false; } + } else { + // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter + // Filter 2, Gradient is valid but not use if jpeg is enabled + this._fail("Unexpected server message", + "Unsupported tight subencoding received, " + + "filter: " + filterId); + } + break; + case "copy": + if (!handleCopy()) { return false; } + break; + } + + + this._FBU.bytes = 0; + this._FBU.rects--; + + return true; + }, + + TIGHT: function () { return this._encHandlers.display_tight(false); }, + TIGHT_PNG: function () { return this._encHandlers.display_tight(true); }, + + last_rect: function () { + this._FBU.rects = 0; + return true; + }, + + handle_FB_resize: function () { + this._fb_width = this._FBU.width; + this._fb_height = this._FBU.height; + 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); + this._timing.fbu_rt_start = (new Date()).getTime(); + this._updateContinuousUpdates(); + + this._FBU.bytes = 0; + this._FBU.rects -= 1; + return true; + }, + + ExtendedDesktopSize: function () { + this._FBU.bytes = 1; + if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } + + this._supportsSetDesktopSize = true; + var number_of_screens = this._sock.rQpeek8(); + + this._FBU.bytes = 4 + (number_of_screens * 16); + if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } + + this._sock.rQskipBytes(1); // number-of-screens + this._sock.rQskipBytes(3); // padding + + for (var i = 0; i < number_of_screens; i += 1) { + // Save the id and flags of the first screen + if (i === 0) { + this._screen_id = this._sock.rQshiftBytes(4); // id + this._sock.rQskipBytes(2); // x-position + this._sock.rQskipBytes(2); // y-position + this._sock.rQskipBytes(2); // width + this._sock.rQskipBytes(2); // height + this._screen_flags = this._sock.rQshiftBytes(4); // flags + } else { + this._sock.rQskipBytes(16); + } + } + + /* + * The x-position indicates the reason for the change: + * + * 0 - server resized on its own + * 1 - this client requested the resize + * 2 - another client requested the resize + */ + + // We need to handle errors when we requested the resize. + if (this._FBU.x === 1 && this._FBU.y !== 0) { + var msg = ""; + // The y-position indicates the status code from the server + switch (this._FBU.y) { + case 1: + msg = "Resize is administratively prohibited"; + break; + case 2: + msg = "Out of resources"; + break; + case 3: + msg = "Invalid screen layout"; + break; + default: + msg = "Unknown reason"; + break; + } + this._notification("Server did not accept the resize request: " + + msg, 'normal'); + return true; + } + + this._encHandlers.handle_FB_resize(); + return true; + }, + + DesktopSize: function () { + this._encHandlers.handle_FB_resize(); + return true; + }, + + Cursor: function () { + Log.Debug(">> set_cursor"); + var x = this._FBU.x; // hotspot-x + var y = this._FBU.y; // hotspot-y + var w = this._FBU.width; + var h = this._FBU.height; + + var pixelslength = w * h * this._fb_Bpp; + var masklength = Math.floor((w + 7) / 8) * h; + + this._FBU.bytes = pixelslength + masklength; + if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; } + + this._display.changeCursor(this._sock.rQshiftBytes(pixelslength), + this._sock.rQshiftBytes(masklength), + x, y, w, h); + + this._FBU.bytes = 0; + this._FBU.rects--; + + Log.Debug("<< set_cursor"); + return true; + }, + + QEMUExtendedKeyEvent: function () { + this._FBU.rects--; + + var keyboardEvent = document.createEvent("keyboardEvent"); + if (keyboardEvent.code !== undefined) { + this._qemuExtKeyEventSupported = true; + this._keyboard.setQEMUVNCKeyboardHandler(); + } + }, + + JPEG_quality_lo: function () { + Log.Error("Server sent jpeg_quality pseudo-encoding"); + }, + + compress_lo: function () { + Log.Error("Server sent compress level pseudo-encoding"); + } +}; diff --git a/core/util.js b/core/util.js deleted file mode 100644 index 08d7d78e..00000000 --- a/core/util.js +++ /dev/null @@ -1,624 +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. - */ - -/* jshint white: false, nonstandard: true */ -/*global window, console, document, navigator, ActiveXObject, INCLUDE_URI */ - -var Util = {}; - -/* - * ------------------------------------------------------ - * Namespaced in Util - * ------------------------------------------------------ - */ - -/* - * Logging/debug routines - */ - -Util._log_level = 'warn'; -Util.init_logging = function (level) { - "use strict"; - if (typeof level === 'undefined') { - level = Util._log_level; - } else { - Util._log_level = level; - } - - Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; - if (typeof window.console !== "undefined") { - /* jshint -W086 */ - switch (level) { - case 'debug': - Util.Debug = console.debug.bind(window.console); - case 'info': - Util.Info = console.info.bind(window.console); - case 'warn': - Util.Warn = console.warn.bind(window.console); - case 'error': - Util.Error = console.error.bind(window.console); - case 'none': - break; - default: - throw new Error("invalid logging type '" + level + "'"); - } - /* jshint +W086 */ - } -}; -Util.get_logging = function () { - return Util._log_level; -}; -// Initialize logging level -Util.init_logging(); - -Util.make_property = function (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 - }; -}; - -Util.make_properties = function (constructor, arr) { - "use strict"; - for (var i = 0; i < arr.length; i++) { - Util.make_property(constructor.prototype, arr[i][0], arr[i][1], arr[i][2]); - } -}; - -Util.set_defaults = function (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) { - Util.Warn('Invalid property ' + keys[i]); - continue; - } - - if (keys[i] in conf) { - setter.call(obj, conf[keys[i]]); - } else { - setter.call(obj, defaults[keys[i]]); - } - } -}; - -/* - * Decode from UTF-8 - */ -Util.decodeUTF8 = function (utf8string) { - "use strict"; - return decodeURIComponent(escape(utf8string)); -}; - - - -/* - * Cross-browser routines - */ - -Util.getPointerEvent = function (e) { - return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e; -}; - -Util.stopEvent = function (e) { - e.stopPropagation(); - e.preventDefault(); -}; - -// Touch detection -Util.isTouchDevice = ('ontouchstart' in document.documentElement) || - // requried for Chrome debugger - (document.ontouchstart !== undefined) || - // required for MS Surface - (navigator.maxTouchPoints > 0) || - (navigator.msMaxTouchPoints > 0); -window.addEventListener('touchstart', function onFirstTouch() { - Util.isTouchDevice = true; - window.removeEventListener('touchstart', onFirstTouch, false); -}, false); - -Util._cursor_uris_supported = null; - -Util.browserSupportsCursorURIs = function () { - if (Util._cursor_uris_supported === null) { - try { - var target = document.createElement('canvas'); - target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default'; - - if (target.style.cursor) { - Util.Info("Data URI scheme cursor supported"); - Util._cursor_uris_supported = true; - } else { - Util.Warn("Data URI scheme cursor not supported"); - Util._cursor_uris_supported = false; - } - } catch (exc) { - Util.Error("Data URI scheme cursor test exception: " + exc); - Util._cursor_uris_supported = false; - } - } - - return Util._cursor_uris_supported; -}; - -// Set browser engine versions. Based on mootools. -Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)}; - -(function () { - "use strict"; - // 'presto': (function () { return (!window.opera) ? false : true; }()), - var detectPresto = function () { - return !!window.opera; - }; - - // 'trident': (function () { return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); - var detectTrident = function () { - if (!window.ActiveXObject) { - return false; - } else { - if (window.XMLHttpRequest) { - return (document.querySelectorAll) ? 6 : 5; - } else { - return 4; - } - } - }; - - // 'webkit': (function () { try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()), - var detectInitialWebkit = function () { - try { - if (navigator.taintEnabled) { - return false; - } else { - if (Util.Features.xpath) { - return (Util.Features.query) ? 525 : 420; - } else { - return 419; - } - } - } catch (e) { - return false; - } - }; - - var detectActualWebkit = function (initial_ver) { - var re = /WebKit\/([0-9\.]*) /; - var str_ver = (navigator.userAgent.match(re) || ['', initial_ver])[1]; - return parseFloat(str_ver, 10); - }; - - // 'gecko': (function () { return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19ssName) ? 19 : 18 : 18); }()) - var detectGecko = function () { - /* jshint -W041 */ - if (!document.getBoxObjectFor && window.mozInnerScreenX == null) { - return false; - } else { - return (document.getElementsByClassName) ? 19 : 18; - } - /* jshint +W041 */ - }; - - Util.Engine = { - // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) - //'presto': (function() { - // return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), - 'presto': detectPresto(), - 'trident': detectTrident(), - 'webkit': detectInitialWebkit(), - 'gecko': detectGecko() - }; - - if (Util.Engine.webkit) { - // Extract actual webkit version if available - Util.Engine.webkit = detectActualWebkit(Util.Engine.webkit); - } -})(); - -Util.Flash = (function () { - "use strict"; - var v, version; - try { - v = navigator.plugins['Shockwave Flash'].description; - } catch (err1) { - try { - v = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); - } catch (err2) { - v = '0 r0'; - } - } - version = v.match(/\d+/g); - return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0}; -}()); - - -Util.Localisation = { - // Currently configured language - language: 'en', - - // Current dictionary of translations - dictionary: undefined, - - // Configure suitable language based on user preferences - setup: function (supportedLanguages) { - var userLanguages; - - Util.Localisation.language = 'en'; // Default: US English - - /* - * Navigator.languages only available in Chrome (32+) and FireFox (32+) - * Fall back to navigator.language for other browsers - */ - if (typeof window.navigator.languages == 'object') { - userLanguages = window.navigator.languages; - } else { - userLanguages = [navigator.language || navigator.userLanguage]; - } - - for (var i = 0;i < userLanguages.length;i++) { - var userLang = userLanguages[i]; - userLang = userLang.toLowerCase(); - userLang = userLang.replace("_", "-"); - userLang = userLang.split("-"); - - // Built-in default? - if ((userLang[0] === 'en') && - ((userLang[1] === undefined) || (userLang[1] === 'us'))) { - return; - } - - // First pass: perfect match - for (var j = 0;j < supportedLanguages.length;j++) { - var supLang = supportedLanguages[j]; - supLang = supLang.toLowerCase(); - supLang = supLang.replace("_", "-"); - supLang = supLang.split("-"); - - if (userLang[0] !== supLang[0]) - continue; - if (userLang[1] !== supLang[1]) - continue; - - Util.Localisation.language = supportedLanguages[j]; - return; - } - - // Second pass: fallback - for (var j = 0;j < supportedLanguages.length;j++) { - supLang = supportedLanguages[j]; - supLang = supLang.toLowerCase(); - supLang = supLang.replace("_", "-"); - supLang = supLang.split("-"); - - if (userLang[0] !== supLang[0]) - continue; - if (supLang[1] !== undefined) - continue; - - Util.Localisation.language = supportedLanguages[j]; - return; - } - } - }, - - // Retrieve localised text - get: function (id) { - if (typeof Util.Localisation.dictionary !== 'undefined' && Util.Localisation.dictionary[id]) { - return Util.Localisation.dictionary[id]; - } else { - return id; - } - }, - - // Traverses the DOM and translates relevant fields - // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate - translateDOM: function () { - function process(elem, enabled) { - function isAnyOf(searchElement, items) { - return items.indexOf(searchElement) !== -1; - } - - function translateAttribute(elem, attr) { - var str = elem.getAttribute(attr); - str = Util.Localisation.get(str); - elem.setAttribute(attr, str); - } - - function translateTextNode(node) { - var str = node.data.trim(); - str = Util.Localisation.get(str); - node.data = str; - } - - if (elem.hasAttribute("translate")) { - if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) { - enabled = true; - } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) { - enabled = false; - } - } - - if (enabled) { - if (elem.hasAttribute("abbr") && - elem.tagName === "TH") { - translateAttribute(elem, "abbr"); - } - if (elem.hasAttribute("alt") && - isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) { - translateAttribute(elem, "alt"); - } - if (elem.hasAttribute("download") && - isAnyOf(elem.tagName, ["A", "AREA"])) { - translateAttribute(elem, "download"); - } - if (elem.hasAttribute("label") && - isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP", - "OPTION", "TRACK"])) { - translateAttribute(elem, "label"); - } - // FIXME: Should update "lang" - if (elem.hasAttribute("placeholder") && - isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) { - translateAttribute(elem, "placeholder"); - } - if (elem.hasAttribute("title")) { - translateAttribute(elem, "title"); - } - if (elem.hasAttribute("value") && - elem.tagName === "INPUT" && - isAnyOf(elem.getAttribute("type"), ["reset", "button"])) { - translateAttribute(elem, "value"); - } - } - - for (var i = 0;i < elem.childNodes.length;i++) { - let node = elem.childNodes[i]; - if (node.nodeType === node.ELEMENT_NODE) { - process(node, enabled); - } else if (node.nodeType === node.TEXT_NODE && enabled) { - translateTextNode(node); - } - } - } - - process(document.body, true); - }, -}; - -// Emulate Element.setCapture() when not supported - -Util._captureRecursion = false; -Util._captureProxy = function (e) { - // Recursion protection as we'll see our own event - if (Util._captureRecursion) return; - - // Clone the event as we cannot dispatch an already dispatched event - var newEv = new e.constructor(e.type, e); - - Util._captureRecursion = true; - Util._captureElem.dispatchEvent(newEv); - Util._captureRecursion = false; - - // Avoid double events - e.stopPropagation(); - - // Respect the wishes of the redirected event handlers - if (newEv.defaultPrevented) { - e.preventDefault(); - } - - // Implicitly release the capture on button release - if ((e.type === "mouseup") || (e.type === "touchend")) { - Util.releaseCapture(); - } -}; - -// Follow cursor style of target element -Util._captureElemChanged = function() { - var captureElem = document.getElementById("noVNC_mouse_capture_elem"); - captureElem.style.cursor = window.getComputedStyle(Util._captureElem).cursor; -}; -Util._captureObserver = new MutationObserver(Util._captureElemChanged); - -Util._captureIndex = 0; - -Util.setCapture = function (elem) { - if (elem.setCapture) { - - elem.setCapture(); - - // IE releases capture on 'click' events which might not trigger - elem.addEventListener('mouseup', Util.releaseCapture); - elem.addEventListener('touchend', Util.releaseCapture); - - } else { - // Release any existing capture in case this method is - // called multiple times without coordination - Util.releaseCapture(); - - // Safari on iOS 9 has a broken constructor for TouchEvent. - // We are fine in this case however, since Safari seems to - // have some sort of implicit setCapture magic anyway. - if (window.TouchEvent !== undefined) { - try { - new TouchEvent("touchstart"); - } catch (TypeError) { - return; - } - } - - var captureElem = document.getElementById("noVNC_mouse_capture_elem"); - - if (captureElem === null) { - captureElem = document.createElement("div"); - captureElem.id = "noVNC_mouse_capture_elem"; - captureElem.style.position = "fixed"; - captureElem.style.top = "0px"; - captureElem.style.left = "0px"; - captureElem.style.width = "100%"; - captureElem.style.height = "100%"; - captureElem.style.zIndex = 10000; - captureElem.style.display = "none"; - document.body.appendChild(captureElem); - - // This is to make sure callers don't get confused by having - // our blocking element as the target - captureElem.addEventListener('contextmenu', Util._captureProxy); - - captureElem.addEventListener('mousemove', Util._captureProxy); - captureElem.addEventListener('mouseup', Util._captureProxy); - - captureElem.addEventListener('touchmove', Util._captureProxy); - captureElem.addEventListener('touchend', Util._captureProxy); - } - - Util._captureElem = elem; - Util._captureIndex++; - - // Track cursor and get initial cursor - Util._captureObserver.observe(elem, {attributes:true}); - Util._captureElemChanged(); - - captureElem.style.display = null; - - // We listen to events on window in order to keep tracking if it - // happens to leave the viewport - window.addEventListener('mousemove', Util._captureProxy); - window.addEventListener('mouseup', Util._captureProxy); - - window.addEventListener('touchmove', Util._captureProxy); - window.addEventListener('touchend', Util._captureProxy); - } -}; - -Util.releaseCapture = function () { - if (document.releaseCapture) { - - document.releaseCapture(); - - } else { - if (!Util._captureElem) { - return; - } - - // There might be events already queued, so we need to wait for - // them to flush. E.g. contextmenu in Microsoft Edge - window.setTimeout(function(expected) { - // Only clear it if it's the expected grab (i.e. no one - // else has initiated a new grab) - if (Util._captureIndex === expected) { - Util._captureElem = null; - } - }, 0, Util._captureIndex); - - Util._captureObserver.disconnect(); - - var captureElem = document.getElementById("noVNC_mouse_capture_elem"); - captureElem.style.display = "none"; - - window.removeEventListener('mousemove', Util._captureProxy); - window.removeEventListener('mouseup', Util._captureProxy); - - window.removeEventListener('touchmove', Util._captureProxy); - window.removeEventListener('touchend', Util._captureProxy); - } -}; - -export default Util; diff --git a/core/util/browsers.js b/core/util/browsers.js new file mode 100644 index 00000000..ab5e09f3 --- /dev/null +++ b/core/util/browsers.js @@ -0,0 +1,120 @@ +/* + * 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. + */ + +import * as Log from './logging.js'; + +// Set browser engine versions. Based on mootools. +const Features = {xpath: !!(document.evaluate), query: !!(document.querySelector)}; + +// 'presto': (function () { return (!window.opera) ? false : true; }()), +var detectPresto = function () { + return !!window.opera; +}; + +// 'trident': (function () { return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); +var detectTrident = function () { + if (!window.ActiveXObject) { + return false; + } else { + if (window.XMLHttpRequest) { + return (document.querySelectorAll) ? 6 : 5; + } else { + return 4; + } + } +}; + +// 'webkit': (function () { try { return (navigator.taintEnabled) ? false : ((Features.xpath) ? ((Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()), +var detectInitialWebkit = function () { + try { + if (navigator.taintEnabled) { + return false; + } else { + if (Features.xpath) { + return (Features.query) ? 525 : 420; + } else { + return 419; + } + } + } catch (e) { + return false; + } +}; + +var detectActualWebkit = function (initial_ver) { + var re = /WebKit\/([0-9\.]*) /; + var str_ver = (navigator.userAgent.match(re) || ['', initial_ver])[1]; + return parseFloat(str_ver, 10); +}; + +// 'gecko': (function () { return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19ssName) ? 19 : 18 : 18); }()) +var detectGecko = function () { + /* jshint -W041 */ + if (!document.getBoxObjectFor && window.mozInnerScreenX == null) { + return false; + } else { + return (document.getElementsByClassName) ? 19 : 18; + } + /* jshint +W041 */ +}; + +const isWebkitInitial = detectInitialWebkit(); + +export const Engine = { + // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) + //'presto': (function() { + // return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), + 'presto': detectPresto(), + 'trident': detectTrident(), + 'webkit': isWebkitInitial ? detectActualWebkit(isWebkitInitial) : false, + 'gecko': detectGecko() +}; + +// Touch detection +export var isTouchDevice = ('ontouchstart' in document.documentElement) || + // requried for Chrome debugger + (document.ontouchstart !== undefined) || + // required for MS Surface + (navigator.maxTouchPoints > 0) || + (navigator.msMaxTouchPoints > 0); +window.addEventListener('touchstart', function onFirstTouch() { + isTouchDevice = true; + window.removeEventListener('touchstart', onFirstTouch, false); +}, false); + +var _cursor_uris_supported = null; + +export function browserSupportsCursorURIs () { + if (_cursor_uris_supported === null) { + try { + var target = document.createElement('canvas'); + target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default'; + + if (target.style.cursor) { + Log.Info("Data URI scheme cursor supported"); + _cursor_uris_supported = true; + } else { + Log.Warn("Data URI scheme cursor not supported"); + _cursor_uris_supported = false; + } + } catch (exc) { + Log.Error("Data URI scheme cursor test exception: " + exc); + _cursor_uris_supported = false; + } + } + + return _cursor_uris_supported; +}; + +export function _forceCursorURIs(enabled) { + if (enabled === undefined || enabled) { + _cursor_uris_supported = true; + } else { + _cursor_uris_supported = false; + } +} diff --git a/core/util/events.js b/core/util/events.js new file mode 100644 index 00000000..8fdf6aa6 --- /dev/null +++ b/core/util/events.js @@ -0,0 +1,161 @@ +/* + * 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. + */ + +/* + * Cross-browser event and position routines + */ + +import * as Log from './logging.js'; + +export function getPointerEvent (e) { + return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e; +}; + +export function stopEvent (e) { + e.stopPropagation(); + e.preventDefault(); +}; + +// Emulate Element.setCapture() when not supported +var _captureRecursion = false; +var _captureElem = null; +const _captureProxy = function (e) { + // Recursion protection as we'll see our own event + if (_captureRecursion) return; + + // Clone the event as we cannot dispatch an already dispatched event + var newEv = new e.constructor(e.type, e); + + _captureRecursion = true; + _captureElem.dispatchEvent(newEv); + _captureRecursion = false; + + // Avoid double events + e.stopPropagation(); + + // Respect the wishes of the redirected event handlers + if (newEv.defaultPrevented) { + e.preventDefault(); + } + + // Implicitly release the capture on button release + if ((e.type === "mouseup") || (e.type === "touchend")) { + releaseCapture(); + } +}; + +// Follow cursor style of target element +const _captureElemChanged = function() { + var captureElem = document.getElementById("noVNC_mouse_capture_elem"); + captureElem.style.cursor = window.getComputedStyle(_captureElem).cursor; +}; +const _captureObserver = new MutationObserver(_captureElemChanged); + +var _captureIndex = 0; + +export function setCapture (elem) { + if (elem.setCapture) { + + elem.setCapture(); + + // IE releases capture on 'click' events which might not trigger + elem.addEventListener('mouseup', releaseCapture); + elem.addEventListener('touchend', releaseCapture); + + } else { + // Release any existing capture in case this method is + // called multiple times without coordination + releaseCapture(); + + // Safari on iOS 9 has a broken constructor for TouchEvent. + // We are fine in this case however, since Safari seems to + // have some sort of implicit setCapture magic anyway. + if (window.TouchEvent !== undefined) { + try { + new TouchEvent("touchstart"); + } catch (TypeError) { + return; + } + } + + var captureElem = document.getElementById("noVNC_mouse_capture_elem"); + + if (captureElem === null) { + captureElem = document.createElement("div"); + captureElem.id = "noVNC_mouse_capture_elem"; + captureElem.style.position = "fixed"; + captureElem.style.top = "0px"; + captureElem.style.left = "0px"; + captureElem.style.width = "100%"; + captureElem.style.height = "100%"; + captureElem.style.zIndex = 10000; + captureElem.style.display = "none"; + document.body.appendChild(captureElem); + + // This is to make sure callers don't get confused by having + // our blocking element as the target + captureElem.addEventListener('contextmenu', _captureProxy); + + captureElem.addEventListener('mousemove', _captureProxy); + captureElem.addEventListener('mouseup', _captureProxy); + + captureElem.addEventListener('touchmove', _captureProxy); + captureElem.addEventListener('touchend', _captureProxy); + } + + _captureElem = elem; + _captureIndex++; + + // Track cursor and get initial cursor + _captureObserver.observe(elem, {attributes:true}); + _captureElemChanged(); + + captureElem.style.display = null; + + // We listen to events on window in order to keep tracking if it + // happens to leave the viewport + window.addEventListener('mousemove', _captureProxy); + window.addEventListener('mouseup', _captureProxy); + + window.addEventListener('touchmove', _captureProxy); + window.addEventListener('touchend', _captureProxy); + } +}; + +export function releaseCapture () { + if (document.releaseCapture) { + + document.releaseCapture(); + + } else { + if (!_captureElem) { + return; + } + + // There might be events already queued, so we need to wait for + // them to flush. E.g. contextmenu in Microsoft Edge + window.setTimeout(function(expected) { + // Only clear it if it's the expected grab (i.e. no one + // else has initiated a new grab) + if (_captureIndex === expected) { + _captureElem = null; + } + }, 0, _captureIndex); + + _captureObserver.disconnect(); + + var captureElem = document.getElementById("noVNC_mouse_capture_elem"); + captureElem.style.display = "none"; + + window.removeEventListener('mousemove', _captureProxy); + window.removeEventListener('mouseup', _captureProxy); + + window.removeEventListener('touchmove', _captureProxy); + window.removeEventListener('touchend', _captureProxy); + } +}; diff --git a/core/util/localization.js b/core/util/localization.js new file mode 100644 index 00000000..2a27a69c --- /dev/null +++ b/core/util/localization.js @@ -0,0 +1,170 @@ +/* + * 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. + */ + +/* + * Localization Utilities + */ + +export function Localizer() { + // Currently configured language + this.language = 'en'; + + // Current dictionary of translations + this.dictionary = undefined; +} + +Localizer.prototype = { + // Configure suitable language based on user preferences + setup: function (supportedLanguages) { + var userLanguages; + + this.language = 'en'; // Default: US English + + /* + * Navigator.languages only available in Chrome (32+) and FireFox (32+) + * Fall back to navigator.language for other browsers + */ + if (typeof window.navigator.languages == 'object') { + userLanguages = window.navigator.languages; + } else { + userLanguages = [navigator.language || navigator.userLanguage]; + } + + for (var i = 0;i < userLanguages.length;i++) { + var userLang = userLanguages[i]; + userLang = userLang.toLowerCase(); + userLang = userLang.replace("_", "-"); + userLang = userLang.split("-"); + + // Built-in default? + if ((userLang[0] === 'en') && + ((userLang[1] === undefined) || (userLang[1] === 'us'))) { + return; + } + + // First pass: perfect match + for (var j = 0;j < supportedLanguages.length;j++) { + var supLang = supportedLanguages[j]; + supLang = supLang.toLowerCase(); + supLang = supLang.replace("_", "-"); + supLang = supLang.split("-"); + + if (userLang[0] !== supLang[0]) + continue; + if (userLang[1] !== supLang[1]) + continue; + + this.language = supportedLanguages[j]; + return; + } + + // Second pass: fallback + for (var j = 0;j < supportedLanguages.length;j++) { + supLang = supportedLanguages[j]; + supLang = supLang.toLowerCase(); + supLang = supLang.replace("_", "-"); + supLang = supLang.split("-"); + + if (userLang[0] !== supLang[0]) + continue; + if (supLang[1] !== undefined) + continue; + + this.language = supportedLanguages[j]; + return; + } + } + }, + + // Retrieve localised text + get: function (id) { + if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) { + return this.dictionary[id]; + } else { + return id; + } + }, + + // Traverses the DOM and translates relevant fields + // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate + translateDOM: function () { + var self = this; + function process(elem, enabled) { + function isAnyOf(searchElement, items) { + return items.indexOf(searchElement) !== -1; + } + + function translateAttribute(elem, attr) { + var str = elem.getAttribute(attr); + str = self.get(str); + elem.setAttribute(attr, str); + } + + function translateTextNode(node) { + var str = node.data.trim(); + str = self.get(str); + node.data = str; + } + + if (elem.hasAttribute("translate")) { + if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) { + enabled = true; + } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) { + enabled = false; + } + } + + if (enabled) { + if (elem.hasAttribute("abbr") && + elem.tagName === "TH") { + translateAttribute(elem, "abbr"); + } + if (elem.hasAttribute("alt") && + isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) { + translateAttribute(elem, "alt"); + } + if (elem.hasAttribute("download") && + isAnyOf(elem.tagName, ["A", "AREA"])) { + translateAttribute(elem, "download"); + } + if (elem.hasAttribute("label") && + isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP", + "OPTION", "TRACK"])) { + translateAttribute(elem, "label"); + } + // FIXME: Should update "lang" + if (elem.hasAttribute("placeholder") && + isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) { + translateAttribute(elem, "placeholder"); + } + if (elem.hasAttribute("title")) { + translateAttribute(elem, "title"); + } + if (elem.hasAttribute("value") && + elem.tagName === "INPUT" && + isAnyOf(elem.getAttribute("type"), ["reset", "button"])) { + translateAttribute(elem, "value"); + } + } + + for (var i = 0;i < elem.childNodes.length;i++) { + let node = elem.childNodes[i]; + if (node.nodeType === node.ELEMENT_NODE) { + process(node, enabled); + } else if (node.nodeType === node.TEXT_NODE && enabled) { + translateTextNode(node); + } + } + } + + process(document.body, true); + }, +} + +export const l10n = new Localizer(); +export default l10n.get.bind(l10n); diff --git a/core/util/logging.js b/core/util/logging.js new file mode 100644 index 00000000..342bfa7c --- /dev/null +++ b/core/util/logging.js @@ -0,0 +1,53 @@ +/* + * 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. + */ + +/* + * Logging/debug routines + */ + +var _log_level = 'warn'; + +var Debug = function (msg) {}; +var Info = function (msg) {}; +var Warn = function (msg) {}; +var Error = function (msg) {}; + +export function init_logging (level) { + if (typeof level === 'undefined') { + level = _log_level; + } else { + _log_level = level; + } + + Debug = Info = Warn = Error = function (msg) {}; + if (typeof window.console !== "undefined") { + /* jshint -W086 */ + switch (level) { + case 'debug': + Debug = console.debug.bind(window.console); + case 'info': + Info = console.info.bind(window.console); + case 'warn': + Warn = console.warn.bind(window.console); + case 'error': + Error = console.error.bind(window.console); + case 'none': + break; + default: + throw new Error("invalid logging type '" + level + "'"); + } + /* jshint +W086 */ + } +}; +export function get_logging () { + return _log_level; +}; +export { Debug, Info, Warn, Error }; + +// Initialize logging level +init_logging(); diff --git a/core/util/properties.js b/core/util/properties.js new file mode 100644 index 00000000..e1d607e0 --- /dev/null +++ b/core/util/properties.js @@ -0,0 +1,138 @@ +/* + * 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]]); + } + } +}; + diff --git a/core/util/strings.js b/core/util/strings.js new file mode 100644 index 00000000..00a6156c --- /dev/null +++ b/core/util/strings.js @@ -0,0 +1,15 @@ +/* + * 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. + */ + +/* + * Decode from UTF-8 + */ +export function decodeUTF8 (utf8string) { + "use strict"; + return decodeURIComponent(escape(utf8string)); +}; diff --git a/core/websock.js b/core/websock.js index 7cb04667..78809a1a 100644 --- a/core/websock.js +++ b/core/websock.js @@ -12,8 +12,7 @@ * read binary data off of the receive queue. */ -import Util from "./util.js"; - +import * as Log from './util/logging.js'; /*jslint browser: true, bitwise: true */ /*global Util*/ @@ -43,313 +42,310 @@ export default function Websock() { }; }; -(function () { - "use strict"; - // this has performance issues in some versions Chromium, and - // doesn't gain a tremendous amount of performance increase in Firefox - // at the moment. It may be valuable to turn it on in the future. - var ENABLE_COPYWITHIN = false; +// this has performance issues in some versions Chromium, and +// doesn't gain a tremendous amount of performance increase in Firefox +// at the moment. It may be valuable to turn it on in the future. +var ENABLE_COPYWITHIN = false; - var MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB +var MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB - var typedArrayToString = (function () { - // This is only for PhantomJS, which doesn't like apply-ing - // with Typed Arrays - try { - var arr = new Uint8Array([1, 2, 3]); - String.fromCharCode.apply(null, arr); - return function (a) { return String.fromCharCode.apply(null, a); }; - } catch (ex) { - return function (a) { - return String.fromCharCode.apply( - null, Array.prototype.slice.call(a)); - }; - } - })(); - - Websock.prototype = { - // Getters and Setters - get_sQ: function () { - return this._sQ; - }, - - get_rQ: function () { - return this._rQ; - }, - - get_rQi: function () { - return this._rQi; - }, - - set_rQi: function (val) { - this._rQi = val; - }, - - // Receive Queue - rQlen: function () { - return this._rQlen - this._rQi; - }, - - rQpeek8: function () { - return this._rQ[this._rQi]; - }, - - rQshift8: function () { - return this._rQ[this._rQi++]; - }, - - rQskip8: function () { - this._rQi++; - }, - - rQskipBytes: function (num) { - this._rQi += num; - }, - - // TODO(directxman12): test performance with these vs a DataView - rQshift16: function () { - return (this._rQ[this._rQi++] << 8) + - this._rQ[this._rQi++]; - }, - - rQshift32: function () { - return (this._rQ[this._rQi++] << 24) + - (this._rQ[this._rQi++] << 16) + - (this._rQ[this._rQi++] << 8) + - this._rQ[this._rQi++]; - }, - - rQshiftStr: function (len) { - if (typeof(len) === 'undefined') { len = this.rQlen(); } - var arr = new Uint8Array(this._rQ.buffer, this._rQi, len); - this._rQi += len; - return typedArrayToString(arr); - }, - - rQshiftBytes: function (len) { - if (typeof(len) === 'undefined') { len = this.rQlen(); } - this._rQi += len; - return new Uint8Array(this._rQ.buffer, this._rQi - len, len); - }, - - rQshiftTo: function (target, len) { - if (len === undefined) { len = this.rQlen(); } - // TODO: make this just use set with views when using a ArrayBuffer to store the rQ - target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); - this._rQi += len; - }, - - rQwhole: function () { - return new Uint8Array(this._rQ.buffer, 0, this._rQlen); - }, - - rQslice: function (start, end) { - if (end) { - return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); - } else { - return new Uint8Array(this._rQ.buffer, this._rQi + start, this._rQlen - this._rQi - start); - } - }, - - // Check to see if we must wait for 'num' bytes (default to FBU.bytes) - // to be available in the receive queue. Return true if we need to - // wait (and possibly print a debug message), otherwise false. - rQwait: function (msg, num, goback) { - var rQlen = this._rQlen - this._rQi; // Skip rQlen() function call - if (rQlen < num) { - if (goback) { - if (this._rQi < goback) { - throw new Error("rQwait cannot backup " + goback + " bytes"); - } - this._rQi -= goback; - } - return true; // true means need more data - } - return false; - }, - - // Send Queue - - flush: function () { - if (this._websocket.bufferedAmount !== 0) { - Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount); - } - - if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) { - this._websocket.send(this._encode_message()); - this._sQlen = 0; - } - }, - - send: function (arr) { - this._sQ.set(arr, this._sQlen); - this._sQlen += arr.length; - this.flush(); - }, - - send_string: function (str) { - this.send(str.split('').map(function (chr) { - return chr.charCodeAt(0); - })); - }, - - // Event Handlers - off: function (evt) { - this._eventHandlers[evt] = function () {}; - }, - - on: function (evt, handler) { - this._eventHandlers[evt] = handler; - }, - - _allocate_buffers: function () { - this._rQ = new Uint8Array(this._rQbufferSize); - this._sQ = new Uint8Array(this._sQbufferSize); - }, - - init: function () { - this._allocate_buffers(); - this._rQi = 0; - this._websocket = null; - }, - - open: function (uri, protocols) { - var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; - this.init(); - - this._websocket = new WebSocket(uri, protocols); - this._websocket.binaryType = 'arraybuffer'; - - this._websocket.onmessage = this._recv_message.bind(this); - this._websocket.onopen = (function () { - Util.Debug('>> WebSock.onopen'); - if (this._websocket.protocol) { - Util.Info("Server choose sub-protocol: " + this._websocket.protocol); - } - - this._eventHandlers.open(); - Util.Debug("<< WebSock.onopen"); - }).bind(this); - this._websocket.onclose = (function (e) { - Util.Debug(">> WebSock.onclose"); - this._eventHandlers.close(e); - Util.Debug("<< WebSock.onclose"); - }).bind(this); - this._websocket.onerror = (function (e) { - Util.Debug(">> WebSock.onerror: " + e); - this._eventHandlers.error(e); - Util.Debug("<< WebSock.onerror: " + e); - }).bind(this); - }, - - close: function () { - if (this._websocket) { - if ((this._websocket.readyState === WebSocket.OPEN) || - (this._websocket.readyState === WebSocket.CONNECTING)) { - Util.Info("Closing WebSocket connection"); - this._websocket.close(); - } - - this._websocket.onmessage = function (e) { return; }; - } - }, - - // private methods - _encode_message: function () { - // Put in a binary arraybuffer - // according to the spec, you can send ArrayBufferViews with the send method - return new Uint8Array(this._sQ.buffer, 0, this._sQlen); - }, - - _expand_compact_rQ: function (min_fit) { - var resizeNeeded = min_fit || this._rQlen - this._rQi > this._rQbufferSize / 2; - if (resizeNeeded) { - if (!min_fit) { - // just double the size if we need to do compaction - this._rQbufferSize *= 2; - } else { - // otherwise, make sure we satisy rQlen - rQi + min_fit < rQbufferSize / 8 - this._rQbufferSize = (this._rQlen - this._rQi + min_fit) * 8; - } - } - - // we don't want to grow unboundedly - if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { - this._rQbufferSize = MAX_RQ_GROW_SIZE; - if (this._rQbufferSize - this._rQlen - this._rQi < min_fit) { - throw new Exception("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); - } - } - - if (resizeNeeded) { - var old_rQbuffer = this._rQ.buffer; - this._rQmax = this._rQbufferSize / 8; - this._rQ = new Uint8Array(this._rQbufferSize); - this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); - } else { - if (ENABLE_COPYWITHIN) { - this._rQ.copyWithin(0, this._rQi); - } else { - this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); - } - } - - this._rQlen = this._rQlen - this._rQi; - this._rQi = 0; - }, - - _decode_message: function (data) { - // push arraybuffer values onto the end - var u8 = new Uint8Array(data); - if (u8.length > this._rQbufferSize - this._rQlen) { - this._expand_compact_rQ(u8.length); - } - this._rQ.set(u8, this._rQlen); - this._rQlen += u8.length; - }, - - _recv_message: function (e) { - try { - this._decode_message(e.data); - if (this.rQlen() > 0) { - this._eventHandlers.message(); - // Compact the receive queue - if (this._rQlen == this._rQi) { - this._rQlen = 0; - this._rQi = 0; - } else if (this._rQlen > this._rQmax) { - this._expand_compact_rQ(); - } - } else { - Util.Debug("Ignoring empty message"); - } - } catch (exc) { - var exception_str = ""; - if (exc.name) { - exception_str += "\n name: " + exc.name + "\n"; - exception_str += " message: " + exc.message + "\n"; - } - - if (typeof exc.description !== 'undefined') { - exception_str += " description: " + exc.description + "\n"; - } - - if (typeof exc.stack !== 'undefined') { - exception_str += exc.stack; - } - - if (exception_str.length > 0) { - Util.Error("recv_message, caught exception: " + exception_str); - } else { - Util.Error("recv_message, caught exception: " + exc); - } - - if (typeof exc.name !== 'undefined') { - this._eventHandlers.error(exc.name + ": " + exc.message); - } else { - this._eventHandlers.error(exc); - } - } - } - }; +var typedArrayToString = (function () { + // This is only for PhantomJS, which doesn't like apply-ing + // with Typed Arrays + try { + var arr = new Uint8Array([1, 2, 3]); + String.fromCharCode.apply(null, arr); + return function (a) { return String.fromCharCode.apply(null, a); }; + } catch (ex) { + return function (a) { + return String.fromCharCode.apply( + null, Array.prototype.slice.call(a)); + }; + } })(); + +Websock.prototype = { + // Getters and Setters + get_sQ: function () { + return this._sQ; + }, + + get_rQ: function () { + return this._rQ; + }, + + get_rQi: function () { + return this._rQi; + }, + + set_rQi: function (val) { + this._rQi = val; + }, + + // Receive Queue + rQlen: function () { + return this._rQlen - this._rQi; + }, + + rQpeek8: function () { + return this._rQ[this._rQi]; + }, + + rQshift8: function () { + return this._rQ[this._rQi++]; + }, + + rQskip8: function () { + this._rQi++; + }, + + rQskipBytes: function (num) { + this._rQi += num; + }, + + // TODO(directxman12): test performance with these vs a DataView + rQshift16: function () { + return (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + }, + + rQshift32: function () { + return (this._rQ[this._rQi++] << 24) + + (this._rQ[this._rQi++] << 16) + + (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + }, + + rQshiftStr: function (len) { + if (typeof(len) === 'undefined') { len = this.rQlen(); } + var arr = new Uint8Array(this._rQ.buffer, this._rQi, len); + this._rQi += len; + return typedArrayToString(arr); + }, + + rQshiftBytes: function (len) { + if (typeof(len) === 'undefined') { len = this.rQlen(); } + this._rQi += len; + return new Uint8Array(this._rQ.buffer, this._rQi - len, len); + }, + + rQshiftTo: function (target, len) { + if (len === undefined) { len = this.rQlen(); } + // TODO: make this just use set with views when using a ArrayBuffer to store the rQ + target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); + this._rQi += len; + }, + + rQwhole: function () { + return new Uint8Array(this._rQ.buffer, 0, this._rQlen); + }, + + rQslice: function (start, end) { + if (end) { + return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); + } else { + return new Uint8Array(this._rQ.buffer, this._rQi + start, this._rQlen - this._rQi - start); + } + }, + + // Check to see if we must wait for 'num' bytes (default to FBU.bytes) + // to be available in the receive queue. Return true if we need to + // wait (and possibly print a debug message), otherwise false. + rQwait: function (msg, num, goback) { + var rQlen = this._rQlen - this._rQi; // Skip rQlen() function call + if (rQlen < num) { + if (goback) { + if (this._rQi < goback) { + throw new Error("rQwait cannot backup " + goback + " bytes"); + } + this._rQi -= goback; + } + return true; // true means need more data + } + return false; + }, + + // Send Queue + + flush: function () { + if (this._websocket.bufferedAmount !== 0) { + Log.Debug("bufferedAmount: " + this._websocket.bufferedAmount); + } + + if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) { + this._websocket.send(this._encode_message()); + this._sQlen = 0; + } + }, + + send: function (arr) { + this._sQ.set(arr, this._sQlen); + this._sQlen += arr.length; + this.flush(); + }, + + send_string: function (str) { + this.send(str.split('').map(function (chr) { + return chr.charCodeAt(0); + })); + }, + + // Event Handlers + off: function (evt) { + this._eventHandlers[evt] = function () {}; + }, + + on: function (evt, handler) { + this._eventHandlers[evt] = handler; + }, + + _allocate_buffers: function () { + this._rQ = new Uint8Array(this._rQbufferSize); + this._sQ = new Uint8Array(this._sQbufferSize); + }, + + init: function () { + this._allocate_buffers(); + this._rQi = 0; + this._websocket = null; + }, + + open: function (uri, protocols) { + var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; + this.init(); + + this._websocket = new WebSocket(uri, protocols); + this._websocket.binaryType = 'arraybuffer'; + + this._websocket.onmessage = this._recv_message.bind(this); + this._websocket.onopen = (function () { + Log.Debug('>> WebSock.onopen'); + if (this._websocket.protocol) { + Log.Info("Server choose sub-protocol: " + this._websocket.protocol); + } + + this._eventHandlers.open(); + Log.Debug("<< WebSock.onopen"); + }).bind(this); + this._websocket.onclose = (function (e) { + Log.Debug(">> WebSock.onclose"); + this._eventHandlers.close(e); + Log.Debug("<< WebSock.onclose"); + }).bind(this); + this._websocket.onerror = (function (e) { + Log.Debug(">> WebSock.onerror: " + e); + this._eventHandlers.error(e); + Log.Debug("<< WebSock.onerror: " + e); + }).bind(this); + }, + + close: function () { + if (this._websocket) { + if ((this._websocket.readyState === WebSocket.OPEN) || + (this._websocket.readyState === WebSocket.CONNECTING)) { + Log.Info("Closing WebSocket connection"); + this._websocket.close(); + } + + this._websocket.onmessage = function (e) { return; }; + } + }, + + // private methods + _encode_message: function () { + // Put in a binary arraybuffer + // according to the spec, you can send ArrayBufferViews with the send method + return new Uint8Array(this._sQ.buffer, 0, this._sQlen); + }, + + _expand_compact_rQ: function (min_fit) { + var resizeNeeded = min_fit || this._rQlen - this._rQi > this._rQbufferSize / 2; + if (resizeNeeded) { + if (!min_fit) { + // just double the size if we need to do compaction + this._rQbufferSize *= 2; + } else { + // otherwise, make sure we satisy rQlen - rQi + min_fit < rQbufferSize / 8 + this._rQbufferSize = (this._rQlen - this._rQi + min_fit) * 8; + } + } + + // we don't want to grow unboundedly + if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { + this._rQbufferSize = MAX_RQ_GROW_SIZE; + if (this._rQbufferSize - this._rQlen - this._rQi < min_fit) { + throw new Exception("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); + } + } + + if (resizeNeeded) { + var old_rQbuffer = this._rQ.buffer; + this._rQmax = this._rQbufferSize / 8; + this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); + } else { + if (ENABLE_COPYWITHIN) { + this._rQ.copyWithin(0, this._rQi); + } else { + this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); + } + } + + this._rQlen = this._rQlen - this._rQi; + this._rQi = 0; + }, + + _decode_message: function (data) { + // push arraybuffer values onto the end + var u8 = new Uint8Array(data); + if (u8.length > this._rQbufferSize - this._rQlen) { + this._expand_compact_rQ(u8.length); + } + this._rQ.set(u8, this._rQlen); + this._rQlen += u8.length; + }, + + _recv_message: function (e) { + try { + this._decode_message(e.data); + if (this.rQlen() > 0) { + this._eventHandlers.message(); + // Compact the receive queue + if (this._rQlen == this._rQi) { + this._rQlen = 0; + this._rQi = 0; + } else if (this._rQlen > this._rQmax) { + this._expand_compact_rQ(); + } + } else { + Log.Debug("Ignoring empty message"); + } + } catch (exc) { + var exception_str = ""; + if (exc.name) { + exception_str += "\n name: " + exc.name + "\n"; + exception_str += " message: " + exc.message + "\n"; + } + + if (typeof exc.description !== 'undefined') { + exception_str += " description: " + exc.description + "\n"; + } + + if (typeof exc.stack !== 'undefined') { + exception_str += exc.stack; + } + + if (exception_str.length > 0) { + Log.Error("recv_message, caught exception: " + exception_str); + } else { + Log.Error("recv_message, caught exception: " + exc); + } + + if (typeof exc.name !== 'undefined') { + this._eventHandlers.error(exc.name + ": " + exc.message); + } else { + this._eventHandlers.error(exc); + } + } + } +};