* for the specific language governing rights and limitations under the
* License.
*
- * The Original Code is guacamole-common-js.
+ * The Original Code is guac-common-js.
*
* The Initial Developer of the Original Code is
* Michael Jumper.
*/
Guacamole.OnScreenKeyboard = function(url) {
- var allKeys = new Array();
- var modifierState = new function() {};
+ var on_screen_keyboard = this;
- function getKeySize(size) {
- return (5*size) + "ex";
- }
+ var scaledElements = [];
+
+ var modifiers = {};
+ var currentModifier = 1;
- function getCapSize(size) {
- return (5*size - 0.5) + "ex";
- }
+ // Function for adding a class to an element
+ var addClass;
- function clearModifiers() {
+ // Function for removing a class from an element
+ var removeClass;
- // Send key release events for all pressed modifiers
- for (var k=0; k<allKeys.length; k++) {
+ // If Node.classList is supported, implement addClass/removeClass using that
+ if (Node.classList) {
- var key = allKeys[k];
- var cap = key.getCap();
- var modifier = cap.getModifier();
+ addClass = function(element, classname) {
+ element.classList.add(classname);
+ };
+
+ removeClass = function(element, classname) {
+ element.classList.remove(classname);
+ };
+
+ }
- if (modifier && isModifierActive(modifier) && !cap.isSticky() && key.isPressed())
- key.release();
+ // Otherwise, implement own
+ else {
- }
+ addClass = function(element, classname) {
- }
+ // Simply add new class
+ element.className += " " + classname;
- function setModifierReleased(modifier) {
- if (isModifierActive(modifier))
- modifierState[modifier]--;
- }
+ };
+
+ removeClass = function(element, classname) {
- function setModifierPressed(modifier) {
- if (modifierState[modifier] == null)
- modifierState[modifier] = 1;
- else
- modifierState[modifier]++;
- }
+ // Filter out classes with given name
+ element.className = element.className.replace(/([^ ]+)[ ]*/g,
+ function(match, testClassname, spaces, offset, string) {
- function isModifierActive(modifier) {
- if (modifierState[modifier] > 0)
- return true;
+ // If same class, remove
+ if (testClassname == classname)
+ return "";
- return false;
- }
+ // Otherwise, allow
+ return match;
+
+ }
+ );
- function toggleModifierPressed(modifier) {
- if (isModifierActive(modifier))
- setModifierReleased(modifier);
- else
- setModifierPressed(modifier);
+ };
+
}
- function refreshAllKeysState() {
- for (var k=0; k<allKeys.length; k++)
- allKeys[k].refreshState();
- }
+ // 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) {
+
+ // 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(",");
+ }
+ // For each child of element, call handler defined in next
+ function parseChildren(element, next) {
- // Modifier represented by this keycap
- var modifier = null;
- if (cap.attributes["modifier"])
- modifier = cap.attributes["modifier"].value;
-
+ var children = element.childNodes;
+ for (var i=0; i<children.length; i++) {
- // 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;
+ // Get child node
+ var child = children[i];
- this.getDisplayText = function() {
- return cap.textContent;
- };
+ // Do not parse text nodes
+ if (!child.tagName)
+ continue;
- this.getKeySym = function() {
- return keysym;
- };
+ // Get handler for node
+ var handler = next[child.tagName];
- this.getRequiredModifiers = function() {
- return reqMod;
- };
+ // Call handler if defined
+ if (handler)
+ handler(child);
- this.getModifier = function() {
- return modifier;
- };
-
- 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;
}
- }
-
- }
-
- if (matches)
- currentCap = keycap;
-
- }
+ });
- rowKey.className = "key";
+ scaledElements.push(new ScaledElement(key_container_element, key_units, 1, true));
+ row.appendChild(key_container_element);
- if (currentCap.getModifier())
- rowKey.className += " modifier";
+ // Set up click handler for key
+ function press(e) {
- if (currentCap.isSticky())
- rowKey.className += " sticky";
+ // Press key if not yet pressed
+ if (!key.pressed) {
- if (isModifierActive(currentCap.getModifier()))
- rowKey.className += " active";
+ addClass(key_element, "guac-keyboard-pressed");
- if (state == STATE_PRESSED)
- rowKey.className += " pressed";
+ // Get current cap based on modifier state
+ var cap = key.getCap(on_screen_keyboard.modifiers);
- keyCap.textContent = currentCap.getDisplayText();
- }
- rowKey.refreshState = refreshState;
+ // Update modifier state
+ if (cap.modifier) {
- 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
+ on_screen_keyboard.modifiers ^= modifierFlag;
- // Set size
- if (size) {
- rowKey.style.width = getKeySize(size);
- keyCap.style.width = getCapSize(size);
- }
+ // Activate modifier if pressed
+ if (on_screen_keyboard.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(on_screen_keyboard.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();
+ return row;
- 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) {
+ function parse_column(e) {
+
+ var col = document.createElement("div");
+ col.className = "guac-keyboard-column";
- var keyboardRow = document.createElement("div");
- keyboardRow.className = "row";
+ if (col.getAttribute("align"))
+ col.style.textAlign = col.getAttribute("align");
- var children = row.childNodes;
- for (var j=0; j<children.length; j++) {
- var child = children[j];
+ // Columns can only contain rows
+ parseChildren(e, {
+ "row": function(e) {
+ col.appendChild(parse_row(e));
+ }
+ });
- // <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 col;
}
- return keyboardRow;
-
- }
-
- function Column(col) {
-
- var keyboardCol = document.createElement("div");
- keyboardCol.className = "col";
- var align = null;
- if (col.attributes["align"])
- align = col.attributes["align"].value;
-
- 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);
+ // Parse document
+ 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));
}
-
- }
-
- if (align)
- keyboardCol.style.textAlign = align;
-
- return keyboardCol;
+
+ });
}
+ // Do not allow selection or mouse movement to propagate/register.
+ keyboard.onselectstart =
+ keyboard.onmousemove =
+ keyboard.onmouseup =
+ keyboard.onmousedown =
+ function(e) {
+ e.stopPropagation();
+ return false;
+ };
+ /**
+ * State of all modifiers.
+ */
+ this.modifiers = 0;
- // Create keyboard
- var keyboard = document.createElement("div");
- keyboard.className = "keyboard";
+ this.onkeydown = null;
+ this.onkeyup = null;
+ this.getElement = function() {
+ return keyboard;
+ };
- // Retrieve keyboard XML
- var xmlhttprequest = new XMLHttpRequest();
- xmlhttprequest.open("GET", url, false);
- xmlhttprequest.send(null);
+ this.resize = function(width) {
- var xml = xmlhttprequest.responseXML;
+ // Get pixel size of a unit
+ var unit = Math.floor(width * 10 / keyboard_size) / 10;
- if (xml) {
+ // Resize all scaled elements
+ for (var i=0; i<scaledElements.length; i++) {
+ var scaledElement = scaledElements[i];
+ scaledElement.scale(unit)
+ }
- // 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));
- }
+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;
- var keyPressedHandler = null;
- var keyReleasedHandler = null;
+ /**
+ * An associative map of all caps by modifier.
+ */
+ this.caps = {};
- keyboard.setKeyPressedHandler = function(kh) { keyPressedHandler = kh; };
- keyboard.setKeyReleasedHandler = function(kh) { keyReleasedHandler = kh; };
+ /**
+ * Bit mask with all modifiers that affect this key set.
+ */
+ this.modifierMask = 0;
- // Do not allow selection or mouse movement to propagate/register.
- keyboard.onselectstart =
- keyboard.onmousemove =
- keyboard.onmouseup =
- keyboard.onmousedown =
- function(e) {
- e.stopPropagation();
- return false;
+ /**
+ * Given the bitwise OR of all active modifiers, returns the key cap
+ * which applies.
+ */
+ this.getCap = function(modifier) {
+ return key.caps[modifier & key.modifierMask];
};
- return keyboard;
-
-};
-
+}
+
+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;
+
+}