]> git.proxmox.com Git - mirror_novnc.git/blobdiff - app/ui.js
Remove alternative style sheets
[mirror_novnc.git] / app / ui.js
index e66ac3a55fdb6ee443f9b45788fa0cee7e99142d..9ff24068124d41c93faa8f288e20fc72a51d2cd2 100644 (file)
--- a/app/ui.js
+++ b/app/ui.js
@@ -2,6 +2,7 @@
  * noVNC: HTML5 VNC client
  * Copyright (C) 2012 Joel Martin
  * Copyright (C) 2016 Samuel Mannehed for Cendio AB
+ * Copyright (C) 2016 Pierre Ossman for Cendio AB
  * Licensed under MPL 2.0 (see LICENSE.txt)
  *
  * See README.md for usage and integration instructions.
@@ -13,6 +14,7 @@
 /* [module]
  * import Util from "../core/util";
  * import KeyTable from "../core/input/keysym";
+ * import keysyms from "./keysymdef";
  * import RFB from "../core/rfb";
  * import Display from "../core/display";
  * import WebUtil from "./webutil";
@@ -23,6 +25,46 @@ var UI;
 (function () {
     "use strict";
 
+    // Fallback for all uncought errors
+    window.addEventListener('error', function(event) {
+        try {
+            var msg = "";
+
+            msg += "<div>";
+            msg += event.message;
+            msg += "</div>";
+
+            msg += " <div class=\"noVNC_location\">";
+            msg += event.filename;
+            msg += ":" + event.lineno + ":" + event.colno;
+            msg += "</div>";
+
+            if ((event.error !== undefined) &&
+                (event.error.stack !== undefined)) {
+                msg += "<div class=\"noVNC_stack\">";
+                msg += event.error.stack;
+                msg += "</div>";
+            }
+
+            document.getElementById('noVNC_fallback_error')
+                .classList.add("noVNC_open");
+            document.getElementById('noVNC_fallback_errormsg').innerHTML = msg;
+        } 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;
+    });
+
+    // Set up translations
+    var LINGUAS = ["de", "el", "nl", "sv"];
+    Util.Localisation.setup(LINGUAS);
+    if (Util.Localisation.language !== "en") {
+        WebUtil.load_scripts(
+            {'app': ["locale/" + Util.Localisation.language + ".js"]});
+    }
+
     /* [begin skip-as-module] */
     // Load supporting scripts
     WebUtil.load_scripts(
@@ -33,29 +75,29 @@ var UI;
     window.onscriptsload = function () { UI.load(); };
     /* [end skip-as-module] */
 
+    var _ = Util.Localisation.get;
+
     UI = {
 
-        rfb_state: 'loaded',
+        connected: false,
+        desktopName: "",
 
         resizeTimeout: null,
-        popupStatusTimeout: null,
+        statusTimeout: null,
         hideKeyboardTimeout: null,
+        idleControlbarTimeout: null,
+        closeControlbarTimeout: null,
 
-        settingsOpen: false,
-        connSettingsOpen: false,
-        clipboardOpen: false,
-        keyboardVisible: false,
-        extraKeysVisible: false,
+        controlbarGrabbed: false,
+        controlbarDrag: false,
+        controlbarMouseDownClientY: 0,
+        controlbarMouseDownOffsetY: 0,
 
-        isTouchDevice: false,
         isSafari: false,
         rememberedClipSetting: null,
         lastKeyboardinput: null,
         defaultKeyboardinputLen: 100,
 
-        ctrlOn: false,
-        altOn: false,
-
         // Setup rfb object, load settings from browser storage, then call
         // UI.init to setup the UI/menus
         load: function(callback) {
@@ -64,16 +106,69 @@ var UI;
 
         // Render default UI and initialize settings menu
         start: function(callback) {
-            UI.isTouchDevice = 'ontouchstart' in document.documentElement;
 
-            // Stylesheet selection dropdown
-            var sheet = WebUtil.selectStylesheet();
-            var sheets = WebUtil.getStylesheets();
-            var i;
-            for (i = 0; i < sheets.length; i += 1) {
-                UI.addOption(document.getElementById('noVNC_setting_stylesheet'),sheets[i].title, sheets[i].title);
+            // Setup global variables first
+            UI.isSafari = (navigator.userAgent.indexOf('Safari') !== -1 &&
+                           navigator.userAgent.indexOf('Chrome') === -1);
+
+            UI.initSettings();
+
+            // Translate the DOM
+            Util.Localisation.translateDOM();
+
+            // 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);
+                UI.forceSetting('clip', true);
+            } else {
+                UI.initSetting('clip', false);
+            }
+
+            // Restore control bar position
+            if (WebUtil.readSetting('controlbar_pos') === 'right') {
+                UI.toggleControlbarSide();
+            }
+
+            // Setup and initialize event handlers
+            UI.setupWindowEvents();
+            UI.setupFullscreen();
+            UI.addControlbarHandlers();
+            UI.addTouchSpecificHandlers();
+            UI.addExtraKeysHandlers();
+            UI.addXvpHandlers();
+            UI.addConnectionControlHandlers();
+            UI.addClipboardHandlers();
+            UI.addSettingsHandlers();
+
+            // 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);
+            }
+        },
+
+        initSettings: function() {
+            var i;
+
             // Logging selection dropdown
             var llevels = ['error', 'warn', 'info', 'debug'];
             for (i = 0; i < llevels.length; i += 1) {
@@ -84,11 +179,6 @@ var UI;
             UI.initSetting('logging', 'warn');
             WebUtil.init_logging(UI.getSetting('logging'));
 
-            UI.initSetting('stylesheet', 'default');
-            WebUtil.selectStylesheet(null);
-            // call twice to get around webkit bug
-            WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
-
             // if port == 80 (or 443) then it won't be present and should be
             // set manually
             var port = window.location.port;
@@ -107,43 +197,24 @@ var UI;
             UI.initSetting('password', '');
             UI.initSetting('encrypt', (window.location.protocol === "https:"));
             UI.initSetting('true_color', true);
-            UI.initSetting('cursor', !UI.isTouchDevice);
+            UI.initSetting('cursor', !Util.isTouchDevice);
             UI.initSetting('resize', 'off');
             UI.initSetting('shared', true);
             UI.initSetting('view_only', false);
             UI.initSetting('path', 'websockify');
             UI.initSetting('repeaterID', '');
-            UI.initSetting('token', '');
-
-            UI.updateVisualState();
-
-            document.getElementById('noVNC_setting_host').focus();
-
-            // Show mouse selector buttons on touch screen devices
-            if (UI.isTouchDevice) {
-                // Show mobile buttons
-                document.getElementById('noVNC_mobile_buttons').style.display = "inline";
-                UI.setMouseButton();
-                // Remove the address bar
-                setTimeout(function() { window.scrollTo(0, 1); }, 100);
-                UI.forceSetting('clip', true);
-            } else {
-                UI.initSetting('clip', false);
-            }
-
-            UI.setViewClip();
-            UI.setBarPosition();
+        },
 
-            window.addEventListener('resize', function () {
-                UI.applyResizeMode();
-                UI.setViewClip();
-                UI.updateViewDrag();
-                UI.setBarPosition();
-            } );
+        setupWindowEvents: function() {
+            window.addEventListener('resize', UI.applyResizeMode);
+            window.addEventListener('resize', UI.updateViewClip);
+            window.addEventListener('resize', UI.updateViewDrag);
 
-            UI.isSafari = (navigator.userAgent.indexOf('Safari') != -1 &&
-                           navigator.userAgent.indexOf('Chrome') == -1);
+            document.getElementById("noVNC_status")
+                .addEventListener('click', UI.hideStatus);
+        },
 
+        setupFullscreen: function() {
             // Only show the button if fullscreen is properly supported
             // * Safari doesn't support alphanumerical input while in fullscreen
             if (!UI.isSafari &&
@@ -151,245 +222,504 @@ var UI;
                  document.documentElement.mozRequestFullScreen ||
                  document.documentElement.webkitRequestFullscreen ||
                  document.body.msRequestFullscreen)) {
-                document.getElementById('noVNC_fullscreen_button').style.display = "inline";
-                window.addEventListener('fullscreenchange', UI.updateFullscreenButton);
-                window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton);
-                window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton);
-                window.addEventListener('msfullscreenchange', UI.updateFullscreenButton);
+                document.getElementById('noVNC_fullscreen_button')
+                    .classList.remove("noVNC_hidden");
+                UI.addFullscreenHandlers();
             }
+        },
+
+        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_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);
+        },
+
+        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.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.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);
 
             window.addEventListener('load', UI.keyboardinputReset);
+        },
 
-            // While connected we want to display a confirmation dialogue
-            // if the user tries to leave the page
-            window.addEventListener('beforeunload', function (e) {
-                if (UI.rfb && UI.rfb_state === 'normal') {
-                    var msg = "You are currently connected.";
-                    e.returnValue = msg;
-                    return msg;
-                } else {
-                    return void 0; // To prevent the dialogue when disconnected
-                }
-            });
+        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);
+        },
 
-            // Show description by default when hosted at for kanaka.github.com
-            if (location.host === "kanaka.github.io") {
-                // Open the description dialog
-                document.getElementById('noVNC_description').style.display = "block";
-            } else {
-                // Show the connect panel on first load unless autoconnecting
-                if (autoconnect === UI.connSettingsOpen) {
-                    UI.toggleConnectPanel();
-                }
-            }
+        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);
+        },
 
-            // Add mouse event click/focus/blur event handlers to the UI
-            UI.addMouseHandlers();
+        addConnectionControlHandlers: function() {
+            document.getElementById("noVNC_connect_controls_button")
+                .addEventListener('click', UI.toggleConnectPanel);
+            document.getElementById("noVNC_disconnect_button")
+                .addEventListener('click', UI.disconnect);
+            document.getElementById("noVNC_connect_button")
+                .addEventListener('click', UI.connect);
 
-            var autoconnect = WebUtil.getConfigVar('autoconnect', false);
-            if (autoconnect === 'true' || autoconnect == '1') {
-                autoconnect = true;
-                UI.connect();
-            } else {
-                autoconnect = false;
-            }
+            document.getElementById("noVNC_password_button")
+                .addEventListener('click', UI.setPassword);
+        },
 
-            if (typeof callback === "function") {
-                callback(UI.rfb);
-            }
+        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);
+        },
+
+        addSettingsHandlers: function() {
+            document.getElementById("noVNC_settings_button")
+                .addEventListener('click', UI.toggleSettingsPanel);
+            document.getElementById("noVNC_settings_apply")
+                .addEventListener('click', UI.settingsApply);
+
+            document.getElementById("noVNC_setting_resize")
+                .addEventListener('change', UI.enableDisableViewClip);
+        },
+
+        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);
         },
 
         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.updateViewDrag,
-                                  'onDesktopName': UI.updateDocumentTitle});
+                                  'onFBResize': UI.updateSessionSize,
+                                  'onDesktopName': UI.updateDesktopName});
                 return true;
             } catch (exc) {
-                UI.updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc);
+                var msg = "Unable to create RFB client -- " + exc;
+                Util.Error(msg);
+                UI.showStatus(msg, 'error');
                 return false;
             }
         },
 
-        addMouseHandlers: function() {
-            // Setup interface handlers that can't be inline
-            document.getElementById("noVNC_view_drag_button").onclick = UI.toggleViewDrag;
-            document.getElementById("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); };
-            document.getElementById("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); };
-            document.getElementById("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); };
-            document.getElementById("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); };
-            document.getElementById("noVNC_keyboard_button").onclick = UI.showKeyboard;
-
-            document.getElementById("noVNC_keyboardinput").oninput = UI.keyInput;
-            document.getElementById("noVNC_keyboardinput").onblur = UI.hideKeyboard;
-            document.getElementById("noVNC_keyboardinput").onsubmit = function () { return false; };
-
-            document.getElementById("noVNC_toggleExtraKeys_button").onclick = UI.toggleExtraKeys;
-            document.getElementById("noVNC_toggleCtrl_button").onclick = UI.toggleCtrl;
-            document.getElementById("noVNC_toggleAlt_button").onclick = UI.toggleAlt;
-            document.getElementById("noVNC_sendTab_button").onclick = UI.sendTab;
-            document.getElementById("noVNC_sendEsc_button").onclick = UI.sendEsc;
-
-            document.getElementById("noVNC_sendCtrlAltDel_button").onclick = UI.sendCtrlAltDel;
-            document.getElementById("noVNC_xvpShutdown_button").onclick = function() { UI.rfb.xvpShutdown(); },
-            document.getElementById("noVNC_xvpReboot_button").onclick = function() { UI.rfb.xvpReboot(); },
-            document.getElementById("noVNC_xvpReset_button").onclick = function() { UI.rfb.xvpReset(); },
-            document.getElementById("noVNC_status").onclick = UI.popupStatus;
-            document.getElementById("noVNC_popup_status").onclick = UI.closePopup;
-            document.getElementById("noVNC_toggleXvp_button").onclick = UI.toggleXvpPanel;
-            document.getElementById("noVNC_clipboard_button").onclick = UI.toggleClipboardPanel;
-            document.getElementById("noVNC_fullscreen_button").onclick = UI.toggleFullscreen;
-            document.getElementById("noVNC_settings_button").onclick = UI.toggleSettingsPanel;
-            document.getElementById("noVNC_connectPanel_button").onclick = UI.toggleConnectPanel;
-            document.getElementById("noVNC_disconnect_button").onclick = UI.disconnect;
-            document.getElementById("noVNC_description_button").onclick = UI.toggleConnectPanel;
-
-            document.getElementById("noVNC_clipboard_text").onfocus = UI.displayBlur;
-            document.getElementById("noVNC_clipboard_text").onblur = UI.displayFocus;
-            document.getElementById("noVNC_clipboard_text").onchange = UI.clipboardSend;
-            document.getElementById("noVNC_clipboard_clear_button").onclick = UI.clipboardClear;
-
-            document.getElementById("noVNC_settings_apply").onclick = UI.settingsApply;
-
-            document.getElementById("noVNC_connect_button").onclick = UI.connect;
-
-            document.getElementById("noVNC_setting_resize").onchange = UI.enableDisableViewClip;
-        },
-
 /* ------^-------
  *     /INIT
  * ==============
  *     VISUAL
  * ------v------*/
 
-        updateState: function(rfb, state, oldstate, msg) {
-            UI.rfb_state = state;
-            var klass;
+        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");
+
             switch (state) {
-                case 'failed':
-                case 'fatal':
-                    klass = "noVNC_status_error";
+                case 'connecting':
+                    document.getElementById("noVNC_transition_text").innerHTML = _("Connecting...");
+                    document.documentElement.classList.add("noVNC_connecting");
                     break;
-                case 'normal':
-                    klass = "noVNC_status_normal";
+                case 'connected':
+                    UI.connected = true;
+                    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 'disconnected':
-                    document.getElementById('noVNC_logo').style.display = "block";
-                    document.getElementById('noVNC_screen').style.display = "none";
-                    /* falls through */
-                case 'loaded':
-                    klass = "noVNC_status_normal";
+                case 'disconnecting':
+                    UI.connected = false;
+                    document.getElementById("noVNC_transition_text").innerHTML = _("Disconnecting...");
+                    document.documentElement.classList.add("noVNC_disconnecting");
                     break;
-                case 'password':
-                    UI.toggleConnectPanel();
-
-                    document.getElementById('noVNC_connect_button').value = "Send Password";
-                    document.getElementById('noVNC_connect_button').onclick = UI.setPassword;
-                    document.getElementById('noVNC_setting_password').focus();
-
-                    klass = "noVNC_status_warn";
+                case 'disconnected':
+                    UI.showStatus(_("Disconnected"));
                     break;
                 default:
-                    klass = "noVNC_status_warn";
+                    msg = "Invalid UI state";
+                    Util.Error(msg);
+                    UI.showStatus(msg, 'error');
                     break;
             }
 
-            if (typeof(msg) !== 'undefined') {
-                document.getElementById('noVNC_control_bar').setAttribute("class", klass);
-                document.getElementById('noVNC_status').innerHTML = msg;
-            }
-
             UI.updateVisualState();
         },
 
         // Disable/enable controls depending on connection state
         updateVisualState: function() {
-            var connected = UI.rfb && UI.rfb_state === 'normal';
-
             //Util.Debug(">> updateVisualState");
-            document.getElementById('noVNC_setting_encrypt').disabled = connected;
-            document.getElementById('noVNC_setting_true_color').disabled = connected;
+            document.getElementById('noVNC_setting_encrypt').disabled = UI.connected;
+            document.getElementById('noVNC_setting_true_color').disabled = UI.connected;
             if (Util.browserSupportsCursorURIs()) {
-                document.getElementById('noVNC_setting_cursor').disabled = connected;
+                document.getElementById('noVNC_setting_cursor').disabled = UI.connected;
             } else {
-                UI.updateSetting('cursor', !UI.isTouchDevice);
+                UI.updateSetting('cursor', !Util.isTouchDevice);
                 document.getElementById('noVNC_setting_cursor').disabled = true;
             }
 
             UI.enableDisableViewClip();
-            document.getElementById('noVNC_setting_resize').disabled = connected;
-            document.getElementById('noVNC_setting_shared').disabled = connected;
-            document.getElementById('noVNC_setting_view_only').disabled = connected;
-            document.getElementById('noVNC_setting_path').disabled = connected;
-            document.getElementById('noVNC_setting_repeaterID').disabled = connected;
-
-            if (connected) {
-                UI.setViewClip();
+            document.getElementById('noVNC_setting_resize').disabled = UI.connected;
+            document.getElementById('noVNC_setting_shared').disabled = UI.connected;
+            document.getElementById('noVNC_setting_view_only').disabled = UI.connected;
+            document.getElementById('noVNC_setting_path').disabled = UI.connected;
+            document.getElementById('noVNC_setting_repeaterID').disabled = UI.connected;
+
+            if (UI.connected) {
+                UI.updateViewClip();
                 UI.setMouseButton(1);
-                document.getElementById('noVNC_clipboard_button').style.display = "inline";
-                document.getElementById('noVNC_keyboard_button').style.display = "inline";
-                document.getElementById('noVNC_extra_keys').style.display = "";
-                document.getElementById('noVNC_sendCtrlAltDel_button').style.display = "inline";
+
+                // Hide the controlbar after 2 seconds
+                UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
             } else {
-                UI.setMouseButton();
-                document.getElementById('noVNC_clipboard_button').style.display = "none";
-                document.getElementById('noVNC_keyboard_button').style.display = "none";
-                document.getElementById('noVNC_extra_keys').style.display = "none";
-                document.getElementById('noVNC_sendCtrlAltDel_button').style.display = "none";
                 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');
             }
 
             // State change disables viewport dragging.
             // It is enabled (toggled) by direct click on the button
-            UI.updateViewDrag(false);
+            UI.setViewDrag(false);
 
-            switch (UI.rfb_state) {
-                case 'fatal':
-                case 'failed':
-                case 'disconnected':
-                    document.getElementById('noVNC_connectPanel_button').style.display = "";
-                    document.getElementById('noVNC_disconnect_button').style.display = "none";
-                    UI.connSettingsOpen = false;
-                    UI.toggleConnectPanel();
+            // State change also closes the password dialog
+            document.getElementById('noVNC_password_dlg')
+                .classList.remove('noVNC_open');
+
+            //Util.Debug("<< updateVisualState");
+        },
+
+        showStatus: function(text, status_type, time) {
+            var statusElem = document.getElementById('noVNC_status');
+
+            clearTimeout(UI.statusTimeout);
+
+            if (typeof status_type === 'undefined') {
+                status_type = 'normal';
+            }
+
+            statusElem.classList.remove("noVNC_status_normal",
+                                        "noVNC_status_warn",
+                                        "noVNC_status_error");
+
+            switch (status_type) {
+                case 'warning':
+                case 'warn':
+                    statusElem.classList.add("noVNC_status_warn");
                     break;
-                case 'loaded':
-                    document.getElementById('noVNC_connectPanel_button').style.display = "";
-                    document.getElementById('noVNC_disconnect_button').style.display = "none";
+                case 'error':
+                    statusElem.classList.add("noVNC_status_error");
                     break;
+                case 'normal':
+                case 'info':
                 default:
-                    document.getElementById('noVNC_connectPanel_button').style.display = "none";
-                    document.getElementById('noVNC_disconnect_button').style.display = "";
+                    statusElem.classList.add("noVNC_status_normal");
                     break;
             }
 
-            //Util.Debug("<< updateVisualState");
+            statusElem.innerHTML = text;
+            statusElem.classList.add("noVNC_open");
+
+            // 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);
+            }
         },
 
-        popupStatus: function(text) {
-            var psp = document.getElementById('noVNC_popup_status');
+        hideStatus: function() {
+            clearTimeout(UI.statusTimeout);
+            document.getElementById('noVNC_status').classList.remove("noVNC_open");
+        },
+
+        notification: function (rfb, msg, level, options) {
+            UI.showStatus(msg, level);
+        },
 
-            clearTimeout(UI.popupStatusTimeout);
+        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");
+        },
 
-            if (typeof text === 'string') {
-                psp.innerHTML = text;
+        keepControlbar: function() {
+            clearTimeout(UI.closeControlbarTimeout);
+        },
+
+        openControlbar: function() {
+            document.getElementById('noVNC_control_bar')
+                .classList.add("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();
+            }
+        },
+
+        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');
+            if (anchor.classList.contains("noVNC_right")) {
+                WebUtil.writeSetting('controlbar_pos', 'left');
+                anchor.classList.remove("noVNC_right");
             } else {
-                psp.innerHTML = document.getElementById('noVNC_status').innerHTML;
+                WebUtil.writeSetting('controlbar_pos', 'right');
+                anchor.classList.add("noVNC_right");
             }
-            psp.style.display = "block";
-            psp.style.left = window.innerWidth/2 -
-                parseInt(window.getComputedStyle(psp).width)/2 -30 + "px";
 
-            // Show the popup for a maximum of 1.5 seconds
-            UI.popupStatusTimeout = setTimeout(UI.closePopup, 1500);
+            // Consider this a movement of the handle
+            UI.controlbarDrag = true;
         },
 
-        closePopup: function() {
-            clearTimeout(UI.popupStatusTimeout);
-            document.getElementById('noVNC_popup_status').style.display = "none";
+        dragControlbarHandle: function (e) {
+            if (!UI.controlbarGrabbed) return;
+
+            var ptr = Util.getPointerEvent(e);
+
+            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();
+                }
+            }
+
+            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;
+
+                UI.controlbarDrag = true;
+            }
+
+            var eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
+
+            UI.moveControlbarHandle(eventY);
+
+            e.preventDefault();
+            e.stopPropagation();
+            UI.keepControlbar();
+            UI.activateControlbar();
+        },
+
+        // 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;
+
+            // These heights need to be non-zero for the below logic to work
+            if (handleHeight === 0 || controlbarBounds.height === 0) {
+                return;
+            }
+
+            var newY = viewportRelativeY;
+
+            // 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;
+
+            } 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;
+        },
+
+        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();
+
+            WebUtil.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();
         },
 
 /* ------^-------
@@ -497,73 +827,78 @@ var UI;
             UI.saveSetting('view_only');
             UI.saveSetting('path');
             UI.saveSetting('repeaterID');
-            UI.saveSetting('stylesheet');
             UI.saveSetting('logging');
 
             // Settings with immediate (non-connected related) effect
-            WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
             WebUtil.init_logging(UI.getSetting('logging'));
-            UI.setViewClip();
+            UI.updateViewClip();
             UI.updateViewDrag();
             //Util.Debug("<< settingsApply");
         },
 
-        // Open menu
-        openSettingsMenu: function() {
-            // Close the description panel
-            document.getElementById('noVNC_description').style.display = "none";
-            // Close clipboard panel if open
-            if (UI.clipboardOpen === true) {
-                UI.toggleClipboardPanel();
-            }
-            // Close connection settings if open
-            if (UI.connSettingsOpen === true) {
-                UI.toggleConnectPanel();
-            }
-            // Close XVP panel if open
-            if (UI.xvpOpen === true) {
-                UI.toggleXvpPanel();
+/* ------^-------
+ *   /SETTINGS
+ * ==============
+ *    PANELS
+ * ------v------*/
+
+        closeAllPanels: function() {
+            UI.closeSettingsPanel();
+            UI.closeXvpPanel();
+            UI.closeClipboardPanel();
+            UI.closeConnectPanel();
+            UI.closeExtraKeys();
+        },
+
+/* ------^-------
+ *   /PANELS
+ * ==============
+ * SETTINGS (panel)
+ * ------v------*/
+
+        openSettingsPanel: function() {
+            UI.closeAllPanels();
+            UI.openControlbar();
+
+            UI.updateSetting('encrypt');
+            UI.updateSetting('true_color');
+            if (Util.browserSupportsCursorURIs()) {
+                UI.updateSetting('cursor');
+            } else {
+                UI.updateSetting('cursor', !Util.isTouchDevice);
+                document.getElementById('noVNC_setting_cursor').disabled = true;
             }
-            document.getElementById('noVNC_settings').style.display = "block";
-            document.getElementById('noVNC_settings_button').className = "noVNC_status_button_selected";
-            UI.settingsOpen = true;
+            UI.updateSetting('clip');
+            UI.updateSetting('resize');
+            UI.updateSetting('shared');
+            UI.updateSetting('view_only');
+            UI.updateSetting('path');
+            UI.updateSetting('repeaterID');
+            UI.updateSetting('logging');
+
+            document.getElementById('noVNC_settings')
+                .classList.add("noVNC_open");
+            document.getElementById('noVNC_settings_button')
+                .classList.add("noVNC_selected");
         },
 
-        // Close menu (without applying settings)
-        closeSettingsMenu: function() {
-            document.getElementById('noVNC_settings').style.display = "none";
-            document.getElementById('noVNC_settings_button').className = "noVNC_status_button";
-            UI.settingsOpen = false;
+        closeSettingsPanel: function() {
+            document.getElementById('noVNC_settings')
+                .classList.remove("noVNC_open");
+            document.getElementById('noVNC_settings_button')
+                .classList.remove("noVNC_selected");
         },
 
         // Toggle the settings menu:
         //   On open, settings are refreshed from saved cookies.
         //   On close, settings are applied
         toggleSettingsPanel: function() {
-            // Close the description panel
-            document.getElementById('noVNC_description').style.display = "none";
-            if (UI.settingsOpen) {
+            if (document.getElementById('noVNC_settings')
+                .classList.contains("noVNC_open")) {
                 UI.settingsApply();
-                UI.closeSettingsMenu();
+                UI.closeSettingsPanel();
             } else {
-                UI.updateSetting('encrypt');
-                UI.updateSetting('true_color');
-                if (Util.browserSupportsCursorURIs()) {
-                    UI.updateSetting('cursor');
-                } else {
-                    UI.updateSetting('cursor', !UI.isTouchDevice);
-                    document.getElementById('noVNC_setting_cursor').disabled = true;
-                }
-                UI.updateSetting('clip');
-                UI.updateSetting('resize');
-                UI.updateSetting('shared');
-                UI.updateSetting('view_only');
-                UI.updateSetting('path');
-                UI.updateSetting('repeaterID');
-                UI.updateSetting('stylesheet');
-                UI.updateSetting('logging');
-
-                UI.openSettingsMenu();
+                UI.openSettingsPanel();
             }
         },
 
@@ -573,45 +908,42 @@ var UI;
  *      XVP
  * ------v------*/
 
-        // Show the XVP panel
+        openXvpPanel: function() {
+            UI.closeAllPanels();
+            UI.openControlbar();
+
+            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() {
-            // Close the description panel
-            document.getElementById('noVNC_description').style.display = "none";
-            // Close settings if open
-            if (UI.settingsOpen === true) {
-                UI.settingsApply();
-                UI.closeSettingsMenu();
-            }
-            // Close connection settings if open
-            if (UI.connSettingsOpen === true) {
-                UI.toggleConnectPanel();
-            }
-            // Close clipboard panel if open
-            if (UI.clipboardOpen === true) {
-                UI.toggleClipboardPanel();
-            }
-            // Toggle XVP panel
-            if (UI.xvpOpen === true) {
-                document.getElementById('noVNC_xvp').style.display = "none";
-                document.getElementById('noVNC_toggleXvp_button').className = "noVNC_status_button";
-                UI.xvpOpen = false;
+            if (document.getElementById('noVNC_xvp')
+                .classList.contains("noVNC_open")) {
+                UI.closeXvpPanel();
             } else {
-                document.getElementById('noVNC_xvp').style.display = "block";
-                document.getElementById('noVNC_toggleXvp_button').className = "noVNC_status_button_selected";
-                UI.xvpOpen = true;
+                UI.openXvpPanel();
             }
         },
 
         // Disable/enable XVP button
         updateXvpButton: function(ver) {
-            if (ver >= 1) {
-                document.getElementById('noVNC_toggleXvp_button').style.display = 'inline';
+            if (ver >= 1 && !UI.rfb.get_view_only()) {
+                document.getElementById('noVNC_xvp_button')
+                    .classList.remove("noVNC_hidden");
             } else {
-                document.getElementById('noVNC_toggleXvp_button').style.display = 'none';
+                document.getElementById('noVNC_xvp_button')
+                    .classList.add("noVNC_hidden");
                 // Close XVP panel if open
-                if (UI.xvpOpen === true) {
-                    UI.toggleXvpPanel();
-                }
+                UI.closeXvpPanel();
             }
         },
 
@@ -621,32 +953,29 @@ var UI;
  *   CLIPBOARD
  * ------v------*/
 
-        // Show the clipboard panel
+        openClipboardPanel: function() {
+            UI.closeAllPanels();
+            UI.openControlbar();
+
+            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");
+        },
+
         toggleClipboardPanel: function() {
-            // Close the description panel
-            document.getElementById('noVNC_description').style.display = "none";
-            // Close settings if open
-            if (UI.settingsOpen === true) {
-                UI.settingsApply();
-                UI.closeSettingsMenu();
-            }
-            // Close connection settings if open
-            if (UI.connSettingsOpen === true) {
-                UI.toggleConnectPanel();
-            }
-            // Close XVP panel if open
-            if (UI.xvpOpen === true) {
-                UI.toggleXvpPanel();
-            }
-            // Toggle Clipboard Panel
-            if (UI.clipboardOpen === true) {
-                document.getElementById('noVNC_clipboard').style.display = "none";
-                document.getElementById('noVNC_clipboard_button').className = "noVNC_status_button";
-                UI.clipboardOpen = false;
+            if (document.getElementById('noVNC_clipboard')
+                .classList.contains("noVNC_open")) {
+                UI.closeClipboardPanel();
             } else {
-                document.getElementById('noVNC_clipboard').style.display = "block";
-                document.getElementById('noVNC_clipboard_button').className = "noVNC_status_button_selected";
-                UI.clipboardOpen = true;
+                UI.openClipboardPanel();
             }
         },
 
@@ -674,63 +1003,55 @@ var UI;
  *  CONNECTION
  * ------v------*/
 
-        // Show the connection settings panel/menu
+        openConnectPanel: function() {
+            UI.closeAllPanels();
+            UI.openControlbar();
+
+            document.getElementById('noVNC_connect_controls')
+                .classList.add("noVNC_open");
+            document.getElementById('noVNC_connect_controls_button')
+                .classList.add("noVNC_selected");
+
+            document.getElementById('noVNC_setting_host').focus();
+        },
+
+        closeConnectPanel: function() {
+            document.getElementById('noVNC_connect_controls')
+                .classList.remove("noVNC_open");
+            document.getElementById('noVNC_connect_controls_button')
+                .classList.remove("noVNC_selected");
+
+            UI.saveSetting('host');
+            UI.saveSetting('port');
+            //UI.saveSetting('password');
+        },
+
         toggleConnectPanel: function() {
-            // Close the description panel
-            document.getElementById('noVNC_description').style.display = "none";
-            // Close connection settings if open
-            if (UI.settingsOpen === true) {
-                UI.settingsApply();
-                UI.closeSettingsMenu();
-                document.getElementById('noVNC_connectPanel_button').className = "noVNC_status_button";
-            }
-            // Close clipboard panel if open
-            if (UI.clipboardOpen === true) {
-                UI.toggleClipboardPanel();
-            }
-            // Close XVP panel if open
-            if (UI.xvpOpen === true) {
-                UI.toggleXvpPanel();
-            }
-
-            // Toggle Connection Panel
-            if (UI.connSettingsOpen === true) {
-                document.getElementById('noVNC_controls').style.display = "none";
-                document.getElementById('noVNC_connectPanel_button').className = "noVNC_status_button";
-                UI.connSettingsOpen = false;
-                UI.saveSetting('host');
-                UI.saveSetting('port');
-                UI.saveSetting('token');
-                //UI.saveSetting('password');
+            if (document.getElementById('noVNC_connect_controls')
+                .classList.contains("noVNC_open")) {
+                UI.closeConnectPanel();
             } else {
-                document.getElementById('noVNC_controls').style.display = "block";
-                document.getElementById('noVNC_connectPanel_button').className = "noVNC_status_button_selected";
-                UI.connSettingsOpen = true;
-                document.getElementById('noVNC_setting_host').focus();
+                UI.openConnectPanel();
             }
         },
 
         connect: function() {
-            UI.closeSettingsMenu();
-            UI.toggleConnectPanel();
-
             var host = document.getElementById('noVNC_setting_host').value;
             var port = document.getElementById('noVNC_setting_port').value;
             var password = document.getElementById('noVNC_setting_password').value;
-            var token = document.getElementById('noVNC_setting_token').value;
             var path = document.getElementById('noVNC_setting_path').value;
 
-            //if token is in path then ignore the new token variable
-            if (token) {
-                path = WebUtil.injectParamIfMissing(path, "token", token);
-            }
-
             if ((!host) || (!port)) {
-                throw new Error("Must set host and port");
+                var msg = _("Must set host and port");
+                Util.Error(msg);
+                UI.showStatus(msg, 'error');
+                return;
             }
 
             if (!UI.initRFB()) return;
 
+            UI.closeAllPanels();
+
             UI.rfb.set_encrypt(UI.getSetting('encrypt'));
             UI.rfb.set_true_color(UI.getSetting('true_color'));
             UI.rfb.set_local_cursor(UI.getSetting('cursor'));
@@ -739,38 +1060,56 @@ var UI;
             UI.rfb.set_repeaterID(UI.getSetting('repeaterID'));
 
             UI.rfb.connect(host, port, password, path);
-
-            //Close dialog.
-            setTimeout(UI.setBarPosition, 100);
-            document.getElementById('noVNC_logo').style.display = "none";
-            document.getElementById('noVNC_screen').style.display = "inline";
         },
 
         disconnect: function() {
-            UI.closeSettingsMenu();
+            UI.closeAllPanels();
             UI.rfb.disconnect();
 
             // Restore the callback used for initial resize
             UI.rfb.set_onFBUComplete(UI.initialResize);
 
-            document.getElementById('noVNC_logo').style.display = "block";
-            document.getElementById('noVNC_screen').style.display = "none";
-
             // Don't display the connection settings until we're actually disconnected
         },
 
+        disconnectFinished: function (rfb, reason) {
+            if (typeof reason !== 'undefined') {
+                UI.showStatus(reason, 'error');
+            }
+            UI.openConnectPanel();
+        },
+
+/* ------^-------
+ *  /CONNECTION
+ * ==============
+ *   PASSWORD
+ * ------v------*/
+
+        passwordRequired: function(rfb, msg) {
+
+            document.getElementById('noVNC_password_dlg')
+                .classList.add('noVNC_open');
+
+            setTimeout(function () {
+                    document.getElementById('noVNC_password_input').focus();
+                }, 100);
+
+            if (typeof msg === 'undefined') {
+                msg = _("Password is required");
+            }
+            Util.Warn(msg);
+            UI.showStatus(msg, "warning");
+        },
+
         setPassword: function() {
-            UI.rfb.sendPassword(document.getElementById('noVNC_setting_password').value);
-            //Reset connect button.
-            document.getElementById('noVNC_connect_button').value = "Connect";
-            document.getElementById('noVNC_connect_button').onclick = UI.connect;
-            //Hide connection panel.
-            UI.toggleConnectPanel();
+            UI.rfb.sendPassword(document.getElementById('noVNC_password_input').value);
+            document.getElementById('noVNC_password_dlg')
+                .classList.remove('noVNC_open');
             return false;
         },
 
 /* ------^-------
- *  /CONNECTION
+ *  /PASSWORD
  * ==============
  *   FULLSCREEN
  * ------v------*/
@@ -809,9 +1148,11 @@ var UI;
                 document.mozFullScreenElement || // currently working methods
                 document.webkitFullscreenElement ||
                 document.msFullscreenElement ) {
-                document.getElementById('noVNC_fullscreen_button').className = "noVNC_status_button_selected";
+                document.getElementById('noVNC_fullscreen_button')
+                    .classList.add("noVNC_selected");
             } else {
-                document.getElementById('noVNC_fullscreen_button').className = "noVNC_status_button";
+                document.getElementById('noVNC_fullscreen_button')
+                    .classList.remove("noVNC_selected");
             }
         },
 
@@ -827,7 +1168,7 @@ var UI;
 
             var screen = UI.screenSize();
 
-            if (screen && UI.rfb_state === 'normal' && UI.rfb.get_display()) {
+            if (screen && UI.connected && UI.rfb.get_display()) {
 
                 var display = UI.rfb.get_display();
                 var resizeMode = UI.getSetting('resize');
@@ -841,29 +1182,26 @@ var UI;
                     // is finished we wait 0.5 seconds before sending the request.
                     clearTimeout(UI.resizeTimeout);
                     UI.resizeTimeout = setTimeout(function(){
-
-                        // Limit the viewport to the size of the browser window
-                        display.set_maxWidth(screen.w);
-                        display.set_maxHeight(screen.h);
-
-                        Util.Debug('Attempting requestDesktopSize(' +
-                                   screen.w + ', ' + screen.h + ')');
-
                         // Request a remote size covering the viewport
-                        UI.rfb.requestDesktopSize(screen.w, screen.h);
+                        if (UI.rfb.requestDesktopSize(screen.w, screen.h)) {
+                            Util.Debug('Requested new desktop size: ' +
+                                       screen.w + 'x' + screen.h);
+                        }
                     }, 500);
 
                 } else if (resizeMode === 'scale' || resizeMode === 'downscale') {
                     var downscaleOnly = resizeMode === 'downscale';
                     var scaleRatio = display.autoscale(screen.w, screen.h, downscaleOnly);
-                    UI.rfb.get_mouse().set_scale(scaleRatio);
-                    Util.Debug('Scaling by ' + UI.rfb.get_mouse().get_scale());
+
+                    if (!UI.rfb.get_view_only()) {
+                        UI.rfb.get_mouse().set_scale(scaleRatio);
+                        Util.Debug('Scaling by ' + UI.rfb.get_mouse().get_scale());
+                    }
                 }
             }
         },
 
-        // The screen is always the same size as the available viewport
-        // in the browser window minus the height of the control bar
+        // Gets the the size of the available viewport in the browser window
         screenSize: function() {
             var screen = document.getElementById('noVNC_screen');
 
@@ -903,60 +1241,34 @@ var UI;
 
         // Set and configure viewport clipping
         setViewClip: function(clip) {
-            var display;
-            if (UI.rfb) {
-                display = UI.rfb.get_display();
-            } else {
-                UI.forceSetting('clip', clip);
-                return;
-            }
+            UI.updateSetting('clip', clip);
+            UI.updateViewClip();
+        },
 
-            var cur_clip = display.get_viewport();
+        // Update parameters that depend on the clip setting
+        updateViewClip: function() {
+            if (!UI.rfb) return;
 
-            if (typeof(clip) !== 'boolean') {
-                // Use current setting
-                clip = UI.getSetting('clip');
-            }
+            var display = UI.rfb.get_display();
+            var cur_clip = display.get_viewport();
+            var new_clip = UI.getSetting('clip');
 
-            if (clip && !cur_clip) {
-                // Turn clipping on
-                UI.updateSetting('clip', true);
-            } else if (!clip && cur_clip) {
-                // Turn clipping off
-                UI.updateSetting('clip', false);
-                display.set_viewport(false);
-                // Disable max dimensions
-                display.set_maxWidth(0);
-                display.set_maxHeight(0);
-                display.viewportChangeSize();
+            if (cur_clip !== new_clip) {
+                display.set_viewport(new_clip);
             }
-            if (UI.getSetting('clip')) {
-                // If clipping, update clipping settings
-                display.set_viewport(true);
-
-                var size = UI.screenSize();
-                if (size) {
-                    display.set_maxWidth(size.w);
-                    display.set_maxHeight(size.h);
 
-                    // Hide potential scrollbars that can skew the position
-                    document.getElementById('noVNC_screen').style.overflow = "hidden";
+            var size = UI.screenSize();
 
-                    // The x position marks the left margin of the canvas,
-                    // remove the margin from both sides to keep it centered
-                    var new_w = size.w - (2 * Util.getPosition(document.getElementById('noVNC_canvas')).x);
-
-                    document.getElementById('noVNC_screen').style.overflow = "visible";
-
-                    display.viewportChangeSize(new_w, size.h);
-                }
+            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);
             }
         },
 
         // Handle special cases where clipping is forced on/off or locked
         enableDisableViewClip: function() {
             var resizeSetting = document.getElementById('noVNC_setting_resize');
-            var connected = UI.rfb && UI.rfb_state === 'normal';
 
             if (UI.isSafari) {
                 // Safari auto-hides the scrollbars which makes them
@@ -971,17 +1283,24 @@ var UI;
                 // The browser is IE and we are in fullscreen mode.
                 // - We need to force clipping while in fullscreen since
                 //   scrollbars doesn't work.
-                UI.popupStatus("Forcing clipping mode since scrollbars aren't supported by IE in fullscreen");
+                var msg = _("Forcing clipping mode since " +
+                            "scrollbars aren't supported " +
+                            "by IE in fullscreen");
+                Util.Debug(msg);
+                UI.showStatus(msg);
                 UI.rememberedClipSetting = UI.getSetting('clip');
                 UI.setViewClip(true);
                 document.getElementById('noVNC_setting_clip').disabled = true;
-            } else if (document.body.msRequestFullscreen && UI.rememberedClip !== null) {
+            } else if (document.body.msRequestFullscreen &&
+                       UI.rememberedClipSetting !== null) {
                 // Restore view clip to what it was before fullscreen on IE
                 UI.setViewClip(UI.rememberedClipSetting);
-                document.getElementById('noVNC_setting_clip').disabled = connected || UI.isTouchDevice;
+                document.getElementById('noVNC_setting_clip').disabled =
+                    UI.connected || Util.isTouchDevice;
             } else {
-                document.getElementById('noVNC_setting_clip').disabled = connected || UI.isTouchDevice;
-                if (UI.isTouchDevice) {
+                document.getElementById('noVNC_setting_clip').disabled =
+                    UI.connected || Util.isTouchDevice;
+                if (Util.isTouchDevice) {
                     UI.setViewClip(true);
                 }
             }
@@ -993,62 +1312,68 @@ var UI;
  *    VIEWDRAG
  * ------v------*/
 
-        // Update the viewport drag state
-        updateViewDrag: function(drag) {
+        toggleViewDrag: function() {
             if (!UI.rfb) return;
 
-            var viewDragButton = document.getElementById('noVNC_view_drag_button');
+            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;
+
+            UI.rfb.set_viewportDrag(drag);
+
+            UI.updateViewDrag();
+        },
+
+        updateViewDrag: function() {
+            var clipping = false;
+
+            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_state === 'normal' &&
-                UI.rfb.get_display().get_viewport() &&
+            if (UI.rfb.get_display().get_viewport() &&
                 UI.rfb.get_display().clippingDisplay()) {
+                clipping = true;
+            }
 
-                viewDragButton.style.display = "inline";
-                viewDragButton.disabled = false;
+            var viewDragButton = document.getElementById('noVNC_view_drag_button');
 
-            } else {
+            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.
-                if (UI.rfb.get_viewportDrag) {
-                    viewDragButton.className = "noVNC_status_button";
-                    UI.rfb.set_viewportDrag(false);
-                }
-
-                // The button is disabled instead of hidden on touch devices
-                if (UI.rfb_state === 'normal' && UI.isTouchDevice) {
-                    viewDragButton.style.display = "inline";
-                    viewDragButton.disabled = true;
-                } else {
-                    viewDragButton.style.display = "none";
-                }
-                return;
+                UI.rfb.set_viewportDrag(false);
             }
 
-            if (typeof(drag) !== "undefined" &&
-                typeof(drag) !== "object") {
-                if (drag) {
-                    viewDragButton.className = "noVNC_status_button_selected";
-                    UI.rfb.set_viewportDrag(true);
-                } else {
-                    viewDragButton.className = "noVNC_status_button";
-                    UI.rfb.set_viewportDrag(false);
-                }
+            if (UI.rfb.get_viewportDrag()) {
+                viewDragButton.classList.add("noVNC_selected");
+            } else {
+                viewDragButton.classList.remove("noVNC_selected");
             }
-        },
 
-        toggleViewDrag: function() {
-            if (!UI.rfb) return;
+            // 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");
 
-            var viewDragButton = document.getElementById('noVNC_view_drag_button');
-            if (UI.rfb.get_viewportDrag()) {
-                viewDragButton.className = "noVNC_status_button";
-                UI.rfb.set_viewportDrag(false);
+                if (clipping) {
+                    viewDragButton.disabled = false;
+                } else {
+                    viewDragButton.disabled = true;
+                }
             } else {
-                viewDragButton.className = "noVNC_status_button_selected";
-                UI.rfb.set_viewportDrag(true);
+                viewDragButton.disabled = false;
+
+                if (clipping) {
+                    viewDragButton.classList.remove("noVNC_hidden");
+                } else {
+                    viewDragButton.classList.add("noVNC_hidden");
+                }
             }
         },
 
@@ -1058,45 +1383,73 @@ var UI;
  *    KEYBOARD
  * ------v------*/
 
-        // On touch devices, show the OS keyboard
-        showKeyboard: function() {
-            var kbi = document.getElementById('noVNC_keyboardinput');
-            var skb = document.getElementById('noVNC_keyboard_button');
-            var l = kbi.value.length;
-            if(UI.keyboardVisible === false) {
-                kbi.focus();
-                try { kbi.setSelectionRange(l, l); } // Move the caret to the end
-                catch (err) {} // setSelectionRange is undefined in Google Chrome
-                UI.keyboardVisible = true;
-                skb.className = "noVNC_status_button_selected";
-            } else if(UI.keyboardVisible === true) {
-                kbi.blur();
-                skb.className = "noVNC_status_button";
-                UI.keyboardVisible = false;
-            }
-        },
-
-        hideKeyboard: function() {
-            document.getElementById('noVNC_keyboard_button').className = "noVNC_status_button";
-            //Weird bug in iOS if you change keyboardVisible
-            //here it does not actually occur so next time
-            //you click keyboard icon it doesnt work.
-            UI.hideKeyboardTimeout = setTimeout(function() {
-                UI.keyboardVisible = false;
-            },100);
-        },
-
-        keepKeyboard: function() {
-            clearTimeout(UI.hideKeyboardTimeout);
-            if(UI.keyboardVisible === true) {
-                document.getElementById('noVNC_keyboardinput').focus();
-                document.getElementById('noVNC_keyboard_button').className = "noVNC_status_button_selected";
-            } else if(UI.keyboardVisible === false) {
-                document.getElementById('noVNC_keyboardinput').blur();
-                document.getElementById('noVNC_keyboard_button').className = "noVNC_status_button";
+        showVirtualKeyboard: function() {
+            if (!Util.isTouchDevice) return;
+
+            var input = document.getElementById('noVNC_keyboardinput');
+
+            if (document.activeElement == input) return;
+
+            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
+        },
+
+        hideVirtualKeyboard: function() {
+            if (!Util.isTouchDevice) return;
+
+            var input = document.getElementById('noVNC_keyboardinput');
+
+            if (document.activeElement != input) return;
+
+            input.blur();
+        },
+
+        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;
+            }
+
+            // Allow clicking on links
+            if (event.target.tagName === "A") {
+                return;
+            }
+
+            // And form elements, except standard noVNC buttons
+            if ((event.target.form !== undefined) &&
+                !event.target.classList.contains("noVNC_button")) {
+                return;
+            }
+
+            event.preventDefault();
+        },
+
         keyboardinputReset: function() {
             var kbi = document.getElementById('noVNC_keyboardinput');
             kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
@@ -1153,7 +1506,7 @@ var UI;
                 UI.rfb.sendKey(KeyTable.XK_BackSpace);
             }
             for (i = newLen - inputs; i < newLen; i++) {
-                UI.rfb.sendKey(newValue.charCodeAt(i));
+                UI.rfb.sendKey(keysyms.fromUnicode(newValue.charCodeAt(i)).keysym);
             }
 
             // Control the text content length in the keyboardinput element
@@ -1168,64 +1521,71 @@ var UI;
                 // 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(UI.keepKeyboard, 0);
+                setTimeout(event.target.focus.bind(event.target), 0);
             } else {
                 UI.lastKeyboardinput = newValue;
             }
         },
 
+/* ------^-------
+ *   /KEYBOARD
+ * ==============
+ *   EXTRA KEYS
+ * ------v------*/
+
+        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");
+        },
+
+        closeExtraKeys: function() {
+            document.getElementById('noVNC_modifiers')
+                .classList.remove("noVNC_open");
+            document.getElementById('noVNC_toggle_extra_keys_button')
+                .classList.remove("noVNC_selected");
+        },
+
         toggleExtraKeys: function() {
-            UI.keepKeyboard();
-            if(UI.extraKeysVisible === false) {
-                document.getElementById('noVNC_toggleCtrl_button').style.display = "inline";
-                document.getElementById('noVNC_toggleAlt_button').style.display = "inline";
-                document.getElementById('noVNC_sendTab_button').style.display = "inline";
-                document.getElementById('noVNC_sendEsc_button').style.display = "inline";
-                document.getElementById('noVNC_toggleExtraKeys_button').className = "noVNC_status_button_selected";
-                UI.extraKeysVisible = true;
-            } else if(UI.extraKeysVisible === true) {
-                document.getElementById('noVNC_toggleCtrl_button').style.display = "";
-                document.getElementById('noVNC_toggleAlt_button').style.display = "";
-                document.getElementById('noVNC_sendTab_button').style.display = "";
-                document.getElementById('noVNC_sendEsc_button').style.display = "";
-                document.getElementById('noVNC_toggleExtraKeys_button').className = "noVNC_status_button";
-                UI.extraKeysVisible = false;
+            if(document.getElementById('noVNC_modifiers')
+                .classList.contains("noVNC_open")) {
+                UI.closeExtraKeys();
+            } else  {
+                UI.openExtraKeys();
             }
         },
 
         sendEsc: function() {
-            UI.keepKeyboard();
             UI.rfb.sendKey(KeyTable.XK_Escape);
         },
 
         sendTab: function() {
-            UI.keepKeyboard();
             UI.rfb.sendKey(KeyTable.XK_Tab);
         },
 
         toggleCtrl: function() {
-            UI.keepKeyboard();
-            if(UI.ctrlOn === false) {
-                UI.rfb.sendKey(KeyTable.XK_Control_L, true);
-                document.getElementById('noVNC_toggleCtrl_button').className = "noVNC_status_button_selected";
-                UI.ctrlOn = true;
-            } else if(UI.ctrlOn === true) {
+            var btn = document.getElementById('noVNC_toggle_ctrl_button');
+            if (btn.classList.contains("noVNC_selected")) {
                 UI.rfb.sendKey(KeyTable.XK_Control_L, false);
-                document.getElementById('noVNC_toggleCtrl_button').className = "noVNC_status_button";
-                UI.ctrlOn = false;
+                btn.classList.remove("noVNC_selected");
+            } else {
+                UI.rfb.sendKey(KeyTable.XK_Control_L, true);
+                btn.classList.add("noVNC_selected");
             }
         },
 
         toggleAlt: function() {
-            UI.keepKeyboard();
-            if(UI.altOn === false) {
-                UI.rfb.sendKey(KeyTable.XK_Alt_L, true);
-                document.getElementById('noVNC_toggleAlt_button').className = "noVNC_status_button_selected";
-                UI.altOn = true;
-            } else if(UI.altOn === true) {
+            var btn = document.getElementById('noVNC_toggle_alt_button');
+            if (btn.classList.contains("noVNC_selected")) {
                 UI.rfb.sendKey(KeyTable.XK_Alt_L, false);
-                document.getElementById('noVNC_toggleAlt_button').className = "noVNC_status_button";
-                UI.altOn = false;
+                btn.classList.remove("noVNC_selected");
+            } else {
+                UI.rfb.sendKey(KeyTable.XK_Alt_L, true);
+                btn.classList.add("noVNC_selected");
             }
         },
 
@@ -1234,50 +1594,60 @@ var UI;
         },
 
 /* ------^-------
- *   /KEYBOARD
+ *   /EXTRA KEYS
  * ==============
  *     MISC
  * ------v------*/
 
         setMouseButton: function(num) {
-            if (typeof num === 'undefined') {
-                // Disable mouse buttons
-                num = -1;
-            }
-            if (UI.rfb) {
+            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) {
-                    button.style.display = "";
+                var button = document.getElementById('noVNC_mouse_button' +
+                                                     blist[b]);
+                if (blist[b] === num && !view_only) {
+                    button.classList.remove("noVNC_hidden");
                 } else {
-                    button.style.display = "none";
+                    button.classList.add("noVNC_hidden");
                 }
             }
         },
 
         displayBlur: function() {
-            if (!UI.rfb) return;
-
-            UI.rfb.get_keyboard().set_focused(false);
-            UI.rfb.get_mouse().set_focused(false);
+            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) return;
+            if (UI.rfb && !UI.rfb.get_view_only()) {
+                UI.rfb.get_keyboard().set_focused(true);
+                UI.rfb.get_mouse().set_focused(true);
+            }
+        },
 
-            UI.rfb.get_keyboard().set_focused(true);
-            UI.rfb.get_mouse().set_focused(true);
+        updateSessionSize: function(rfb, width, height) {
+            UI.updateViewClip();
+            UI.updateViewDrag();
         },
 
-        // Display the desktop name in the document title
-        updateDocumentTitle: function(rfb, name) {
+        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();
+            }
+        },
+
         //Helper to add options to dropdown.
         addOption: function(selectbox, text, value) {
             var optn = document.createElement("OPTION");
@@ -1286,14 +1656,6 @@ var UI;
             selectbox.options.add(optn);
         },
 
-        setBarPosition: function() {
-            document.getElementById('noVNC_control_bar').style.top = (window.pageYOffset) + 'px';
-            document.getElementById('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px';
-
-            var vncwidth = document.getElementById('noVNC_container').style.offsetWidth;
-            document.getElementById('noVNC_control_bar').style.width = vncwidth + 'px';
-        }
-
 /* ------^-------
  *    /MISC
  * ==============