diff --git a/guacamole-common-js/src/main/webapp/modules/OnScreenKeyboard.js b/guacamole-common-js/src/main/webapp/modules/OnScreenKeyboard.js index 71a5a0897..4a2e0e560 100644 --- a/guacamole-common-js/src/main/webapp/modules/OnScreenKeyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/OnScreenKeyboard.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,110 +23,100 @@ var Guacamole = Guacamole || {}; /** - * Dynamic on-screen keyboard. Given the URL to an XML keyboard layout file, - * this object will download and use the XML to construct a clickable on-screen - * keyboard with its own key events. - * + * Dynamic on-screen keyboard. Given the layout object for an on-screen + * keyboard, this object will construct a clickable on-screen keyboard with its + * own key events. + * * @constructor - * @param {String} url The URL of an XML keyboard layout file. + * @param {Guacamole.OnScreenKeyboard.Layout} layout + * The layout of the on-screen keyboard to display. */ -Guacamole.OnScreenKeyboard = function(url) { - - var on_screen_keyboard = this; +Guacamole.OnScreenKeyboard = function(layout) { /** - * State of all modifiers. This is the bitwise OR of all active modifier - * values. - * - * @private + * Reference to this Guacamole.OnScreenKeyboard. + * + * @type Guacamole.OnScreenKeyboard */ - var modifiers = 0; + var osk = this; /** * Map of currently-set modifiers to the keysym associated with their * original press. When the modifier is cleared, this keysym must be * released. * + * @private * @type Object. */ - var modifier_keysyms = {}; + var modifierKeysyms = {}; + /** + * Map of all key names to their current pressed states. If a key is not + * pressed, it may not be in this map at all, but all pressed keys will + * have a corresponding mapping to true. + * + * @type Object. + */ + var pressed = {}; + + /** + * All scalable elements which are part of the on-screen keyboard. Each + * scalable element is carefully controlled to ensure the interface layout + * and sizing remains constant, even on browsers that would otherwise + * experience rounding error due to unit conversions. + * + * @private + * @type ScaledElement[] + */ var scaledElements = []; - - var modifier_masks = {}; - var next_mask = 1; /** - * Adds a class to an element. + * Adds a CSS class to an element. * * @private * @function - * @param {Element} element The element to add a class to. - * @param {String} classname The name of the class to add. + * @param {Element} element + * The element to add a class to. + * + * @param {String} classname + * The name of the class to add. */ - var addClass; + var addClass = function addClass(element, classname) { - /** - * Removes a class from an element. - * - * @private - * @function - * @param {Element} element The element to remove a class from. - * @param {String} classname The name of the class to remove. - */ - var removeClass; - - /** - * The number of mousemove events to require before re-enabling mouse - * event handling after receiving a touch event. - */ - this.touchMouseThreshold = 3; - - /** - * Counter of mouse events to ignore. This decremented by mousemove, and - * while non-zero, mouse events will have no effect. - * @private - */ - var ignore_mouse = 0; - - // Ignore all pending mouse events when touch events are the apparent source - function ignorePendingMouseEvents() { ignore_mouse = on_screen_keyboard.touchMouseThreshold; } - - // If Node.classList is supported, implement addClass/removeClass using that - if (Node.classList) { - - /** @ignore */ - addClass = function(element, classname) { + // If classList supported, use that + if (element.classList) element.classList.add(classname); - }; - - /** @ignore */ - removeClass = function(element, classname) { - element.classList.remove(classname); - }; - - } - // Otherwise, implement own - else { - - /** @ignore */ - addClass = function(element, classname) { - - // Simply add new class + // Otherwise, simply append the class + else element.className += " " + classname; - }; - - /** @ignore */ - removeClass = function(element, classname) { + }; - // Filter out classes with given name + /** + * Removes a CSS class from an element. + * + * @private + * @function + * @param {Element} element + * The element to remove a class from. + * + * @param {String} classname + * The name of the class to remove. + */ + var removeClass = function removeClass(element, classname) { + + // If classList supported, use that + if (element.classList) + element.classList.remove(classname); + + // Otherwise, manually filter out classes with given name + else { element.className = element.className.replace(/([^ ]+)[ ]*/g, - function(match, testClassname, spaces, offset, string) { + function removeMatchingClasses(match, testClassname) { // If same class, remove - if (testClassname == classname) + if (testClassname === classname) return ""; // Otherwise, allow @@ -134,393 +124,282 @@ Guacamole.OnScreenKeyboard = function(url) { } ); - - }; - - } - - // Returns a unique power-of-two value for the modifier with the - // given name. The same value will be returned for the same modifier. - function getModifierMask(name) { - - var value = modifier_masks[name]; - if (!value) { - - // Get current modifier, advance to next - value = next_mask; - next_mask <<= 1; - - // Store value of this modifier - modifier_masks[name] = value; - } - return value; - - } + }; - function ScaledElement(element, width, height, scaleFont) { + /** + * Counter of mouse events to ignore. This decremented by mousemove, and + * while non-zero, mouse events will have no effect. + * + * @private + * @type Number + */ + var ignoreMouse = 0; - this.width = width; - this.height = height; + /** + * Ignores all pending mouse events when touch events are the apparent + * source. Mouse events are ignored until at least touchMouseThreshold + * mouse events occur without corresponding touch events. + * + * @private + */ + var ignorePendingMouseEvents = function ignorePendingMouseEvents() { + ignoreMouse = osk.touchMouseThreshold; + }; + /** + * An element whose dimensions are maintained according to an arbitrary + * scale. The conversion factor for these arbitrary units to pixels is + * provided later via a call to scale(). + * + * @private + * @constructor + * @param {Element} element + * The element whose scale should be maintained. + * + * @param {Number} width + * The width of the element, in arbitrary units, relative to other + * ScaledElements. + * + * @param {Number} height + * The height of the element, in arbitrary units, relative to other + * ScaledElements. + * + * @param {Boolean} [scaleFont=false] + * Whether the line height and font size should be scaled as well. + */ + var ScaledElement = function ScaledElement(element, width, height, scaleFont) { + + /** + * The width of this ScaledElement, in arbitrary units, relative to + * other ScaledElements. + * + * @type Number + */ + this.width = width; + + /** + * The height of this ScaledElement, in arbitrary units, relative to + * other ScaledElements. + * + * @type Number + */ + this.height = height; + + /** + * Resizes the associated element, updating its dimensions according to + * the given pixels per unit. + * + * @param {Number} pixels + * The number of pixels to assign per arbitrary unit. + */ this.scale = function(pixels) { - element.style.width = (width * pixels) + "px"; - element.style.height = (height * pixels) + "px"; + // Scale element width/height + element.style.width = (width * pixels) + "px"; + element.style.height = (height * pixels) + "px"; + + // Scale font, if requested if (scaleFont) { element.style.lineHeight = (height * pixels) + "px"; element.style.fontSize = pixels + "px"; } - } - } + }; - // For each child of element, call handler defined in next - function parseChildren(element, next) { + }; - var children = element.childNodes; - for (var i=0; i= 0; i--) { + + // Get candidate key + var candidate = keys[i]; + + // If all required modifiers are pressed, use that key + if (modifiersPressed(candidate.requires)) + return candidate; + + } + + // No valid key + return null; + + }; + + /** + * Presses the key having the given name, updating the associated key + * element with the "guac-keyboard-pressed" CSS class. If the key is + * already pressed, this function has no effect. + * + * @param {String} keyName + * The name of the key to press. + * + * @param {String} keyElement + * The element associated with the given key. + */ + var press = function press(keyName, keyElement) { + + // Press key if not yet pressed + if (!pressed[keyName]) { + + addClass(keyElement, "guac-keyboard-pressed"); + + // Get current key based on modifier state + var key = getActiveKey(keyName); + + // Update modifier state + if (key.modifier) { + + // Construct classname for modifier + var modifierClass = "guac-keyboard-modifier-" + getCSSName(key.modifier); + + // Retrieve originally-pressed keysym, if modifier was already pressed + var originalKeysym = modifierKeysyms[key.modifier]; + + // Activate modifier if not pressed + if (!originalKeysym) { + + addClass(keyboard, modifierClass); + modifierKeysyms[key.modifier] = key.keysym; + + // Send key event + if (osk.onkeydown) + osk.onkeydown(key.keysym); + + } + + // Deactivate if not pressed + else { + + removeClass(keyboard, modifierClass); + delete modifierKeysyms[key.modifier]; + + // Send key event + if (osk.onkeyup) + osk.onkeyup(originalKeysym); + + } + + } + + // If not modifier, send key event now + else if (osk.onkeydown) + osk.onkeydown(key.keysym); + + // Mark key as pressed + pressed[keyName] = true; + + } + + }; + + /** + * Releases the key having the given name, removing the + * "guac-keyboard-pressed" CSS class from the associated element. If the + * key is already released, this function has no effect. + * + * @param {String} keyName + * The name of the key to release. + * + * @param {String} keyElement + * The element associated with the given key. + */ + var release = function release(keyName, keyElement) { + + // Release key if currently pressed + if (pressed[keyName]) { + + removeClass(keyElement, "guac-keyboard-pressed"); + + // Get current key based on modifier state + var key = getActiveKey(keyName); + + // Send key event if not a modifier key + if (!key.modifier && osk.onkeyup) + osk.onkeyup(key.keysym); + + // Mark key as released + pressed[keyName] = false; + + } + + }; // Create keyboard var keyboard = document.createElement("div"); keyboard.className = "guac-keyboard"; - // Retrieve keyboard XML - var xmlhttprequest = new XMLHttpRequest(); - xmlhttprequest.open("GET", url, false); - xmlhttprequest.send(null); - - var xml = xmlhttprequest.responseXML; - - if (xml) { - - function parse_row(e) { - - var row = document.createElement("div"); - row.className = "guac-keyboard-row"; - - parseChildren(e, { - - "column": function(e) { - row.appendChild(parse_column(e)); - }, - - "gap": function parse_gap(e) { - - // Create element - var gap = document.createElement("div"); - gap.className = "guac-keyboard-gap"; - - // Set gap size - var gap_units = 1; - if (e.getAttribute("size")) - gap_units = parseFloat(e.getAttribute("size")); - - scaledElements.push(new ScaledElement(gap, gap_units, gap_units)); - row.appendChild(gap); - - }, - - "key": function parse_key(e) { - - // Create element - var key_element = document.createElement("div"); - key_element.className = "guac-keyboard-key"; - - // Append class if specified - if (e.getAttribute("class")) - key_element.className += " " + e.getAttribute("class"); - - // Position keys using container div - var key_container_element = document.createElement("div"); - key_container_element.className = "guac-keyboard-key-container"; - key_container_element.appendChild(key_element); - - // Create key - var key = new Guacamole.OnScreenKeyboard.Key(); - - // Set key size - var key_units = 1; - if (e.getAttribute("size")) - key_units = parseFloat(e.getAttribute("size")); - - key.size = key_units; - - parseChildren(e, { - "cap": function parse_cap(e) { - - // TODO: Handle "sticky" attribute - - // Get content of key cap - var content = e.textContent || e.text; - - // If read as blank, assume cap is a single space. - if (content.length == 0) - content = " "; - - // Get keysym - var real_keysym = null; - if (e.getAttribute("keysym")) - real_keysym = parseInt(e.getAttribute("keysym")); - - // If no keysym specified, try to get from key content - else if (content.length == 1) { - - var charCode = content.charCodeAt(0); - if (charCode >= 0x0000 && charCode <= 0x00FF) - real_keysym = charCode; - else if (charCode >= 0x0100 && charCode <= 0x10FFFF) - real_keysym = 0x01000000 | charCode; - - } - - // Create cap - var cap = new Guacamole.OnScreenKeyboard.Cap(content, real_keysym); - - if (e.getAttribute("modifier")) - cap.modifier = e.getAttribute("modifier"); - - // Create cap element - var cap_element = document.createElement("div"); - cap_element.className = "guac-keyboard-cap"; - cap_element.textContent = content; - key_element.appendChild(cap_element); - - // Append class if specified - if (e.getAttribute("class")) - cap_element.className += " " + e.getAttribute("class"); - - // Get modifier value - var modifierValue = 0; - if (e.getAttribute("if")) { - - // Get modifier value for specified comma-delimited - // list of required modifiers. - var requirements = e.getAttribute("if").split(","); - for (var i=0; i} keys + * A mapping of key name to key definition, where the key definition is + * the title of the key (a string), the keysym (a number), a single + * Key object, or an array of Key objects. + * + * @returns {Object.} + * A more-predictable mapping of key name to key definition, where the + * key definition is always simply an array of Key objects. + */ + var getKeys = function getKeys(keys) { + + var keyArrays = {}; + + // Coerce all keys into individual key arrays + for (var name in layout.keys) { + keyArrays[name] = asKeyArray(name, keys[name]); + } + + return keyArrays; + + }; + + /** + * Map of all key names to their corresponding set of keys. Each key name + * may correspond to multiple keys due to the effect of modifiers. + * + * @type Object. + */ + this.keys = getKeys(layout.keys); + + /** + * Given an arbitrary string representing the name of some component of the + * on-screen keyboard, returns a string formatted for use as a CSS class + * name. The result will be lowercase. Word boundaries previously denoted + * by CamelCase will be replaced by individual hyphens, as will all + * contiguous non-alphanumeric characters. + * + * @private + * @param {String} name + * An arbitrary string representing the name of some component of the + * on-screen keyboard. + * + * @returns {String} + * A string formatted for use as a CSS class name. + */ + var getCSSName = function getCSSName(name) { + + // Convert name from possibly-CamelCase to hyphenated lowercase + var cssName = name + .replace(/([a-z])([A-Z])/g, '$1-$2') + .replace(/[^A-Za-z0-9]+/g, '-') + .toLowerCase(); + + return cssName; + + }; + + /** + * Appends DOM elements to the given element as dictated by the layout + * structure object provided. If a name is provided, an additional CSS + * class, prepended with "guac-keyboard-", will be added to the top-level + * element. + * + * If the layout structure object is an array, all elements within that + * array will be recursively appended as children of a group, and the + * top-level element will be given the CSS class "guac-keyboard-group". + * + * If the layout structure object is an object, all properties within that + * object will be recursively appended as children of a group, and the + * top-level element will be given the CSS class "guac-keyboard-group". The + * name of each property will be applied as the name of each child object + * for the sake of CSS. Each property will be added in sorted order. + * + * If the layout structure object is a string, the key having that name + * will be appended. The key will be given the CSS class + * "guac-keyboard-key" and "guac-keyboard-key-NAME", where NAME is the name + * of the key. If the name of the key is a single character, this will + * first be transformed into the C-style hexadecimal literal for the + * Unicode codepoint of that character. For example, the key "A" would + * become "guac-keyboard-key-0x41". + * + * If the layout structure object is a number, a gap of that size will be + * inserted. The gap will be given the CSS class "guac-keyboard-gap", and + * will be scaled according to the same size units as each key. + * + * @private + * @param {Element} element + * The element to append elements to. + * + * @param {Array|Object|String|Number} object + * The layout structure object to use when constructing the elements to + * append. + * + * @param {String} [name] + * The name of the top-level element being appended, if any. + */ + var appendElements = function appendElements(element, object, name) { + + var i; + + // Create div which will become the group or key + var div = document.createElement('div'); + + // Add class based on name, if name given + if (name) + addClass(div, 'guac-keyboard-' + getCSSName(name)); + + // If an array, append each element + if (object instanceof Array) { + + // Add group class + addClass(div, 'guac-keyboard-group'); + + // Append all elements of array + for (i=0; i < object.length; i++) + appendElements(div, object[i]); + + } + + // If an object, append each property value + else if (object instanceof Object) { + + // Add group class + addClass(div, 'guac-keyboard-group'); + + // Append all children, sorted by name + var names = Object.keys(object).sort(); + for (i=0; i < names.length; i++) { + var name = names[i]; + appendElements(div, object[name], name); + } + + } + + // If a number, create as a gap + else if (typeof object === 'number') { + + // Add gap class + addClass(div, 'guac-keyboard-gap'); + + // Maintain scale + scaledElements.push(new ScaledElement(div, object, object)); + + } + + // If a string, create as a key + else if (typeof object === 'string') { + + // If key name is only one character, use codepoint for name + var keyName = object; + if (keyName.length === 1) + keyName = '0x' + keyName.charCodeAt(0).toString(16); + + // Add key container class + addClass(div, 'guac-keyboard-key-container'); + + // Create key element which will contain all possible caps + var keyElement = document.createElement('div'); + keyElement.className = 'guac-keyboard-key ' + + 'guac-keyboard-key-' + getCSSName(keyName); + + // Add all associated keys as caps within DOM + var keys = osk.keys[object]; + if (keys) { + for (i=0; i < keys.length; i++) { + + // Get current key + var key = keys[i]; + + // Create cap element for key + var capElement = document.createElement('div'); + capElement.className = 'guac-keyboard-cap'; + capElement.textContent = key.title; + + // Add classes for any requirements + for (var j=0; j < key.requires.length; j++) { + var requirement = key.requires[j]; + addClass(capElement, 'guac-keyboard-requires-' + getCSSName(requirement)); + addClass(keyElement, 'guac-keyboard-uses-' + getCSSName(requirement)); + } + + // Add cap to key within DOM + keyElement.appendChild(capElement); + + } + } + + // Add key to DOM, maintain scale + div.appendChild(keyElement); + scaledElements.push(new ScaledElement(div, osk.layout.keyWidths[object] || 1, 1, true)); + + /** + * Handles a touch event which results in the pressing of an OSK + * key. Touch events will result in mouse events being ignored for + * touchMouseThreshold events. + * + * @param {TouchEvent} e + * The touch event being handled. + */ + var touchPress = function touchPress(e) { + e.preventDefault(); + ignoreMouse = osk.touchMouseThreshold; + press(object, keyElement); + }; + + /** + * Handles a touch event which results in the release of an OSK + * key. Touch events will result in mouse events being ignored for + * touchMouseThreshold events. + * + * @param {TouchEvent} e + * The touch event being handled. + */ + var touchRelease = function touchRelease(e) { + e.preventDefault(); + ignoreMouse = osk.touchMouseThreshold; + release(object, keyElement); + }; + + /** + * Handles a mouse event which results in the pressing of an OSK + * key. If mouse events are currently being ignored, this handler + * does nothing. + * + * @param {MouseEvent} e + * The touch event being handled. + */ + var mousePress = function mousePress(e) { + e.preventDefault(); + if (ignoreMouse === 0) + press(object, keyElement); + }; + + /** + * Handles a mouse event which results in the release of an OSK + * key. If mouse events are currently being ignored, this handler + * does nothing. + * + * @param {MouseEvent} e + * The touch event being handled. + */ + var mouseRelease = function mouseRelease(e) { + e.preventDefault(); + if (ignoreMouse === 0) + release(object, keyElement); + }; + + // Handle touch events on key + keyElement.addEventListener("touchstart", touchPress, true); + keyElement.addEventListener("touchend", touchRelease, true); + + // Handle mouse events on key + keyElement.addEventListener("mousedown", mousePress, true); + keyElement.addEventListener("mouseup", mouseRelease, true); + keyElement.addEventListener("mouseout", mouseRelease, true); + + } // end if object is key name + + // Add newly-created group/key + element.appendChild(div); + + }; + + // Create keyboard layout in DOM + appendElements(keyboard, layout.layout); + }; /** - * Basic representation of a cap of a key. The cap is the visible part of a key - * and determines the active behavior of a key when pressed. The state of all - * modifiers on the keyboard determines the active cap for all keys, thus - * each cap is associated with a set of modifiers. - * + * Represents an entire on-screen keyboard layout, including all available + * keys, their behaviors, and their relative position and sizing. + * * @constructor - * @param {String} text The text to be displayed within this cap. - * @param {Number} keysym The keysym this cap sends when its associated key is - * pressed or released. - * @param {String} modifier The modifier represented by this cap. + * @param {Guacamole.OnScreenKeyboard.Layout|Object} template + * The object whose identically-named properties will be used to initialize + * the properties of this layout. */ -Guacamole.OnScreenKeyboard.Cap = function(text, keysym, modifier) { - - /** - * Modifier represented by this keycap - */ - this.modifier = null; - - /** - * The text to be displayed within this keycap - */ - this.text = text; +Guacamole.OnScreenKeyboard.Layout = function(template) { /** - * The keysym this cap sends when its associated key is pressed/released + * The language of keyboard layout, such as "en_US". This property is for + * informational purposes only, but it is recommend to conform to the + * [language code]_[country code] format. + * + * @type String */ - this.keysym = keysym; + this.language = template.language; + + /** + * The type of keyboard layout, such as "qwerty". This property is for + * informational purposes only, and does not conform to any standard. + * + * @type String + */ + this.type = template.type; + + /** + * Map of key name to corresponding keysym, title, or key object. If only + * the keysym or title is provided, the key object will be created + * implicitly. In all cases, the name property of the key object will be + * taken from the name given in the mapping. + * + * @type Object. + */ + this.keys = template.keys; + + /** + * Arbitrarily nested, arbitrarily grouped key names. The contents of the + * layout will be traversed to produce an identically-nested grouping of + * keys in the DOM tree. All strings will be transformed into their + * corresponding sets of keys, while all objects and arrays will be + * transformed into named groups and anonymous groups respectively. Any + * numbers present will be transformed into gaps of that size, scaled + * according to the same units as each key. + * + * @type Object + */ + this.layout = template.layout; + + /** + * The width of the entire keyboard, in arbitrary units. The width of each + * key is relative to this width, as both width values are assumed to be in + * the same units. The conversion factor between these units and pixels is + * derived later via a call to resize() on the Guacamole.OnScreenKeyboard. + * + * @type Number + */ + this.width = template.width; + + /** + * The width of each key, in arbitrary units, relative to other keys in + * this layout. The true pixel size of each key will be determined by the + * overall size of the keyboard. If not defined here, the width of each + * key will default to 1. + * + * @type Object. + */ + this.keyWidths = template.keyWidths || {}; + +}; + +/** + * Represents a single key, or a single possible behavior of a key. Each key + * on the on-screen keyboard must have at least one associated + * Guacamole.OnScreenKeyboard.Key, whether that key is explicitly defined or + * implied, and may have multiple Guacamole.OnScreenKeyboard.Key if behavior + * depends on modifier states. + * + * @constructor + * @param {Guacamole.OnScreenKeyboard.Key|Object} template + * The object whose identically-named properties will be used to initialize + * the properties of this key. + * + * @param {String} [name] + * The name to use instead of any name provided within the template, if + * any. If omitted, the name within the template will be used, assuming the + * template contains a name. + */ +Guacamole.OnScreenKeyboard.Key = function(template, name) { + + /** + * The unique name identifying this key within the keyboard layout. + * + * @type String + */ + this.name = name || template.name; + + /** + * The human-readable title that will be displayed to the user within the + * key. If not provided, this will be derived from the key name. + * + * @type String + */ + this.title = template.title || this.name; + + /** + * The keysym to be pressed/released when this key is pressed/released. If + * not provided, this will be derived from the title if the title is a + * single character. + * + * @type Number + */ + this.keysym = template.keysym || (function deriveKeysym(title) { + + // Do not derive keysym if title is not exactly one character + if (!title || title.length !== 1) + return null; + + // For characters between U+0000 and U+00FF, the keysym is the codepoint + var charCode = title.charCodeAt(0); + if (charCode >= 0x0000 && charCode <= 0x00FF) + return charCode; + + // For characters between U+0100 and U+10FFFF, the keysym is the codepoint or'd with 0x01000000 + if (charCode >= 0x0100 && charCode <= 0x10FFFF) + return 0x01000000 | charCode; + + // Unable to derive keysym + return null; + + })(this.title); + + /** + * The name of the modifier set when the key is pressed and cleared when + * this key is released, if any. The names of modifiers are distinct from + * the names of keys; both the "RightShift" and "LeftShift" keys may set + * the "shift" modifier, for example. By default, the key will affect no + * modifiers. + * + * @type String + */ + this.modifier = template.modifier; + + /** + * An array containing the names of each modifier required for this key to + * have an effect. For example, a lowercase letter may require nothing, + * while an uppercase letter would require "shift", assuming the Shift key + * is named "shift" within the layout. By default, the key will require + * no modifiers. + * + * @type String[] + */ + this.requires = template.requires || []; - // Set modifier if provided - if (modifier) this.modifier = modifier; - }; diff --git a/guacamole/src/main/webapp/app/osk/directives/guacOsk.js b/guacamole/src/main/webapp/app/osk/directives/guacOsk.js index decb06f8d..bca9bc9b4 100644 --- a/guacamole/src/main/webapp/app/osk/directives/guacOsk.js +++ b/guacamole/src/main/webapp/app/osk/directives/guacOsk.js @@ -40,8 +40,13 @@ angular.module('osk').directive('guacOsk', [function guacOsk() { }, templateUrl: 'app/osk/templates/guacOsk.html', - controller: ['$scope', '$rootScope', '$window', '$element', - function guacOsk($scope, $rootScope, $window, $element) { + controller: ['$scope', '$injector', '$element', + function guacOsk($scope, $injector, $element) { + + // Required services + var $http = $injector.get('$http'); + var $rootScope = $injector.get('$rootScope'); + var cacheService = $injector.get('cacheService'); /** * The current on-screen keyboard, if any. @@ -67,7 +72,7 @@ angular.module('osk').directive('guacOsk', [function guacOsk() { }; // Set layout whenever URL changes - $scope.$watch("layout", function setLayout(layout) { + $scope.$watch("layout", function setLayout(url) { // Remove current keyboard if (keyboard) { @@ -76,24 +81,40 @@ angular.module('osk').directive('guacOsk', [function guacOsk() { } // Load new keyboard - if (layout) { + if (url) { - // Add OSK element - keyboard = new Guacamole.OnScreenKeyboard(layout); - main.appendChild(keyboard.getElement()); + // Retrieve layout JSON + $http({ + cache : cacheService.languages, + method : 'GET', + url : url + }) - // Init size - keyboard.resize(main.offsetWidth); + // Build OSK with retrieved layout + .success(function layoutRetrieved(layout) { - // Broadcast keydown for each key pressed - keyboard.onkeydown = function(keysym) { - $rootScope.$broadcast('guacSyntheticKeydown', keysym); - }; - - // Broadcast keydown for each key released - keyboard.onkeyup = function(keysym) { - $rootScope.$broadcast('guacSyntheticKeyup', keysym); - }; + // Abort if the layout changed while we were waiting for a response + if ($scope.layout !== url) + return; + + // Add OSK element + keyboard = new Guacamole.OnScreenKeyboard(layout); + main.appendChild(keyboard.getElement()); + + // Init size + keyboard.resize(main.offsetWidth); + + // Broadcast keydown for each key pressed + keyboard.onkeydown = function(keysym) { + $rootScope.$broadcast('guacSyntheticKeydown', keysym); + }; + + // Broadcast keydown for each key released + keyboard.onkeyup = function(keysym) { + $rootScope.$broadcast('guacSyntheticKeyup', keysym); + }; + + }); } diff --git a/guacamole/src/main/webapp/app/osk/styles/osk.css b/guacamole/src/main/webapp/app/osk/styles/osk.css index ad131bed4..f61ac6f9d 100644 --- a/guacamole/src/main/webapp/app/osk/styles/osk.css +++ b/guacamole/src/main/webapp/app/osk/styles/osk.css @@ -36,8 +36,15 @@ vertical-align: middle; } +.guac-keyboard, +.guac-keyboard * { + overflow: hidden; + white-space: nowrap; +} + .guac-keyboard .guac-keyboard-key-container { display: inline-block; + margin: 0.05em; } .guac-keyboard .guac-keyboard-key { @@ -51,7 +58,6 @@ .guac-keyboard .guac-keyboard-cap { color: white; - font-family: sans-serif; font-size: 50%; font-weight: lighter; text-align: center; @@ -67,26 +73,26 @@ border-color: #666; } -.guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key.shift, -.guac-keyboard.guac-keyboard-modifier-numsym .guac-keyboard-key.numsym { +/* Active shift */ +.guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-rshift, +.guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-lshift, + +/* Active ctrl */ +.guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-rctrl, +.guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-lctrl, + +/* Active alt */ +.guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-ralt, +.guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-lalt, + +/* Active caps */ +.guac-keyboard.guac-keyboard-modifier-caps .guac-keyboard-key-caps { background: #882; border-color: #DD4; } -.guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key.control, -.guac-keyboard.guac-keyboard-modifier-numsym .guac-keyboard-key.numsym { - background: #882; - border-color: #DD4; -} - -.guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key.alt, -.guac-keyboard.guac-keyboard-modifier-numsym .guac-keyboard-key.numsym { - background: #882; - border-color: #DD4; -} - -.guac-keyboard.guac-keyboard-modifier-super .guac-keyboard-key.super, -.guac-keyboard.guac-keyboard-modifier-numsym .guac-keyboard-key.numsym { +.guac-keyboard.guac-keyboard-modifier-super .guac-keyboard-key-super, +.guac-keyboard.guac-keyboard-modifier-super .guac-keyboard-key-menu { background: #882; border-color: #DD4; } @@ -97,16 +103,54 @@ border-style: inset; } -.guac-keyboard .guac-keyboard-row { +.guac-keyboard .guac-keyboard-group { line-height: 0; } -.guac-keyboard .guac-keyboard-column { +.guac-keyboard .guac-keyboard-group.guac-keyboard-alpha, +.guac-keyboard .guac-keyboard-group.guac-keyboard-movement { display: inline-block; text-align: center; vertical-align: top; } +.guac-keyboard .guac-keyboard-group.guac-keyboard-main { + + /* IE10 */ + display: -ms-flexbox; + -ms-flex-align: stretch; + -ms-flex-direction: row; + + /* Ancient Mozilla */ + display: -moz-box; + -moz-box-align: stretch; + -moz-box-orient: horizontal; + + /* Ancient WebKit */ + display: -webkit-box; + -webkit-box-align: stretch; + -webkit-box-orient: horizontal; + + /* Old WebKit */ + display: -webkit-flex; + -webkit-align-items: stretch; + -webkit-flex-direction: row; + + /* W3C */ + display: flex; + align-items: stretch; + flex-direction: row; + +} + +.guac-keyboard .guac-keyboard-group.guac-keyboard-movement { + -ms-flex: 1 1 auto; + -moz-box-flex: 1; + -webkit-box-flex: 1; + -webkit-flex: 1 1 auto; + flex: 1 1 auto; +} + .guac-keyboard .guac-keyboard-gap { display: inline-block; } @@ -115,9 +159,6 @@ .guac-keyboard:not(.guac-keyboard-modifier-caps) .guac-keyboard-cap.guac-keyboard-requires-caps, -.guac-keyboard:not(.guac-keyboard-modifier-numsym) -.guac-keyboard-cap.guac-keyboard-requires-numsym, - .guac-keyboard:not(.guac-keyboard-modifier-shift) .guac-keyboard-cap.guac-keyboard-requires-shift, @@ -127,10 +168,6 @@ .guac-keyboard-key.guac-keyboard-uses-shift .guac-keyboard-cap:not(.guac-keyboard-requires-shift), -.guac-keyboard.guac-keyboard-modifier-numsym -.guac-keyboard-key.guac-keyboard-uses-numsym -.guac-keyboard-cap:not(.guac-keyboard-requires-numsym), - .guac-keyboard.guac-keyboard-modifier-caps .guac-keyboard-key.guac-keyboard-uses-caps .guac-keyboard-cap:not(.guac-keyboard-requires-caps) { diff --git a/guacamole/src/main/webapp/layouts/en-us-qwerty-mobile.xml b/guacamole/src/main/webapp/layouts/en-us-qwerty-mobile.xml deleted file mode 100644 index a61ec4c2b..000000000 --- a/guacamole/src/main/webapp/layouts/en-us-qwerty-mobile.xml +++ /dev/null @@ -1,316 +0,0 @@ - - - - - - - - - - Tab - - - - q - 1 - Q - q - - - - w - 2 - W - w - - - - e - 3 - E - e - - - - r - 4 - R - r - - - - t - 5 - T - t - - - - y - 6 - Y - y - - - - u - 7 - U - u - - - - i - 8 - I - i - - - - o - 9 - O - o - - - - p - 0 - P - p - - - - [ - { - - - - ] - } - - - - Back - - - - - - - - ?123 - - - - a - # - A - a - - - - s - $ - S - s - - - - d - % - D - d - - - - f - & - F - f - - - - g - * - G - g - - - - h - - - H - h - - - - j - + - J - j - - - - k - ( - K - k - - - - l - ) - L - l - - - - ; - : - - - - ' - " - - - - Enter - - - - - - - - Shift - - - - z - < - Z - z - - - - x - > - X - x - - - - c - = - C - c - - - - v - ' - V - v - - - - b - ; - B - b - - - - n - , - N - n - - - - m - . - M - m - - - - , - ! - ! - ! - - - - . - ? - ? - ? - - - - / - ? - - - - Shift - - - - - - - - Ctrl - - - - Super - - - - Alt - - - - - - - - Alt - - - - Menu - - - - Ctrl - - - diff --git a/guacamole/src/main/webapp/layouts/en-us-qwerty.json b/guacamole/src/main/webapp/layouts/en-us-qwerty.json new file mode 100644 index 000000000..fc7cdfcea --- /dev/null +++ b/guacamole/src/main/webapp/layouts/en-us-qwerty.json @@ -0,0 +1,400 @@ +{ + + "language" : "en_US", + "type" : "qwerty", + "width" : 22, + + "keys" : { + + "Back" : 65288, + "Tab" : 65289, + "Enter" : 65293, + "Esc" : 65307, + "Home" : 65360, + "PgUp" : 65365, + "PgDn" : 65366, + "End" : 65367, + "Ins" : 65379, + "F1" : 65470, + "F2" : 65471, + "F3" : 65472, + "F4" : 65473, + "F5" : 65474, + "F6" : 65475, + "F7" : 65476, + "F8" : 65477, + "F9" : 65478, + "F10" : 65479, + "F11" : 65480, + "F12" : 65481, + "Del" : 65535, + + "Space" : " ", + + "Left" : [{ + "title" : "←", + "keysym" : 65361 + }], + "Up" : [{ + "title" : "↑", + "keysym" : 65362 + }], + "Right" : [{ + "title" : "→", + "keysym" : 65363 + }], + "Down" : [{ + "title" : "↓", + "keysym" : 65364 + }], + + "Menu" : [{ + "title" : "Menu", + "modifier" : "super", + "keysym" : 65383 + }], + "LShift" : [{ + "title" : "Shift", + "modifier" : "shift", + "keysym" : 65505 + }], + "RShift" : [{ + "title" : "Shift", + "modifier" : "shift", + "keysym" : 65506 + }], + "LCtrl" : [{ + "title" : "Ctrl", + "modifier" : "control", + "keysym" : 65507 + }], + "RCtrl" : [{ + "title" : "Ctrl", + "modifier" : "control", + "keysym" : 65508 + }], + "Caps" : [{ + "title" : "Caps", + "modifier" : "caps", + "keysym" : 65509 + }], + "LAlt" : [{ + "title" : "Alt", + "modifier" : "alt", + "keysym" : 65513 + }], + "RAlt" : [{ + "title" : "Alt", + "modifier" : "alt", + "keysym" : 65514 + }], + "Super" : [{ + "title" : "Super", + "modifier" : "super", + "keysym" : 65515 + }], + + "`" : [ + { "title" : "`", "requires" : [ ] }, + { "title" : "~", "requires" : [ "shift" ] } + ], + "1" : [ + { "title" : "1", "requires" : [ ] }, + { "title" : "!", "requires" : [ "shift" ] } + ], + "2" : [ + { "title" : "2", "requires" : [ ] }, + { "title" : "@", "requires" : [ "shift" ] } + ], + "3" : [ + { "title" : "3", "requires" : [ ] }, + { "title" : "#", "requires" : [ "shift" ] } + ], + "4" : [ + { "title" : "4", "requires" : [ ] }, + { "title" : "$", "requires" : [ "shift" ] } + ], + "5" : [ + { "title" : "5", "requires" : [ ] }, + { "title" : "%", "requires" : [ "shift" ] } + ], + "6" : [ + { "title" : "6", "requires" : [ ] }, + { "title" : "^", "requires" : [ "shift" ] } + ], + "7" : [ + { "title" : "7", "requires" : [ ] }, + { "title" : "&", "requires" : [ "shift" ] } + ], + "8" : [ + { "title" : "8", "requires" : [ ] }, + { "title" : "*", "requires" : [ "shift" ] } + ], + "9" : [ + { "title" : "9", "requires" : [ ] }, + { "title" : "(", "requires" : [ "shift" ] } + ], + "0" : [ + { "title" : "0", "requires" : [ ] }, + { "title" : ")", "requires" : [ "shift" ] } + ], + "-" : [ + { "title" : "-", "requires" : [ ] }, + { "title" : "_", "requires" : [ "shift" ] } + ], + "=" : [ + { "title" : "=", "requires" : [ ] }, + { "title" : "+", "requires" : [ "shift" ] } + ], + "," : [ + { "title" : ",", "requires" : [ ] }, + { "title" : "<", "requires" : [ "shift" ] } + ], + "." : [ + { "title" : ".", "requires" : [ ] }, + { "title" : ">", "requires" : [ "shift" ] } + ], + "/" : [ + { "title" : "/", "requires" : [ ] }, + { "title" : "?", "requires" : [ "shift" ] } + ], + "[" : [ + { "title" : "[", "requires" : [ ] }, + { "title" : "{", "requires" : [ "shift" ] } + ], + "]" : [ + { "title" : "]", "requires" : [ ] }, + { "title" : "}", "requires" : [ "shift" ] } + ], + "\\" : [ + { "title" : "\\", "requires" : [ ] }, + { "title" : "|", "requires" : [ "shift" ] } + ], + ";" : [ + { "title" : ";", "requires" : [ ] }, + { "title" : ":", "requires" : [ "shift" ] } + ], + "'" : [ + { "title" : "'", "requires" : [ ] }, + { "title" : "\"", "requires" : [ "shift" ] } + ], + + "q" : [ + { "title" : "q", "requires" : [ ] }, + { "title" : "Q", "requires" : [ "caps" ] }, + { "title" : "Q", "requires" : [ "shift" ] }, + { "title" : "q", "requires" : [ "caps", "shift" ] } + ], + "w" : [ + { "title" : "w", "requires" : [ ] }, + { "title" : "W", "requires" : [ "caps" ] }, + { "title" : "W", "requires" : [ "shift" ] }, + { "title" : "w", "requires" : [ "caps", "shift" ] } + ], + "e" : [ + { "title" : "e", "requires" : [ ] }, + { "title" : "E", "requires" : [ "caps" ] }, + { "title" : "E", "requires" : [ "shift" ] }, + { "title" : "e", "requires" : [ "caps", "shift" ] } + ], + "r" : [ + { "title" : "r", "requires" : [ ] }, + { "title" : "R", "requires" : [ "caps" ] }, + { "title" : "R", "requires" : [ "shift" ] }, + { "title" : "r", "requires" : [ "caps", "shift" ] } + ], + "t" : [ + { "title" : "t", "requires" : [ ] }, + { "title" : "T", "requires" : [ "caps" ] }, + { "title" : "T", "requires" : [ "shift" ] }, + { "title" : "t", "requires" : [ "caps", "shift" ] } + ], + "y" : [ + { "title" : "y", "requires" : [ ] }, + { "title" : "Y", "requires" : [ "caps" ] }, + { "title" : "Y", "requires" : [ "shift" ] }, + { "title" : "y", "requires" : [ "caps", "shift" ] } + ], + "u" : [ + { "title" : "u", "requires" : [ ] }, + { "title" : "U", "requires" : [ "caps" ] }, + { "title" : "U", "requires" : [ "shift" ] }, + { "title" : "u", "requires" : [ "caps", "shift" ] } + ], + "i" : [ + { "title" : "i", "requires" : [ ] }, + { "title" : "I", "requires" : [ "caps" ] }, + { "title" : "I", "requires" : [ "shift" ] }, + { "title" : "i", "requires" : [ "caps", "shift" ] } + ], + "o" : [ + { "title" : "o", "requires" : [ ] }, + { "title" : "O", "requires" : [ "caps" ] }, + { "title" : "O", "requires" : [ "shift" ] }, + { "title" : "o", "requires" : [ "caps", "shift" ] } + ], + "p" : [ + { "title" : "p", "requires" : [ ] }, + { "title" : "P", "requires" : [ "caps" ] }, + { "title" : "P", "requires" : [ "shift" ] }, + { "title" : "p", "requires" : [ "caps", "shift" ] } + ], + "a" : [ + { "title" : "a", "requires" : [ ] }, + { "title" : "A", "requires" : [ "caps" ] }, + { "title" : "A", "requires" : [ "shift" ] }, + { "title" : "a", "requires" : [ "caps", "shift" ] } + ], + "s" : [ + { "title" : "s", "requires" : [ ] }, + { "title" : "S", "requires" : [ "caps" ] }, + { "title" : "S", "requires" : [ "shift" ] }, + { "title" : "s", "requires" : [ "caps", "shift" ] } + ], + "d" : [ + { "title" : "d", "requires" : [ ] }, + { "title" : "D", "requires" : [ "caps" ] }, + { "title" : "D", "requires" : [ "shift" ] }, + { "title" : "d", "requires" : [ "caps", "shift" ] } + ], + "f" : [ + { "title" : "f", "requires" : [ ] }, + { "title" : "F", "requires" : [ "caps" ] }, + { "title" : "F", "requires" : [ "shift" ] }, + { "title" : "f", "requires" : [ "caps", "shift" ] } + ], + "g" : [ + { "title" : "g", "requires" : [ ] }, + { "title" : "G", "requires" : [ "caps" ] }, + { "title" : "G", "requires" : [ "shift" ] }, + { "title" : "g", "requires" : [ "caps", "shift" ] } + ], + "h" : [ + { "title" : "h", "requires" : [ ] }, + { "title" : "H", "requires" : [ "caps" ] }, + { "title" : "H", "requires" : [ "shift" ] }, + { "title" : "h", "requires" : [ "caps", "shift" ] } + ], + "j" : [ + { "title" : "j", "requires" : [ ] }, + { "title" : "J", "requires" : [ "caps" ] }, + { "title" : "J", "requires" : [ "shift" ] }, + { "title" : "j", "requires" : [ "caps", "shift" ] } + ], + "k" : [ + { "title" : "k", "requires" : [ ] }, + { "title" : "K", "requires" : [ "caps" ] }, + { "title" : "K", "requires" : [ "shift" ] }, + { "title" : "k", "requires" : [ "caps", "shift" ] } + ], + "l" : [ + { "title" : "l", "requires" : [ ] }, + { "title" : "L", "requires" : [ "caps" ] }, + { "title" : "L", "requires" : [ "shift" ] }, + { "title" : "l", "requires" : [ "caps", "shift" ] } + ], + "z" : [ + { "title" : "z", "requires" : [ ] }, + { "title" : "Z", "requires" : [ "caps" ] }, + { "title" : "Z", "requires" : [ "shift" ] }, + { "title" : "z", "requires" : [ "caps", "shift" ] } + ], + "x" : [ + { "title" : "x", "requires" : [ ] }, + { "title" : "X", "requires" : [ "caps" ] }, + { "title" : "X", "requires" : [ "shift" ] }, + { "title" : "x", "requires" : [ "caps", "shift" ] } + ], + "c" : [ + { "title" : "c", "requires" : [ ] }, + { "title" : "C", "requires" : [ "caps" ] }, + { "title" : "C", "requires" : [ "shift" ] }, + { "title" : "c", "requires" : [ "caps", "shift" ] } + ], + "v" : [ + { "title" : "v", "requires" : [ ] }, + { "title" : "V", "requires" : [ "caps" ] }, + { "title" : "V", "requires" : [ "shift" ] }, + { "title" : "v", "requires" : [ "caps", "shift" ] } + ], + "b" : [ + { "title" : "b", "requires" : [ ] }, + { "title" : "B", "requires" : [ "caps" ] }, + { "title" : "B", "requires" : [ "shift" ] }, + { "title" : "b", "requires" : [ "caps", "shift" ] } + ], + "n" : [ + { "title" : "n", "requires" : [ ] }, + { "title" : "N", "requires" : [ "caps" ] }, + { "title" : "N", "requires" : [ "shift" ] }, + { "title" : "n", "requires" : [ "caps", "shift" ] } + ], + "m" : [ + { "title" : "m", "requires" : [ ] }, + { "title" : "M", "requires" : [ "caps" ] }, + { "title" : "M", "requires" : [ "shift" ] }, + { "title" : "m", "requires" : [ "caps", "shift" ] } + ] + + }, + + "layout" : [ + + [ "Esc", 0.7, "F1", "F2", "F3", "F4", + 0.7, "F5", "F6", "F7", "F8", + 0.7, "F9", "F10", "F11", "F12" ], + + [ 0.1 ], + + { + "main" : { + "alpha" : [ + + [ "`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Back" ], + [ "Tab", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "\\" ], + [ "Caps", "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "Enter" ], + [ "LShift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "RShift" ], + [ "LCtrl", "Super", "LAlt", "Space", "RAlt", "Menu", "RCtrl" ] + + ], + + "movement" : [ + [ "Ins", "Home", "PgUp" ], + [ "Del", "End", "PgDn" ], + [ 1 ], + [ "Up" ], + [ "Left", "Down", "Right" ] + ] + } + } + + ], + + "keyWidths" : { + + "Back" : 2, + "Tab" : 1.5, + "\\" : 1.5, + "Caps" : 1.85, + "Enter" : 2.25, + "LShift" : 2.1, + "RShift" : 3.1, + + "LCtrl" : 1.6, + "Super" : 1.6, + "LAlt" : 1.6, + "Space" : 6.1, + "RAlt" : 1.6, + "Menu" : 1.6, + "RCtrl" : 1.6, + + "Ins" : 1.6, + "Home" : 1.6, + "PgUp" : 1.6, + "Del" : 1.6, + "End" : 1.6, + "PgDn" : 1.6 + + } + +} diff --git a/guacamole/src/main/webapp/layouts/en-us-qwerty.xml b/guacamole/src/main/webapp/layouts/en-us-qwerty.xml deleted file mode 100644 index 8cf8bb5e0..000000000 --- a/guacamole/src/main/webapp/layouts/en-us-qwerty.xml +++ /dev/null @@ -1,500 +0,0 @@ - - - - - - - - - - Esc - - - - F1 - - - - F2 - - - - F3 - - - - F4 - - - - F5 - - - - F6 - - - - F7 - - - - F8 - - - - F9 - - - - F10 - - - - F11 - - - - F12 - - - - - - - - - ` - ~ - - - - 1 - ! - - - - 2 - @ - - - - 3 - # - - - - 4 - $ - - - - 5 - % - - - - 6 - ^ - - - - 7 - & - - - - 8 - * - - - - 9 - ( - - - - 0 - ) - - - - - - _ - - - - = - + - - - - Back - - - - - - - - Tab - - - - q - Q - Q - q - - - - w - W - W - w - - - - e - E - E - e - - - - r - R - R - r - - - - t - T - T - t - - - - y - Y - Y - y - - - - u - U - U - u - - - - i - I - I - i - - - - o - O - O - o - - - - p - P - P - p - - - - [ - { - - - - ] - } - - - - \ - | - - - - - - - - Caps - - - - a - A - A - a - - - - s - S - S - s - - - - d - D - D - d - - - - f - F - F - f - - - - g - G - G - g - - - - h - H - H - h - - - - j - J - J - j - - - - k - K - K - k - - - - l - L - L - l - - - - ; - : - - - - ' - " - - - - Enter - - - - - - - - Shift - - - - z - Z - Z - z - - - - x - X - X - x - - - - c - C - C - c - - - - v - V - V - v - - - - b - B - B - b - - - - n - N - N - n - - - - m - M - M - m - - - - , - < - - - - . - > - - - - / - ? - - - - Shift - - - - - - - - Ctrl - - - - Super - - - - Alt - - - - - - - - Alt - - - - Menu - - - - Ctrl - - - - - - - - - - - - Ins - - - - Home - - - - PgUp - - - - - - Del - - - - End - - - - PgDn - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index 2ea7b6f78..79e5a6db6 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -117,7 +117,7 @@ "TEXT_RECONNECT_COUNTDOWN" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}...", "TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}", - "URL_OSK_LAYOUT" : "layouts/en-us-qwerty.xml" + "URL_OSK_LAYOUT" : "layouts/en-us-qwerty.json" },