-/*
- * Guacamole - Clientless Remote Desktop
- * Copyright (C) 2010 Michael Jumper
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
*
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
*
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-
-function GuacamoleOnScreenKeyboard(url) {
-
- var tabIndex = 1;
- var allKeys = new Array();
- var modifierState = new function() {};
-
- function getKeySize(size) {
- return (5*size) + "ex";
- }
+ * The Original Code is guac-common-js.
+ *
+ * The Initial Developer of the Original Code is
+ * Michael Jumper.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
- function getCapSize(size) {
- return (5*size - 0.5) + "ex";
+/**
+ * Namespace for all Guacamole JavaScript objects.
+ * @namespace
+ */
+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.
+ *
+ * @constructor
+ * @param {String} url The URL of an XML keyboard layout file.
+ */
+Guacamole.OnScreenKeyboard = function(url) {
+
+ var on_screen_keyboard = this;
+
+ /**
+ * State of all modifiers. This is the bitwise OR of all active modifier
+ * values.
+ *
+ * @private
+ */
+ var modifiers = 0;
+
+ var scaledElements = [];
+
+ var modifiers = {};
+ var currentModifier = 1;
+
+ /**
+ * Adds a 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.
+ */
+ var addClass;
+
+ /**
+ * 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;
+
+ // If Node.classList is supported, implement addClass/removeClass using that
+ if (Node.classList) {
+
+ /** @ignore */
+ addClass = function(element, classname) {
+ element.classList.add(classname);
+ };
+
+ /** @ignore */
+ removeClass = function(element, classname) {
+ element.classList.remove(classname);
+ };
+
}
- function clearModifiers() {
-
- // Send key release events for all pressed modifiers
- for (var k=0; k<allKeys.length; k++) {
+ // Otherwise, implement own
+ else {
- var key = allKeys[k];
- var cap = key.getCap();
- var modifier = cap.getModifier();
+ /** @ignore */
+ addClass = function(element, classname) {
- if (modifier && isModifierActive(modifier) && !cap.isSticky() && key.isPressed())
- key.release();
+ // Simply add new class
+ element.className += " " + classname;
- }
-
- }
+ };
+
+ /** @ignore */
+ removeClass = function(element, classname) {
- function setModifierReleased(modifier) {
- if (isModifierActive(modifier))
- modifierState[modifier]--;
- }
+ // Filter out classes with given name
+ element.className = element.className.replace(/([^ ]+)[ ]*/g,
+ function(match, testClassname, spaces, offset, string) {
- function setModifierPressed(modifier) {
- if (modifierState[modifier] == null)
- modifierState[modifier] = 1;
- else
- modifierState[modifier]++;
- }
+ // If same class, remove
+ if (testClassname == classname)
+ return "";
- function isModifierActive(modifier) {
- if (modifierState[modifier] > 0)
- return true;
+ // Otherwise, allow
+ return match;
+
+ }
+ );
- return false;
+ };
+
}
- function toggleModifierPressed(modifier) {
- if (isModifierActive(modifier))
- setModifierReleased(modifier);
- else
- setModifierPressed(modifier);
- }
+ // 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 getModifier(name) {
+
+ var value = modifiers[name];
+ if (!value) {
- function refreshAllKeysState() {
- for (var k=0; k<allKeys.length; k++)
- allKeys[k].refreshState();
- }
+ // Get current modifier, advance to next
+ value = currentModifier;
+ currentModifier <<= 1;
- function Key(key) {
+ // Store value of this modifier
+ modifiers[name] = value;
- function Cap(cap) {
+ }
- // Displayed text
- var displayText = cap.textContent;
+ return value;
- // Keysym
- var keysym = null;
- if (cap.attributes["keysym"])
- keysym = parseInt(cap.attributes["keysym"].value);
+ }
- // If keysym not specified, get keysym from display text.
- else if (displayText.length == 1) {
+ function ScaledElement(element, width, height, scaleFont) {
- var charCode = displayText.charCodeAt(0);
+ this.width = width;
+ this.height = height;
- if (charCode >= 0x0000 && charCode <= 0x00FF)
- keysym = charCode;
+ this.scale = function(pixels) {
+ element.style.width = (width * pixels) + "px";
+ element.style.height = (height * pixels) + "px";
- else if (charCode >= 0x0100 && charCode <= 0x10FFFF)
- keysym = 0x01000000 | charCode;
+ if (scaleFont) {
+ element.style.lineHeight = (height * pixels) + "px";
+ element.style.fontSize = pixels + "px";
}
+ }
- // Required modifiers for this keycap
- var reqMod = null;
- if (cap.attributes["if"])
- reqMod = cap.attributes["if"].value.split(",");
-
+ }
- // Modifier represented by this keycap
- var modifier = null;
- if (cap.attributes["modifier"])
- modifier = cap.attributes["modifier"].value;
-
+ // For each child of element, call handler defined in next
+ function parseChildren(element, next) {
- // Whether this key is sticky (toggles)
- // Currently only valid for modifiers.
- var sticky = false;
- if (cap.attributes["sticky"] && cap.attributes["sticky"].value == "true")
- sticky = true;
+ var children = element.childNodes;
+ for (var i=0; i<children.length; i++) {
- this.getDisplayText = function() {
- return cap.textContent;
- };
+ // Get child node
+ var child = children[i];
- this.getKeySym = function() {
- return keysym;
- };
+ // Do not parse text nodes
+ if (!child.tagName)
+ continue;
- this.getRequiredModifiers = function() {
- return reqMod;
- };
+ // Get handler for node
+ var handler = next[child.tagName];
- this.getModifier = function() {
- return modifier;
- };
+ // Call handler if defined
+ if (handler)
+ handler(child);
- this.isSticky = function() {
- return sticky;
- };
+ // Throw exception if no handler
+ else
+ throw new Error(
+ "Unexpected " + child.tagName
+ + " within " + element.tagName
+ );
}
- var size = null;
- if (key.attributes["size"])
- size = parseFloat(key.attributes["size"].value);
-
- var caps = key.getElementsByTagName("cap");
- var keycaps = new Array();
- for (var i=0; i<caps.length; i++)
- keycaps.push(new Cap(caps[i]));
-
- var rowKey = document.createElement("div");
- rowKey.className = "key";
-
- var keyCap = document.createElement("div");
- keyCap.className = "cap";
- rowKey.appendChild(keyCap);
-
-
- var STATE_RELEASED = 0;
- var STATE_PRESSED = 1;
- var state = STATE_RELEASED;
-
- rowKey.isPressed = function() {
- return state == STATE_PRESSED;
- }
+ }
- var currentCap = null;
- function refreshState(modifier) {
+ // Create keyboard
+ var keyboard = document.createElement("div");
+ keyboard.className = "guac-keyboard";
- // Find current cap
- currentCap = null;
- for (var j=0; j<keycaps.length; j++) {
+ // Retrieve keyboard XML
+ var xmlhttprequest = new XMLHttpRequest();
+ xmlhttprequest.open("GET", url, false);
+ xmlhttprequest.send(null);
- var keycap = keycaps[j];
- var required = keycap.getRequiredModifiers();
+ var xml = xmlhttprequest.responseXML;
- var matches = true;
+ if (xml) {
- // If modifiers required, make sure all modifiers are active
- if (required) {
+ 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<requirements.length; i++) {
+ modifierValue |= getModifier(requirements[i]);
+ addClass(cap_element, "guac-keyboard-requires-" + requirements[i]);
+ addClass(key_element, "guac-keyboard-uses-" + requirements[i]);
+ }
+
+ }
+
+ // Store cap
+ key.modifierMask |= modifierValue;
+ key.caps[modifierValue] = cap;
- for (var k=0; k<required.length; k++) {
- if (!isModifierActive(required[k])) {
- matches = false;
- break;
}
- }
+ });
- }
+ scaledElements.push(new ScaledElement(key_container_element, key_units, 1, true));
+ row.appendChild(key_container_element);
- if (matches)
- currentCap = keycap;
-
- }
+ // Set up click handler for key
+ function press(e) {
- rowKey.className = "key";
+ // Press key if not yet pressed
+ if (!key.pressed) {
- if (currentCap.getModifier())
- rowKey.className += " modifier";
+ addClass(key_element, "guac-keyboard-pressed");
- if (currentCap.isSticky())
- rowKey.className += " sticky";
+ // Get current cap based on modifier state
+ var cap = key.getCap(modifiers);
- if (isModifierActive(currentCap.getModifier()))
- rowKey.className += " active";
+ // Update modifier state
+ if (cap.modifier) {
- if (state == STATE_PRESSED)
- rowKey.className += " pressed";
-
- keyCap.textContent = currentCap.getDisplayText();
- }
- rowKey.refreshState = refreshState;
-
- rowKey.getCap = function() {
- return currentCap;
- };
+ // Construct classname for modifier
+ var modifierClass = "guac-keyboard-modifier-" + cap.modifier;
+ var modifierFlag = getModifier(cap.modifier);
- refreshState();
+ // Toggle modifier state
+ modifiers ^= modifierFlag;
- // Set size
- if (size) {
- rowKey.style.width = getKeySize(size);
- keyCap.style.width = getCapSize(size);
- }
+ // Activate modifier if pressed
+ if (modifiers & modifierFlag) {
+
+ addClass(keyboard, modifierClass);
+
+ // Send key event
+ if (on_screen_keyboard.onkeydown && cap.keysym)
+ on_screen_keyboard.onkeydown(cap.keysym);
+ }
+ // Deactivate if not pressed
+ else {
- // Set pressed, if released
- function press() {
+ removeClass(keyboard, modifierClass);
+
+ // Send key event
+ if (on_screen_keyboard.onkeyup && cap.keysym)
+ on_screen_keyboard.onkeyup(cap.keysym);
- if (state == STATE_RELEASED) {
+ }
- state = STATE_PRESSED;
+ }
- var keysym = currentCap.getKeySym();
- var modifier = currentCap.getModifier();
- var sticky = currentCap.isSticky();
+ // If not modifier, send key event now
+ else if (on_screen_keyboard.onkeydown && cap.keysym)
+ on_screen_keyboard.onkeydown(cap.keysym);
- if (keyPressedHandler && keysym)
- keyPressedHandler(keysym);
+ // Mark key as pressed
+ key.pressed = true;
- if (modifier) {
+ }
- // If sticky modifier, toggle
- if (sticky)
- toggleModifierPressed(modifier);
+ e.preventDefault();
- // Otherwise, just set on.
- else
- setModifierPressed(modifier);
+ };
- refreshAllKeysState();
- }
- else
- refreshState();
- }
+ function release(e) {
- }
- rowKey.press = press;
+ // Release key if currently pressed
+ if (key.pressed) {
+ // Get current cap based on modifier state
+ var cap = key.getCap(modifiers);
- // Set released, if pressed
- function release() {
+ removeClass(key_element, "guac-keyboard-pressed");
- if (state == STATE_PRESSED) {
+ // Send key event if not a modifier key
+ if (!cap.modifier && on_screen_keyboard.onkeyup && cap.keysym)
+ on_screen_keyboard.onkeyup(cap.keysym);
- state = STATE_RELEASED;
+ // Mark key as released
+ key.pressed = false;
- var keysym = currentCap.getKeySym();
- var modifier = currentCap.getModifier();
- var sticky = currentCap.isSticky();
+ }
- if (keyReleasedHandler && keysym)
- keyReleasedHandler(keysym);
+ e.preventDefault();
- if (modifier) {
+ };
- // If not sticky modifier, release modifier
- if (!sticky) {
- setModifierReleased(modifier);
- refreshAllKeysState();
- }
- else
- refreshState();
+ key_element.addEventListener("mousedown", press, true);
+ key_element.addEventListener("touchstart", press, true);
- }
- else {
- refreshState();
+ key_element.addEventListener("mouseup", release, true);
+ key_element.addEventListener("mouseout", release, true);
+ key_element.addEventListener("touchend", release, true);
- // If not a modifier, also release all pressed modifiers
- clearModifiers();
}
+
+ });
- }
-
- }
- rowKey.release = release;
-
- // Toggle press/release states
- function toggle() {
- if (state == STATE_PRESSED)
- release();
- else
- press();
- }
-
-
- // Send key press on mousedown
- rowKey.onmousedown = function(e) {
-
- e.stopPropagation();
-
- var modifier = currentCap.getModifier();
- var sticky = currentCap.isSticky();
-
- // Toggle non-sticky modifiers
- if (modifier && !sticky)
- toggle();
-
- // Press all others
- else
- press();
-
- return false;
- };
-
- // Send key release on mouseup/out
- rowKey.onmouseout =
- rowKey.onmouseout =
- rowKey.onmouseup = function(e) {
-
- e.stopPropagation();
-
- var modifier = currentCap.getModifier();
- var sticky = currentCap.isSticky();
-
- // Release non-modifiers and sticky modifiers
- if (!modifier || sticky)
- release();
-
- return false;
- };
-
- rowKey.onselectstart = function() { return false; };
-
- return rowKey;
-
- }
-
- function Gap(gap) {
-
- var keyboardGap = document.createElement("div");
- keyboardGap.className = "gap";
- keyboardGap.textContent = " ";
-
- var size = null;
- if (gap.attributes["size"])
- size = parseFloat(gap.attributes["size"].value);
-
- if (size) {
- keyboardGap.style.width = getKeySize(size);
- keyboardGap.style.height = getKeySize(size);
- }
-
- return keyboardGap;
-
- }
-
- function Row(row) {
-
- var keyboardRow = document.createElement("div");
- keyboardRow.className = "row";
-
- var children = row.childNodes;
- for (var j=0; j<children.length; j++) {
- var child = children[j];
-
- // <row> can contain <key> or <column>
- if (child.tagName == "key") {
- var key = new Key(child);
- keyboardRow.appendChild(key);
- allKeys.push(key);
- }
- else if (child.tagName == "gap") {
- var gap = new Gap(child);
- keyboardRow.appendChild(gap);
- }
- else if (child.tagName == "column") {
- var col = new Column(child);
- keyboardRow.appendChild(col);
- }
+ return row;
}
- return keyboardRow;
-
- }
-
- function Column(col) {
+ function parse_column(e) {
+
+ var col = document.createElement("div");
+ col.className = "guac-keyboard-column";
- var keyboardCol = document.createElement("div");
- keyboardCol.className = "col";
+ if (col.getAttribute("align"))
+ col.style.textAlign = col.getAttribute("align");
- var align = null;
- if (col.attributes["align"])
- align = col.attributes["align"].value;
+ // Columns can only contain rows
+ parseChildren(e, {
+ "row": function(e) {
+ col.appendChild(parse_row(e));
+ }
+ });
- var children = col.childNodes;
- for (var j=0; j<children.length; j++) {
- var child = children[j];
-
- // <column> can only contain <row>
- if (child.tagName == "row") {
- var row = new Row(child);
- keyboardCol.appendChild(row);
- }
+ return col;
}
- if (align)
- keyboardCol.style.textAlign = align;
-
- return keyboardCol;
-
- }
-
-
-
- // Create keyboard
- var keyboard = document.createElement("div");
- keyboard.className = "keyboard";
-
-
- // Retrieve keyboard XML
- var xmlhttprequest = new XMLHttpRequest();
- xmlhttprequest.open("GET", url, false);
- xmlhttprequest.send(null);
-
- var xml = xmlhttprequest.responseXML;
-
- if (xml) {
// Parse document
- var root = xml.documentElement;
- if (root) {
-
- var children = root.childNodes;
- for (var i=0; i<children.length; i++) {
- var child = children[i];
-
- // <keyboard> can contain <row> or <column>
- if (child.tagName == "row") {
- keyboard.appendChild(new Row(child));
- }
- else if (child.tagName == "column") {
- keyboard.appendChild(new Column(child));
- }
-
+ var keyboard_element = xml.documentElement;
+ if (keyboard_element.tagName != "keyboard")
+ throw new Error("Root element must be keyboard");
+
+ // Get attributes
+ if (!keyboard_element.getAttribute("size"))
+ throw new Error("size attribute is required for keyboard");
+
+ var keyboard_size = parseFloat(keyboard_element.getAttribute("size"));
+
+ parseChildren(keyboard_element, {
+
+ "row": function(e) {
+ keyboard.appendChild(parse_row(e));
+ },
+
+ "column": function(e) {
+ keyboard.appendChild(parse_column(e));
}
-
- }
+
+ });
}
- var keyPressedHandler = null;
- var keyReleasedHandler = null;
-
- keyboard.setKeyPressedHandler = function(kh) { keyPressedHandler = kh; };
- keyboard.setKeyReleasedHandler = function(kh) { keyReleasedHandler = kh; };
-
// Do not allow selection or mouse movement to propagate/register.
keyboard.onselectstart =
keyboard.onmousemove =
return false;
};
- return keyboard;
-}
+ /**
+ * Fired whenever the user presses a key on this Guacamole.OnScreenKeyboard.
+ *
+ * @event
+ * @param {Number} keysym The keysym of the key being pressed.
+ */
+ this.onkeydown = null;
+
+ /**
+ * Fired whenever the user releases a key on this Guacamole.OnScreenKeyboard.
+ *
+ * @event
+ * @param {Number} keysym The keysym of the key being released.
+ */
+ this.onkeyup = null;
+
+ /**
+ * Returns the element containing the entire on-screen keyboard.
+ * @returns {Element} The element containing the entire on-screen keyboard.
+ */
+ this.getElement = function() {
+ return keyboard;
+ };
+ /**
+ * Resizes all elements within this Guacamole.OnScreenKeyboard such that
+ * the width is close to but does not exceed the specified width. The
+ * height of the keyboard is determined based on the width.
+ *
+ * @param {Number} width The width to resize this Guacamole.OnScreenKeyboard
+ * to, in pixels.
+ */
+ this.resize = function(width) {
+
+ // Get pixel size of a unit
+ var unit = Math.floor(width * 10 / keyboard_size) / 10;
+
+ // Resize all scaled elements
+ for (var i=0; i<scaledElements.length; i++) {
+ var scaledElement = scaledElements[i];
+ scaledElement.scale(unit)
+ }
+
+ };
+
+};
+
+
+/**
+ * Basic representation of a single key of a keyboard. Each key has a set of
+ * caps associated with tuples of modifiers. The cap determins what happens
+ * when a key is pressed, while it is the state of modifier keys that determines
+ * what cap is in effect on any particular key.
+ *
+ * @constructor
+ */
+Guacamole.OnScreenKeyboard.Key = function() {
+
+ var key = this;
+
+ /**
+ * Whether this key is currently pressed.
+ */
+ this.pressed = false;
+
+ /**
+ * Width of the key, relative to the size of the keyboard.
+ */
+ this.size = 1;
+
+ /**
+ * An associative map of all caps by modifier.
+ */
+ this.caps = {};
+
+ /**
+ * Bit mask with all modifiers that affect this key set.
+ */
+ this.modifierMask = 0;
+
+ /**
+ * Given the bitwise OR of all active modifiers, returns the key cap
+ * which applies.
+ */
+ this.getCap = function(modifier) {
+ return key.caps[modifier & key.modifierMask];
+ };
+
+};
+
+/**
+ * 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.
+ *
+ * @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.
+ */
+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;
+
+ /**
+ * The keysym this cap sends when its associated key is pressed/released
+ */
+ this.keysym = keysym;
+
+ // Set modifier if provided
+ if (modifier) this.modifier = modifier;
+
+};