Source: main/webapp/modules/KeyEventInterpreter.js

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Guacamole = Guacamole || {};

/**
 * An object that will accept raw key events and produce a chronologically
 * ordered array of key event objects. These events can be obtained by
 * calling getEvents().
 *
 * @constructor
 * @param {number} [startTimestamp=0]
 *     The starting timestamp for the recording being intepreted. If provided,
 *     the timestamp of each intepreted event will be relative to this timestamp.
 *     If not provided, the raw recording timestamp will be used.
 */
Guacamole.KeyEventInterpreter = function KeyEventInterpreter(startTimestamp) {

    // Default to 0 seconds to keep the raw timestamps
    if (startTimestamp === undefined || startTimestamp === null)
        startTimestamp = 0;

    /**
     * A precursor array to the KNOWN_KEYS map. The objects contained within
     * will be constructed into full KeyDefinition objects.
     *
     * @constant
     * @private
     * @type {Object[]}
     */
    var _KNOWN_KEYS = [
        {keysym: 0xFE03, name: 'AltGr' },
        {keysym: 0xFF08, name: 'Backspace' },
        {keysym: 0xFF09, name: 'Tab' },
        {keysym: 0xFF0B, name: 'Clear' },
        {keysym: 0xFF0D, name: 'Return', value: "\n" },
        {keysym: 0xFF13, name: 'Pause' },
        {keysym: 0xFF14, name: 'Scroll' },
        {keysym: 0xFF15, name: 'SysReq' },
        {keysym: 0xFF1B, name: 'Escape' },
        {keysym: 0xFF50, name: 'Home' },
        {keysym: 0xFF51, name: 'Left' },
        {keysym: 0xFF52, name: 'Up' },
        {keysym: 0xFF53, name: 'Right' },
        {keysym: 0xFF54, name: 'Down' },
        {keysym: 0xFF55, name: 'Page Up' },
        {keysym: 0xFF56, name: 'Page Down' },
        {keysym: 0xFF57, name: 'End' },
        {keysym: 0xFF63, name: 'Insert' },
        {keysym: 0xFF65, name: 'Undo' },
        {keysym: 0xFF6A, name: 'Help' },
        {keysym: 0xFF7F, name: 'Num' },
        {keysym: 0xFF80, name: 'Space', value: " " },
        {keysym: 0xFF8D, name: 'Enter', value: "\n" },
        {keysym: 0xFF95, name: 'Home' },
        {keysym: 0xFF96, name: 'Left' },
        {keysym: 0xFF97, name: 'Up' },
        {keysym: 0xFF98, name: 'Right' },
        {keysym: 0xFF99, name: 'Down' },
        {keysym: 0xFF9A, name: 'Page Up' },
        {keysym: 0xFF9B, name: 'Page Down' },
        {keysym: 0xFF9C, name: 'End' },
        {keysym: 0xFF9E, name: 'Insert' },
        {keysym: 0xFFAA, name: '*', value: "*" },
        {keysym: 0xFFAB, name: '+', value: "+" },
        {keysym: 0xFFAD, name: '-', value: "-" },
        {keysym: 0xFFAE, name: '.', value: "." },
        {keysym: 0xFFAF, name: '/', value: "/" },
        {keysym: 0xFFB0, name: '0', value: "0" },
        {keysym: 0xFFB1, name: '1', value: "1" },
        {keysym: 0xFFB2, name: '2', value: "2" },
        {keysym: 0xFFB3, name: '3', value: "3" },
        {keysym: 0xFFB4, name: '4', value: "4" },
        {keysym: 0xFFB5, name: '5', value: "5" },
        {keysym: 0xFFB6, name: '6', value: "6" },
        {keysym: 0xFFB7, name: '7', value: "7" },
        {keysym: 0xFFB8, name: '8', value: "8" },
        {keysym: 0xFFB9, name: '9', value: "9" },
        {keysym: 0xFFBE, name: 'F1' },
        {keysym: 0xFFBF, name: 'F2' },
        {keysym: 0xFFC0, name: 'F3' },
        {keysym: 0xFFC1, name: 'F4' },
        {keysym: 0xFFC2, name: 'F5' },
        {keysym: 0xFFC3, name: 'F6' },
        {keysym: 0xFFC4, name: 'F7' },
        {keysym: 0xFFC5, name: 'F8' },
        {keysym: 0xFFC6, name: 'F9' },
        {keysym: 0xFFC7, name: 'F10' },
        {keysym: 0xFFC8, name: 'F11' },
        {keysym: 0xFFC9, name: 'F12' },
        {keysym: 0xFFCA, name: 'F13' },
        {keysym: 0xFFCB, name: 'F14' },
        {keysym: 0xFFCC, name: 'F15' },
        {keysym: 0xFFCD, name: 'F16' },
        {keysym: 0xFFCE, name: 'F17' },
        {keysym: 0xFFCF, name: 'F18' },
        {keysym: 0xFFD0, name: 'F19' },
        {keysym: 0xFFD1, name: 'F20' },
        {keysym: 0xFFD2, name: 'F21' },
        {keysym: 0xFFD3, name: 'F22' },
        {keysym: 0xFFD4, name: 'F23' },
        {keysym: 0xFFD5, name: 'F24' },
        {keysym: 0xFFE1, name: 'Shift' },
        {keysym: 0xFFE2, name: 'Shift' },
        {keysym: 0xFFE3, name: 'Ctrl' },
        {keysym: 0xFFE4, name: 'Ctrl' },
        {keysym: 0xFFE5, name: 'Caps' },
        {keysym: 0xFFE7, name: 'Meta' },
        {keysym: 0xFFE8, name: 'Meta' },
        {keysym: 0xFFE9, name: 'Alt' },
        {keysym: 0xFFEA, name: 'Alt' },
        {keysym: 0xFFEB, name: 'Super' },
        {keysym: 0xFFEC, name: 'Super' },
        {keysym: 0xFFED, name: 'Hyper' },
        {keysym: 0xFFEE, name: 'Hyper' },
        {keysym: 0xFFFF, name: 'Delete' }
    ];

    /**
     * All known keys, as a map of X11 keysym to KeyDefinition.
     *
     * @constant
     * @private
     * @type {Object.<String, KeyDefinition>}
     */
    var KNOWN_KEYS = {};
    _KNOWN_KEYS.forEach(function createKeyDefinitionMap(keyDefinition) {

        // Construct a map of keysym to KeyDefinition object
        KNOWN_KEYS[keyDefinition.keysym] = (
                new Guacamole.KeyEventInterpreter.KeyDefinition(keyDefinition));

    });

    /**
     * All key events parsed as of the most recent handleKeyEvent() invocation.
     *
     * @private
     * @type {!Guacamole.KeyEventInterpreter.KeyEvent[]}
     */
    var parsedEvents = [];

    /**
     * If the provided keysym corresponds to a valid UTF-8 character, return
     * a KeyDefinition for that keysym. Otherwise, return null.
     *
     * @private
     * @param {Number} keysym
     *     The keysym to produce a UTF-8 KeyDefinition for, if valid.
     *
     * @returns {Guacamole.KeyEventInterpreter.KeyDefinition}
     *     A KeyDefinition for the provided keysym, if it's a valid UTF-8
     *     keysym, or null otherwise.
     */
    function getUnicodeKeyDefinition(keysym) {

        // Translate only if keysym maps to Unicode
        if (keysym < 0x00 || (keysym > 0xFF && (keysym | 0xFFFF) != 0x0100FFFF))
            return null;

        // Convert to UTF8 string
        var codepoint = keysym & 0xFFFF;
        var name = String.fromCharCode(codepoint);

        // Create and return the definition
        return new Guacamole.KeyEventInterpreter.KeyDefinition({
                keysym: keysym, name: name, value: name});

    }

    /**
     * Return a KeyDefinition corresponding to the provided keysym.
     *
     * @private
     * @param {Number} keysym
     *     The keysym to return a KeyDefinition for.
     *
     * @returns {KeyDefinition}
     *     A KeyDefinition corresponding to the provided keysym.
     */
    function getKeyDefinitionByKeysym(keysym) {

        // If it's a known type, return the existing definition
        if (keysym in KNOWN_KEYS)
            return KNOWN_KEYS[keysym];

        // Return a UTF-8 KeyDefinition, if valid
        var definition = getUnicodeKeyDefinition(keysym);
        if (definition != null)
            return definition;

        // If it's not UTF-8, return an unknown definition, with the name
        // just set to the hex value of the keysym
        return new Guacamole.KeyEventInterpreter.KeyDefinition({
            keysym: keysym,
            name: '0x' + String(keysym.toString(16))
        })

    }

    /**
     * Handles a raw key event, appending a new key event object for every
     * handled raw event.
     *
     * @param {!string[]} args
     *     The arguments of the key event.
     */
    this.handleKeyEvent = function handleKeyEvent(args) {

        // The X11 keysym
        var keysym = parseInt(args[0]);

        // Either 1 or 0 for pressed or released, respectively
        var pressed = parseInt(args[1]);

        // The timestamp when this key event occured
        var timestamp = parseInt(args[2]);

        // The timestamp relative to the provided initial timestamp
        var relativeTimestap = timestamp - startTimestamp;

        // Known information about the parsed key
        var definition = getKeyDefinitionByKeysym(keysym);

        // Push the latest parsed event into the list
        parsedEvents.push(new Guacamole.KeyEventInterpreter.KeyEvent({
            definition: definition,
            pressed: pressed,
            timestamp: relativeTimestap
        }));

    };

    /**
     * Return the current batch of typed text. Note that the batch may be
     * incomplete, as more key events might be processed before the next
     * batch starts.
     *
     * @returns {Guacamole.KeyEventInterpreter.KeyEvent[]}
     *     The current batch of text.
     */
    this.getEvents = function getEvents() {
        return parsedEvents;
    };

};

/**
 * A definition for a known key.
 *
 * @constructor
 * @param {Guacamole.KeyEventInterpreter.KeyDefinition|object} [template={}]
 *     The object whose properties should be copied within the new
 *     KeyDefinition.
 */
Guacamole.KeyEventInterpreter.KeyDefinition = function KeyDefinition(template) {

    // Use empty object by default
    template = template || {};

    /**
     * The X11 keysym of the key.
     * @type {!number}
     */
    this.keysym = parseInt(template.keysym);

    /**
     * A human-readable name for the key.
     * @type {!String}
     */
    this.name = template.name;

    /**
     * The value which would be typed in a typical text editor, if any. If the
     * key is not associated with any typeable value, this will be undefined.
     * @type {String}
     */
    this.value = template.value;

};

/**
 * A granular description of an extracted key event, including a human-readable
 * text representation of the event, whether the event is directly typed or not,
 * and the timestamp when the event occured.
 *
 * @constructor
 * @param {Guacamole.KeyEventInterpreter.KeyEvent|object} [template={}]
 *     The object whose properties should be copied within the new
 *     KeyEvent.
 */
Guacamole.KeyEventInterpreter.KeyEvent = function KeyEvent(template) {

    // Use empty object by default
    template = template || {};

    /**
     * The key definition for the pressed key.
     *
     * @type {!Guacamole.KeyEventInterpreter.KeyDefinition}
     */
    this.definition = template.definition;

    /**
     * True if the key was pressed to create this event, or false if it was
     * released.
     *
     * @type {!boolean}
     */
    this.pressed = !!template.pressed;

    /**
     * The timestamp from the recording when this event occured.
     *
     * @type {!Number}
     */
    this.timestamp = template.timestamp;

};