Source: core/event/key-manager.js

/**
 * @module montage/core/event/key-manager
 * @requires montage/core/core
 */

var Montage = require("../core").Montage,
    defaultEventManager = require("./event-manager").defaultEventManager,
    MutableEvent = require("./mutable-event").MutableEvent;

var KEYNAMES_TO_KEYCODES = {
    // W3C Key Code
    backspace:              8,
    tab:                    9,
    enter:                  13,
    shift:                  16,
    control:                17,
    alt:                    18,
    capslock:               20,
    escape:                 27,
    space:                  32,
    pageup:                 33,
    pagedown:               34,
    end:                    35,
    home:                   36,
    left:                   37,
    up:                     38,
    right:                  39,
    down:                   40,
    delete:                 46,

    //https://www.w3.org/TR/uievents-key/
    arrowup:                38,
    arrowright:             39,
    arrowdown:              40,
    arrowdelete:            46,

    // W3C Optional Key Code (mostly for US keyboard layout)
    semicolon:              186,
    colon:                  186,
    equal:                  187,
    plus:                   187,
    comma:                  188,
    less:                   188,
    minus:                  189,
    underscore:             189,
    period:                 190,
    greater:                190,
    slash:                  191,
    questionmark:           191,
    backtick:               192,
    tilde:                  192,
    openingsquarebracket:   219,
    openingcurlybracket:    219,
    backslash:              220,
    pipe:                   220,
    closingsquarebracket:   221,
    closingcurlybracket:    221,
    singlequote:            222,
    doublequote:            222,


    // Non standard Virtal key code but commonly used
    clear:                  12,
    meta:                   91,
    contextmenu:            93,
    numpad0:                96,
    numpad1:                97,
    numpad2:                98,
    numpad3:                99,
    numpad4:                100,
    numpad5:                101,
    numpad6:                102,
    numpad7:                103,
    numpad8:                104,
    numpad9:                105,
    multiply:               106,
    add:                    107,
    subtract:               109,
    decimal:                110,
    divide:                 111,
    f1:                     112,
    f2:                     113,
    f3:                     114,
    f4:                     115,
    f5:                     116,
    f6:                     117,
    f7:                     118,
    f8:                     119,
    f9:                     120,
    f10:                    121,
    f11:                    122,
    f12:                    123,
    f13:                    124,
    f14:                    125,
    f15:                    126,
    f16:                    127,
    f17:                    128,
    f18:                    129,
    f19:                    130,
    f20:                    131,
    f21:                    132,
    f22:                    133,
    f23:                    134,
    f24:                    135
},

KEYCODES_TO_KEYNAMES = null,    // Will be build from the KEYNAMES_TO_KEYCODES dictionary

OPERAMAC_KEYNAMES_TO_KEYCODES = {
    meta:                   17,
    control:                57392,
    f17:                    63252,
    f18:                    63253,
    f19:                    63254,
    f20:                    63255,
    f21:                    63256,
    f22:                    63257,
    f23:                    63258,
    f24:                    63259
},

FIREFOX_KEYNAMES_TO_KEYCODES = {
    f13:                    44,
    f14:                    145,
    f15:                    19,
    f16:                    63251,
    f17:                    63252,
    f18:                    63253,
    f19:                    63254,
    f20:                    63255,
    f21:                    63256,
    f22:                    63257,
    f23:                    63258,
    f24:                    63259,
    meta:                   224
},

KEYNAMES_ALIASES = {
    cmd:        "command",
    ctl:        "control",
    ctrl:       "control",
    win:        "meta",
    windows:    "meta",
    opt:        "alt",
    option:     "alt"
},

MODIFIERS = {
    meta:       {name:"meta",    value:1},
    alt:        {name:"alt",     value:2},
    control:    {name:"control", value:4},
    shift:      {name:"shift",   value:8}
},

NORMALIZED_KEYS = {
    semicolon:              ";",
    colon:                  ":",
    equal:                  "=",
    plus:                   "+",
    comma:                  ",",
    less:                   "<",
    minus:                  "-",
    underscore:             "_",
    period:                 ".",
    greater:                ">",
    slash:                  "/",
    questionmark:           "?",
    backtick:               "`",
    tilde:                  "~",
    openingsquarebracket:   "[",
    openingcurlybracket:    "{",
    backslash:              "\\",
    pipe:                   "|",
    closingsquarebracket:   "]",
    closingcurlybracket:    "}",
    singlequote:            "'",
    doublequote:            "\"",
    multiply:               "*",
    add:                    "+",
    subtract:               "-",
    decimal:                ".",
    divide:                 "/"
},

NORMALIZED_CHARS = null;    // Will be generated from the NORMALIZED_KEYS


/* Event type dispatched by KeyComposer */
var KEYPRESS_EVENT_TYPE = "keyPress",
    LONGKEYPRESS_EVENT_TYPE = "longKeyPress",
    KEYRELEASE_EVENT_TYPE = "keyRelease";

var defaultKeyManager = null;

/**
 * The KeyManager dispatches KeyComposer events when it detects a keyComposer
 * has been pressed or released.  Do not create a KeyManager directly but
 * instead require for the defaultKeyManager:
 *
 * ```js
 * require("./key-manager").defaultKeyManager
 * ```
 *
 * @class KeyManager
 * @classdesc Dispatches events to a key composer. There can only be one.
 * @extends Montage
*/
var KeyManager = exports.KeyManager = Montage.specialize(/** @lends KeyManager# */ {

    /**
      @private
    */
    _keyEventsListenerInstalled: {
        value: false
    },

    /**
      @private
    */
    _composerKeyMap: {
        value: {}
    },

    /**
      @private
    */
    _triggeredKeys: {
        value: {}
    },

    /**
      @private
    */
    _longPressKeys: {
        value: {}
    },

    /**
      @private
    */
    _cleanupTimeout: {
        value: null
    },

    /**
      @private
    */
    _cleanupThreshold: {
        value: 2000
    },

    /**
      @private
    */
    _longPressThreshold: {
        value: 1000
    },

    /**
     * The number of milliseconds a key must be pressed in order to dispatch a
     * longKeyPress event.
     * @type {number}
     * @default 1000
     */
    longPressThreshold: {
        get: function () {
            return this._longPressThreshold;
        },
        set: function (value) {
            if (value > 0 && value !== this._longPressThreshold) {
                this._longPressThreshold = value;
                if (this._longPressThreshold > this._cleanupThreshold - 100) {
                    this._cleanupThreshold = this._longPressThreshold + 100;
                } else {
                    this._cleanupThreshold = 2000;
                }
            }
        }
    },

    /**
     * Register a composerKey.
     * @function
     * @param {Object} keyComposer. The key to register.
     */
    registerKey: {
        value: function (keyComposer) {
                // validates the keys:
            var normalizedKeys = this._normalizeKeySequence(keyComposer.keys),
                modifiersAndKey,
                map = this._composerKeyMap,
                mapModifiersEntry,
                mapKeyEntry,
                keyAlreadyRegistered = false,
                entry,
                i;

            if (normalizedKeys) {
                modifiersAndKey = this._convertKeysToModifiersAndKeyCode(normalizedKeys);

                mapModifiersEntry = map[modifiersAndKey.modifiers];
                if (!mapModifiersEntry) {
                    mapModifiersEntry = map[modifiersAndKey.modifiers] = {};
                }
                mapKeyEntry = mapModifiersEntry[modifiersAndKey.keyCode];
                if (mapKeyEntry) {
                    for (i in mapKeyEntry) {
                        if (mapKeyEntry.hasOwnProperty(i)) {
                            entry = mapKeyEntry[i];
                            if (entry.object === keyComposer) {
                                entry.refCount ++;
                                keyAlreadyRegistered = true;
                                break;
                            }
                        }
                    }
                    if (!keyAlreadyRegistered) {
                        mapKeyEntry.push({object: keyComposer, refCount: 1});
                    }
                } else {
                    mapModifiersEntry[modifiersAndKey.keyCode] = [{object: keyComposer, refCount: 1}];
                }

                keyComposer._modifiers = modifiersAndKey.modifiers;
                keyComposer._keyCode = modifiersAndKey.keyCode;

                this._registerListeners();
            }
        }
    },

    /**
     * Unregister a composerKey. if a key has been registered multiple time,
     * unregister must be called the same amount of time before the key is
     * actually unregistered.
     * @function
     * @param {Object} keyComposer The key to unregister.
     */
    unregisterKey: {
        value: function (keyComposer) {
            var map = this._composerKeyMap,
                mapModifiersEntry,
                mapKeyEntry,
                entry,
                i;

            mapModifiersEntry = map[keyComposer._modifiers];
            if (mapModifiersEntry) {
                mapKeyEntry = mapModifiersEntry[keyComposer._keyCode];
                for (i in mapKeyEntry) {
                    if (mapKeyEntry.hasOwnProperty(i)) {
                        entry = mapKeyEntry[i];
                        if (entry.object === keyComposer) {
                            entry.refCount --;
                            if (entry.refCount <= 0) {
                                mapKeyEntry.splice(i, 1);
                                if (mapKeyEntry.length === 0) {
                                    delete mapModifiersEntry[keyComposer._keyCode];
                                    if (Object.keys(mapModifiersEntry).length === 0) {
                                        delete map[keyComposer._modifiers];
                                        if (Object.keys(map).length === 0) {
                                            this._unregisterListeners();
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    },

    constructor: {
        value: function () {
            var userAgent = global.navigator ? global.navigator.userAgent : "",
                code;

            if (defaultKeyManager) {
                console.warn("Rather than creating a new KeyManager object, you might want to use the default key manager");
            }

            // Browser detection
            if (userAgent.match(/ firefox\//i)) {
                this._firefox = true;
            } else if (userAgent.match(/ appleWebkit\//i)) {
                this._webkit = true;
                if (userAgent.match(/ chrome\//i)) {
                    this._chrome = true;
                }
            } else if (userAgent.match(/^opera\//i)) {
                this._opera = true;
            }

            // Platform Detection
            if (userAgent.match(/macintosh/i)) {
                this._mac = true;
                if (this._opera) {
                    this._operaMac = true;
                }
            }

            // Handle the platform specific COMMAND modifier
            if (this._mac) {
                MODIFIERS.command = MODIFIERS.meta;
            } else {
                MODIFIERS.command = MODIFIERS.control;
            }

            // Patch the KeyCode dictionary for Opera or Firefox
            if (this._operaMac) {
                for (code in OPERAMAC_KEYNAMES_TO_KEYCODES) {
                    if (OPERAMAC_KEYNAMES_TO_KEYCODES.hasOwnProperty(code)) {
                        KEYNAMES_TO_KEYCODES[code] = OPERAMAC_KEYNAMES_TO_KEYCODES[code];   
                    }
                }
            }
            if (this._firefox) {
                for (code in FIREFOX_KEYNAMES_TO_KEYCODES) {
                    if (FIREFOX_KEYNAMES_TO_KEYCODES.hasOwnProperty(code)) {
                        KEYNAMES_TO_KEYCODES[code] = FIREFOX_KEYNAMES_TO_KEYCODES[code];   
                    }
                }
            }

            var keyName;

            // Generate the KEYNAMES dictionary
            KEYCODES_TO_KEYNAMES = {};
            for (keyName in KEYNAMES_TO_KEYCODES) {
                if (KEYNAMES_TO_KEYCODES.hasOwnProperty(keyName)) {
                    code = KEYNAMES_TO_KEYCODES[keyName];
                    if (KEYCODES_TO_KEYNAMES[code] === undefined) {
                        // If we have more than one name for a keycode, use only the first one
                        KEYCODES_TO_KEYNAMES[code] = keyName;
                    }   
                }
            }

            // Generate the NORMALIZED_CHARS dictionary
            NORMALIZED_CHARS = {};
            for (keyName in NORMALIZED_KEYS) {
                if (NORMALIZED_KEYS.hasOwnProperty(keyName)) {
                    code = NORMALIZED_KEYS[keyName];
                    if (NORMALIZED_CHARS[code] === undefined) {
                        // If we have more than one name for a char, use only the first one
                        NORMALIZED_CHARS[code] = keyName;
                    }
                }
            }

            this._shiftKey = false;
            this._altKey = false;
            this._metaKey = false;
            this._ctrlKey = false;
        }
    },

    captureKeydown: {
        value: function (event) {
            var keyCode,
                identifierCode,
                keyIdentifier = event.key || event.keyIdentifier,
                submap,
                stopped = false;

            // console.log("  captureKeydown:", event.keyCode, event.charCode, event.keyIdentifier);

            this._preprocessKeyEvent(event);
            // console.log("KEYDOWN MODIFIERS:", this._modifiers, "KEYCODE:", this._keyCode);

            submap = this._submap;
            if (submap) {
                keyCode = this._keyCode;

                // Check the keyCode for a match...
                if (keyCode && submap[keyCode]) {
                    stopped = this._dispatchComposerKeyMatches(submap[keyCode], event);
                }

                // Check the keyIdentifier for a match
                if (!stopped && keyIdentifier) {
                    identifierCode = KEYNAMES_TO_KEYCODES[keyIdentifier.toLowerCase()] ||
                        this._decodeKeyIdentifier(keyIdentifier);
                    if (identifierCode && identifierCode !== keyCode && submap[identifierCode]) {
                        this._dispatchComposerKeyMatches(submap[identifierCode], event);
                    }
                }
            }

            this._setupCleanupTimer();
        }
    },

    captureKeypress: {
        value: function (event) {
            var charCode = event.charCode,
                keyIdentifier = event.key || event.keyIdentifier,
                keyCode,
                identifierCode,
                submap,
                stopped = false;

            // console.log("  captureKeypress:", event.keyCode, event.charCode, event.keyIdentifier, this._modifiers);
            this._preprocessKeyEvent(event);

            submap = this._submap;
            // console.log("KEYPRESS MODIFIERS:", this._modifiers, this._keyCode, event.charCode);

            if (submap) {
                keyCode = this._keyCode;

                // Check the keyCode for a match...
                if (keyCode && submap[keyCode]) {
                    stopped = this._dispatchComposerKeyMatches(submap[keyCode], event);
                }

                // Check the charCode for a match...
                if (!stopped && charCode && charCode !== keyCode && submap[charCode]) {
                    stopped = this._dispatchComposerKeyMatches(submap[charCode], event);
                }

                // Check the keyIdentifier for a match
                if (!stopped && keyIdentifier) {
                    identifierCode = KEYNAMES_TO_KEYCODES[keyIdentifier.toLowerCase()] ||
                        this._decodeKeyIdentifier(keyIdentifier);
                    if (identifierCode && identifierCode !== keyCode && submap[identifierCode]) {
                        this._dispatchComposerKeyMatches(submap[identifierCode], event);
                    }
                }
            }

            this._setupCleanupTimer();
        }
    },

    captureKeyup: {
        value: function (event) {
            var keyCode = event.keyCode,
                keyIdentifier = event.key || event.keyIdentifier,
                identifierCode,
                submap,
                dispatchedKeyCode = 0,
                triggeredKey,
                uuid,
                stopped = false;

            // Dispatch a keyup event for all composerKey triggered during the keydown/keypress phase
            // and for the current match

            // console.log("  captureKeyup:", event.keyCode, event.charCode, event.keyIdentifier);
            this._preprocessKeyEvent(event);
            submap = this._submap;

            // console.log("KEYUP MODIFIERS:", this._modifiers, this._keyCode);
            if (submap) {
                keyCode = this._keyCode;

                // Check the keyCode for a match...
                if (keyCode && submap[keyCode]) {
                    stopped = this._dispatchComposerKeyMatches(submap[keyCode], event);
                    dispatchedKeyCode = keyCode;
                }

                // Check the keyIdentifier for a match
                if (!stopped && keyIdentifier) {
                    identifierCode = KEYNAMES_TO_KEYCODES[keyIdentifier.toLowerCase()] ||
                        this._decodeKeyIdentifier(keyIdentifier);
                    if (identifierCode && identifierCode !== dispatchedKeyCode && submap[identifierCode]) {
                        stopped = this._dispatchComposerKeyMatches(submap[identifierCode], event);
                    }
                }
            }

            // In case the user release the modifier key before releasing the main key, we still need to fire a keyup event
            if (!stopped) {
                for (uuid in this._triggeredKeys) {
                    if (this._triggeredKeys.hasOwnProperty(uuid)) {
                        triggeredKey = this._triggeredKeys[uuid];
                        if (triggeredKey._keyCode === keyCode || triggeredKey._keyCode === identifierCode) {
                            this._dispatchComposerKeyMatches([triggeredKey], event);
                        }
                    }
                }
            }

            this._cleanup();
        }
    },

    _normalizeKeySequence: {
        value: function (keySequence) {
            var modifiersOrder = [MODIFIERS.meta.name, MODIFIERS.alt.name, MODIFIERS.control.name, MODIFIERS.shift.name],
                keys = keySequence.toLowerCase().replace(/ /g, "").replace(/\+\+/g, "+add").split("+"),
                nbrKeys = keys.length,
                key,
                normalizedKeys = [],
                i;

            for (i = 0; i < nbrKeys - 1; i ++) {
                // convert alias
                key = keys[i];
                key = KEYNAMES_ALIASES[key] || key;

                // make sure it's a modifier
                key = MODIFIERS[key];
                if (key) {
                    normalizedKeys.push(key.name);
                } else {
                    console.warn("Invalid key sequence (modifier):", keySequence);
                    return null;
                }
            }

            normalizedKeys.sort(function (a, b) {
                return modifiersOrder.indexOf(a) - modifiersOrder.indexOf(b);
            });

            key = keys[nbrKeys - 1];
            if (key.length > 1 && !KEYNAMES_TO_KEYCODES[key]) {
                console.warn("Invalid key sequence (key):", keySequence, key);
                return null;
            }

            if (normalizedKeys.length) {
                return normalizedKeys.join("+") + "+" + key;
            } else {
                return key;
            }
        }
    },

    _preprocessKeyEvent: {
        value: function (event) {
            var thisRef = this,
                eventType = event.type,
                keyCode = event.keyCode,
                keyIdentifier = event.key || event.keyIdentifier,
                modifiers,
                value;

            if (this._operaMac) {
                if (this._operaModifierTimeout) {
                    clearTimeout(this._operaModifierTimeout);
                    this._operaModifierTimeout = null;
                }
            }

            if (eventType === "keydown" || eventType === "keyup") {
                if (this._operaMac) {
                    // Opera Mac is not very good at keeping track of which modifiers are pressed or not!
                    value = (eventType === "keydown");
                    if (keyCode === KEYNAMES_TO_KEYCODES.shift) {
                        this._shiftKey = value;
                    } else if (keyCode === KEYNAMES_TO_KEYCODES.alt) {
                        this._altKey = value;
                    } else if (keyCode === KEYNAMES_TO_KEYCODES.meta) {
                        if (this._mac) {
                            this._metaKey = value;
                        }
                    } else if (keyCode === KEYNAMES_TO_KEYCODES.control) {
                        this._ctrlKey = value;
                    }

                    if (eventType === "keyup") {
                        // Setup a timeout to force reset the modifier state ~3 seconds after the last key up
                        // This is to recover when we miss a keyup event which seems to occurs once in a while with Opera
                        this._operaModifierTimeout = setTimeout(function (){
                            thisRef._shiftKey = false;
                            thisRef._altKey = false;
                            thisRef._metaKey = false;
                            thisRef._ctrlKey = false;
                            thisRef._operaModifierTimeout = null;
                        }, this._cleanupThreshold + 1000);
                    }
                } else {
                    this._shiftKey = event.shiftKey;
                    this._altKey = event.altKey;
                    this._metaKey = event.metaKey;
                    this._ctrlKey = event.ctrlKey;
                }
            }

            if (this._mac && this._webkit && keyCode === KEYNAMES_TO_KEYCODES.contextmenu) {
                // Webkit browsers will interpret the right command key as the window context-menu but will keep the
                // meta modifier on preventing us from having a "context-menu" shortcut. We need to clear the meta flag
                // (the limitation is that we wont be able to support a "META+CONTEXT-MENU" shortcut
                this._metaKey = false;
            }

            if (this._chrome) {
                // Chrome (at least on Mac) generate the same keycode for the NumKeyPad = and NumKeyPad +
                if (!this._shiftKey && keyCode === KEYNAMES_TO_KEYCODES.plus && (keyIdentifier === "U+002B" || keyIdentifier === "+")) {
                    event.keyCode = KEYNAMES_TO_KEYCODES.add;
                }
            }

            this._modifiers = modifiers = (this._shiftKey ? MODIFIERS.shift.value : 0) +
                (this._altKey ? MODIFIERS.alt.value : 0) +
                (this._metaKey ? MODIFIERS.meta.value : 0) +
                (this._ctrlKey ? MODIFIERS.control.value : 0);

            this._submap = this._composerKeyMap[modifiers];
            this._keyCode = event.keyCode;
            this._keyIdentifier = keyIdentifier;
        }
    },

    _registerListeners: {
        value: function () {
            if (!this._keyEventsListenerInstalled) {
                window.addEventListener("keydown", this, true);
                window.addEventListener("keypress", this, true);
                window.addEventListener("keyup", this, true);
                this._keyEventsListenerInstalled = true;
            }
        }
    },

    _unregisterListeners: {
        value: function () {
            if (this._keyEventsListenerInstalled) {
                window.removeEventListener("keydown", this, true);
                window.removeEventListener("keypress", this, true);
                window.removeEventListener("keyup", this, true);
                this._keyEventsListenerInstalled = false;
            }
        }
    },

    _dispatchComposerKeyMatches: {
        value: function (matches, event) {
            var thisRef = this,
                stopped = false,
                keyUp = event.type === "keyup",
                keyDown = event.type === "keydown",
                eventType = keyUp ? KEYRELEASE_EVENT_TYPE : KEYPRESS_EVENT_TYPE,
                nbrMatches = matches.length,
                keyComposer,
                keyComposerEvent,
                triggeredKeys,
                i;

            // Slice the array so that keys registered or unregistered during
            // dispatch don't break the dispatching
            matches = matches.slice();

            // matches could be either an array of matches or an array of keyComposers
            for (i = 0; i < nbrMatches && !stopped; i ++) {
                keyComposer = matches[i].object || matches[i];

                // Make sure keyboard event's target is a descendant of the keyComposer's element
                var target = event.target,
                    element = keyComposer.element,
                    onTarget = (keyComposer.element === window);

                while (!onTarget) {
                    onTarget = (target === element);

                    if (target === document) {
                        break;
                    } else {
                        target = target.parentElement;
                        if (!target) {
                            target = document;
                        }
                    }
                }

                // Most components can't receive key events directly: the events target the window,
                // but we should also fire them on composers of the activeTarget component
                if (!onTarget && defaultEventManager.activeTarget !== keyComposer.component) {
                    continue;
                }

                if (keyUp) {
                    triggeredKeys = Object.keys(this._triggeredKeys);
                    if (triggeredKeys.indexOf(keyComposer.uuid) === -1) {
                        // Do not generate a keyup event if the composerKey has not been triggered on keydown or keypress
                        continue;
                    }

                    if (keyComposer._longPressTimeout) {
                        clearTimeout(keyComposer._longPressTimeout);
                        keyComposer._longPressTimeout = null;
                        delete this._longPressKeys[keyComposer.uuid];
                    }
                } else {
                    if (keyDown) {
                        // Reset trigger
                        delete this._triggeredKeys[keyComposer.uuid];
                    } else if (this._triggeredKeys[keyComposer.uuid]) {
                        // that key has already been triggered, let's ignore it...
                        continue;
                    }

                    if (keyComposer._shouldDispatchLongPress && !keyComposer._longPressTimeout) {

                        /* jshint loopfunc:true */
                        keyComposer._longPressTimeout = setTimeout(function () {
                            var longPressEvent = MutableEvent.fromEvent(longPressEvent);
                            longPressEvent.type = LONGKEYPRESS_EVENT_TYPE;
                            longPressEvent.activeElement = event.target;
                            longPressEvent.identifier = keyComposer.identifier;

                            keyComposer._longPressTimeout = null;
                            keyComposer.dispatchEvent(longPressEvent);
                            delete thisRef._longPressKeys[keyComposer.uuid];
                        }, this._longPressThreshold);   
                        /* jshint loopfunc:false */

                        // Let's remember any longKeyPress key waiting for timeout
                        this._longPressKeys[keyComposer.uuid] = keyComposer;
                    }
                }
                
                keyComposerEvent = MutableEvent.fromEvent(event);
                keyComposerEvent.type = eventType;
                keyComposerEvent.activeElement = event.target;
                keyComposerEvent.identifier = keyComposer.identifier;
                keyComposerEvent.keyComposer = keyComposer;

                keyComposer.dispatchEvent(keyComposerEvent);

                if (keyComposerEvent.propagationStopped) {
                    stopped = true;
                }

                if (keyUp) {
                    // We already dispatched a keyup event for this key, let's remove it
                    delete this._triggeredKeys[keyComposer.uuid];
                } else {
                    // We need to remember any composerKey triggered during the keydown phase in order to generate an equivalent keyup
                    this._triggeredKeys[keyComposer.uuid] = keyComposer;
                }
            }

            // if the composer key even has been stopped, we need clean the list of triggered keys as we wont get a keyup event anymore.
            // We need also stop pending longPress key
            if (stopped) {
                for (i = (keyUp ? i : 0); i < nbrMatches; i ++) {
                    keyComposer = matches[i].object || matches[i];
                    delete this._triggeredKeys[keyComposer.uuid];

                    if (keyComposer._longPressTimeout) {
                        clearTimeout(keyComposer._longPressTimeout);
                        keyComposer._longPressTimeout = null;
                        delete this._longPressKeys[keyComposer.uuid];
                    }
                }
            }

            return stopped;
        }
    },

    _cleanup: {
        value: function () {
            var keyComposer,
                i;

            if (this._cleanupTimeout) {
                clearTimeout(this._cleanupTimeout);
            }

            for (i in this._triggeredKeys) {
                if (this._triggeredKeys.hasOwnProperty(i)) {
                    delete this._triggeredKeys[i];
                }
            }

            for (i in this._longPressKeys) {
                if (this._longPressKeys.hasOwnProperty(i)) {
                    keyComposer = this._longPressKeys[i];
                    clearTimeout(keyComposer._longPressTimeout);
                    keyComposer._longPressTimeout = null;
                    delete this._longPressKeys[i];
                }
            }

            this._cleanupTimeout = null;
        }
    },

    _setupCleanupTimer: {
        value: function () {
            var thisRef = this;

            // When a keydown event is stopped, we will not received the corresponding keyup event.
            // If we haven't detected any key events for 2 seconds, let's presume the user is done using the keyboard
            // and do some internal cleanup in case we missed some keyup events.

            if (this._cleanupTimeout) {
                clearTimeout(this._cleanupTimeout);
            }
            this._cleanupTimeout = setTimeout(function () {
                thisRef._cleanup();
            }, this._cleanupThreshold);
        }
    },

    _convertKeysToModifiersAndKeyCode: {
        value: function (keys) {
            var nbrKeys,
                key,
                i,
                keyCode = 0,
                modifiers = 0;

            keys = keys.toLowerCase().replace(/ /g, "").replace(/\+\+/g, "+add").split("+");
            nbrKeys = keys.length;

            // Convert the keys into a modifiers mask
            for (i = 0; i < nbrKeys - 1; i ++) {
                // convert alias
                key = keys[i];
                key = KEYNAMES_ALIASES[key] || key;

                // make sure it's a modifier
                key = MODIFIERS[key];
                if (key) {
                    modifiers |= key.value;
                } else {
                    console.warn("Invalid Key sequence:", keys);
                    return null;
                }
            }

            // Extract the final key
            key = keys[nbrKeys - 1];
            key = NORMALIZED_CHARS[key] || key;
            key = NORMALIZED_KEYS[key] || key;  // This is needed for browsers that don't use W3C Optional Key Codes

            if (key.length > 1) {
                keyCode = KEYNAMES_TO_KEYCODES[key];

                // If the key append to be a modifier, we need adjust modifiers accordingly
                key = MODIFIERS[KEYNAMES_ALIASES[key] || key];
                if (key) {
                    modifiers |= key.value;
                }
            } else {
                keyCode = key.toUpperCase().charCodeAt(0);
            }

            return {modifiers: modifiers, keyCode: keyCode};
        }
    },

    _decodeKeyIdentifier: {
        value: function (identifier) {
            if (identifier.match(/U\+/)) {
                return parseInt(identifier.substring(2), 16);
            }
        }
    }
});

Montage.defineProperty(exports, "defaultKeyManager", {
    get: function () {
        if (!defaultKeyManager) {
            defaultKeyManager = new KeyManager();
        }
        return defaultKeyManager;
    }
});