Modules (181)

KeyBindingManager

Description

Manages the mapping of keyboard inputs to commands.

Dependencies

Variables

Private

CtrlDownStates Enumeration

Type
number
    var CtrlDownStates = {
        "NOT_YET_DETECTED"    : 0,
        "DETECTED"            : 1,
        "DETECTED_AND_IGNORED": 2   // For consecutive ctrl keydown events while a Ctrl key is being hold down
    };
Private

MAX_INTERVAL_FOR_CTRL_ALT_KEYS

Type
number
    var MAX_INTERVAL_FOR_CTRL_ALT_KEYS = 30;
Private

_allCommands

Type
Array.<string>
    var _allCommands = [];
Private

_commandMap

Type
!Object.<string, Array.<{key: string, displayKey: string}>>
    var _commandMap  = {};
Private

_ctrlDown

Type
CtrlDownStates, boolean
    var _ctrlDown = CtrlDownStates.NOT_YET_DETECTED,
        _altGrDown = false;
Private

_customKeyMap

Type
UserKeyBinding
    var _customKeyMap      = {},
        _customKeyMapCache = {};
Private

_displayKeyMap

Type
{key: string, displayKey: string}
    var _displayKeyMap        = { "up":    "\u2191",
                                  "down":  "\u2193",
                                  "left":  "\u2190",
                                  "right": "\u2192",
                                  "-":     "\u2212" };

    var _specialCommands      = [Commands.EDIT_UNDO, Commands.EDIT_REDO, Commands.EDIT_SELECT_ALL,
                                 Commands.EDIT_CUT, Commands.EDIT_COPY, Commands.EDIT_PASTE],
        _reservedShortcuts    = ["Ctrl-Z", "Ctrl-Y", "Ctrl-A", "Ctrl-X", "Ctrl-C", "Ctrl-V"],
        _macReservedShortcuts = ["Cmd-,", "Cmd-H", "Cmd-Alt-H", "Cmd-M", "Cmd-Shift-Z", "Cmd-Q"],
        _keyNames             = ["Up", "Down", "Left", "Right", "Backspace", "Enter", "Space", "Tab",
                                 "PageUp", "PageDown", "Home", "End", "Insert", "Delete"];
Private

_enabled

Type
boolean
    var _enabled = true;
Private

_globalKeydownHooks

Type
Array.<function(Event): boolean>
    var _globalKeydownHooks = [];
Private

_keyMap

Type
!Object.<string, {commandID: string, key: string, displayKey: string}>
    var _keyMap            = {},    // For the actual key bindings including user specified ones
        // For the default factory key bindings, cloned from _keyMap after all extensions are loaded.
        _defaultKeyMap     = {};
Private

_lastKeyIdentifier

Type
string
    var _lastKeyIdentifier;
Private

_lastTimeStamp

Type
number
    var _lastTimeStamp;
Private Public API

_loadUserKeyMap

Type
Function
    var _loadUserKeyMap;
Private Public API

_onCtrlUp

Type
Function
    var _onCtrlUp;
Private

_showErrors

Type
boolean
    var _showErrors = true;

Functions

Private

_addBinding

commandID string
keyBinding string,{{key: string,displayKey: string}}
a single shortcut.
platform nullable string
"all" indicates all platforms, not overridable - undefined indicates all platforms, overridden by platform-specific binding
userBindings optional boolean
true if adding a user key binding or undefined otherwise.
Returns: ?{key: string,displayKey:String}
Returns a record for valid key bindings. Returns null when key binding platform does not match, binding does not normalize, or is already assigned.
    function _addBinding(commandID, keyBinding, platform, userBindings) {
        var key,
            result = null,
            normalized,
            normalizedDisplay,
            explicitPlatform = keyBinding.platform || platform,
            targetPlatform,
            command,
            bindingsToDelete = [],
            existing;

        // For platform: "all", use explicit current plaform
        if (explicitPlatform && explicitPlatform !== "all") {
            targetPlatform = explicitPlatform;
        } else {
            targetPlatform = brackets.platform;
        }


        // Skip if the key binding is not for this platform.
        if (explicitPlatform === "mac" && brackets.platform !== "mac") {
            return null;
        }

        // if the request does not specify an explicit platform, and we're
        // currently on a mac, then replace Ctrl with Cmd.
        key = (keyBinding.key) || keyBinding;
        if (brackets.platform === "mac" && (explicitPlatform === undefined || explicitPlatform === "all")) {
            key = key.replace("Ctrl", "Cmd");
            if (keyBinding.displayKey !== undefined) {
                keyBinding.displayKey = keyBinding.displayKey.replace("Ctrl", "Cmd");
            }
        }

        normalized = normalizeKeyDescriptorString(key);

        // skip if the key binding is invalid
        if (!normalized) {
            console.error("Unable to parse key binding " + key + ". Permitted modifiers: Ctrl, Cmd, Alt, Opt, Shift; separated by '-' (not '+').");
            return null;
        }

        // check for duplicate key bindings
        existing = _keyMap[normalized];

        // for cross-platform compatibility
        if (exports.useWindowsCompatibleBindings) {
            // windows-only key bindings are used as the default binding
            // only if a default binding wasn't already defined
            if (explicitPlatform === "win") {
                // search for a generic or platform-specific binding if it
                // already exists
                if (existing && (!existing.explicitPlatform ||
                                 existing.explicitPlatform === brackets.platform ||
                                 existing.explicitPlatform === "all")) {
                    // do not clobber existing binding with windows-only binding
                    return null;
                }

                // target this windows binding for the current platform
                targetPlatform = brackets.platform;
            }
        }

        // skip if this binding doesn't match the current platform
        if (targetPlatform !== brackets.platform) {
            return null;
        }

        // skip if the key is already assigned
        if (existing) {
            if (!existing.explicitPlatform && explicitPlatform) {
                // remove the the generic binding to replace with this new platform-specific binding
                removeBinding(normalized);
                existing = false;
            }
        }

        // delete existing bindings when
        // (1) replacing a windows-compatible binding with a generic or
        //     platform-specific binding
        // (2) replacing a generic binding with a platform-specific binding
        var existingBindings = _commandMap[commandID] || [],
            isWindowsCompatible,
            isReplaceGeneric,
            ignoreGeneric;

        existingBindings.forEach(function (binding) {
            // remove windows-only bindings in _commandMap
            isWindowsCompatible = exports.useWindowsCompatibleBindings &&
                binding.explicitPlatform === "win";

            // remove existing generic binding
            isReplaceGeneric = !binding.explicitPlatform &&
                explicitPlatform;

            if (isWindowsCompatible || isReplaceGeneric) {
                bindingsToDelete.push(binding);
            } else {
                // existing binding is platform-specific and the requested binding is generic
                ignoreGeneric = binding.explicitPlatform && !explicitPlatform;
            }
        });

        if (ignoreGeneric) {
            // explicit command binding overrides this one
            return null;
        }

        if (existing) {
            // do not re-assign a key binding
            console.error("Cannot assign " + normalized + " to " + commandID + ". It is already assigned to " + _keyMap[normalized].commandID);
            return null;
        }

        // remove generic or windows-compatible bindings
        bindingsToDelete.forEach(function (binding) {
            removeBinding(binding.key);
        });

        // optional display-friendly string (e.g. CMD-+ instead of CMD-=)
        normalizedDisplay = (keyBinding.displayKey) ? normalizeKeyDescriptorString(keyBinding.displayKey) : normalized;

        // 1-to-many commandID mapping to key binding
        if (!_commandMap[commandID]) {
            _commandMap[commandID] = [];
        }

        result = {
            key                 : normalized,
            displayKey          : normalizedDisplay,
            explicitPlatform    : explicitPlatform
        };

        _commandMap[commandID].push(result);

        // 1-to-1 key binding to commandID
        _keyMap[normalized] = {
            commandID           : commandID,
            key                 : normalized,
            displayKey          : normalizedDisplay,
            explicitPlatform    : explicitPlatform
        };

        if (!userBindings) {
            _updateCommandAndKeyMaps(_keyMap[normalized]);
        }

        // notify listeners
        command = CommandManager.get(commandID);

        if (command) {
            command.trigger("keyBindingAdded", result);
        }

        return result;
    }
Private

_applyUserKeyBindings

    function _applyUserKeyBindings() {
        var remappedCommands   = [],
            remappedKeys       = [],
            restrictedCommands = [],
            restrictedKeys     = [],
            invalidKeys        = [],
            invalidCommands    = [],
            multipleKeys       = [],
            duplicateBindings  = [],
            errorMessage       = "";

        _.forEach(_customKeyMap, function (commandID, key) {
            var normalizedKey    = normalizeKeyDescriptorString(key),
                existingBindings = _commandMap[commandID] || [];

            // Skip this since we don't allow user to update key binding of a special
            // command like cut, copy, paste, undo, redo and select all.
            if (_isSpecialCommand(commandID)) {
                restrictedCommands.push(commandID);
                return;
            }

            // Skip this since we don't allow user to update a shortcut used in
            // a special command or any Mac system command.
            if (_isReservedShortcuts(normalizedKey)) {
                restrictedKeys.push(key);
                return;
            }

            // Skip this if the key is invalid.
            if (!normalizedKey) {
                invalidKeys.push(key);
                return;
            }

            if (_isKeyAssigned(normalizedKey)) {
                if (remappedKeys.indexOf(normalizedKey) !== -1) {
                    // JSON parser already removed all the duplicates that have the exact
                    // same case or order in their keys. So we're only detecting duplicate
                    // bindings that have different orders or different cases used in the key.
                    duplicateBindings.push(key);
                    return;
                }
                // The same key binding already exists, so skip this.
                if (_keyMap[normalizedKey].commandID === commandID) {
                    // Still need to add it to the remappedCommands so that
                    // we can detect any duplicate later on.
                    remappedCommands.push(commandID);
                    return;
                }
                removeBinding(normalizedKey);
            }

            if (remappedKeys.indexOf(normalizedKey) === -1) {
                remappedKeys.push(normalizedKey);
            }

            // Remove another key binding if the new key binding is for a command
            // that has a different key binding. e.g. "Ctrl-W": "edit.selectLine"
            // requires us to remove "Ctrl-W" from "file.close" command, but we
            // also need to remove "Ctrl-L" from "edit.selectLine".
            if (existingBindings.length) {
                existingBindings.forEach(function (binding) {
                    removeBinding(binding.key);
                });
            }

            if (commandID) {
                if (_allCommands.indexOf(commandID) !== -1) {
                    if (remappedCommands.indexOf(commandID) === -1) {
                        var keybinding = { key: normalizedKey };

                        keybinding.displayKey = _getDisplayKey(normalizedKey);
                        _addBinding(commandID, keybinding.displayKey ? keybinding : normalizedKey, brackets.platform, true);
                        remappedCommands.push(commandID);
                    } else {
                        multipleKeys.push(commandID);
                    }
                } else {
                    invalidCommands.push(commandID);
                }
            }
        });

        if (restrictedCommands.length) {
            errorMessage = StringUtils.format(Strings.ERROR_RESTRICTED_COMMANDS, _getBulletList(restrictedCommands));
        }

        if (restrictedKeys.length) {
            errorMessage += StringUtils.format(Strings.ERROR_RESTRICTED_SHORTCUTS, _getBulletList(restrictedKeys));
        }

        if (multipleKeys.length) {
            errorMessage += StringUtils.format(Strings.ERROR_MULTIPLE_SHORTCUTS, _getBulletList(multipleKeys));
        }

        if (duplicateBindings.length) {
            errorMessage += StringUtils.format(Strings.ERROR_DUPLICATE_SHORTCUTS, _getBulletList(duplicateBindings));
        }

        if (invalidKeys.length) {
            errorMessage += StringUtils.format(Strings.ERROR_INVALID_SHORTCUTS, _getBulletList(invalidKeys));
        }

        if (invalidCommands.length) {
            errorMessage += StringUtils.format(Strings.ERROR_NONEXISTENT_COMMANDS, _getBulletList(invalidCommands));
        }

        if (_showErrors && errorMessage) {
            _showErrorsAndOpenKeyMap("", errorMessage);
        }
    }
Private

_buildKeyDescriptor

hasCtrl boolean
Is Ctrl key enabled
hasAlt boolean
Is Alt key enabled
hasShift boolean
Is Shift key enabled
key string
The key that's pressed
Returns: string
The normalized key descriptor
    function _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key) {
        if (!key) {
            console.log("KeyBindingManager _buildKeyDescriptor() - No key provided!");
            return "";
        }

        var keyDescriptor = [];

        if (hasMacCtrl) {
            keyDescriptor.push("Ctrl");
        }
        if (hasAlt) {
            keyDescriptor.push("Alt");
        }
        if (hasShift) {
            keyDescriptor.push("Shift");
        }

        if (hasCtrl) {
            // Windows display Ctrl first, Mac displays Command symbol last
            if (brackets.platform === "mac") {
                keyDescriptor.push("Cmd");
            } else {
                keyDescriptor.unshift("Ctrl");
            }
        }

        keyDescriptor.push(key);

        return keyDescriptor.join("-");
    }
Private

_detectAltGrKeyDown

e non-nullable KeyboardEvent
keyboard event object
    function _detectAltGrKeyDown(e) {
        if (brackets.platform !== "win") {
            return;
        }

        if (!_altGrDown) {
            if (_ctrlDown !== CtrlDownStates.DETECTED_AND_IGNORED && e.ctrlKey && e.keyIdentifier === "Control") {
                _ctrlDown = CtrlDownStates.DETECTED;
            } else if (e.repeat && e.ctrlKey && e.keyIdentifier === "Control") {
                // We get here if the user is holding down left/right Control key. Set it to false
                // so that we don't misidentify the combination of Ctrl and Alt keys as AltGr key.
                _ctrlDown = CtrlDownStates.DETECTED_AND_IGNORED;
            } else if (_ctrlDown === CtrlDownStates.DETECTED && e.altKey && e.ctrlKey && e.keyIdentifier === "Alt" &&
                        (e.timeStamp - _lastTimeStamp) < MAX_INTERVAL_FOR_CTRL_ALT_KEYS) {
                _altGrDown = true;
                _lastKeyIdentifier = "Alt";
                _enabled = false;
                $(window).on("keyup", _onCtrlUp);
            } else {
                // Reset _ctrlDown so that we can start over in detecting the two key events
                // required for AltGr key.
                _ctrlDown = CtrlDownStates.NOT_YET_DETECTED;
            }
            _lastTimeStamp = e.timeStamp;
        } else if (e.keyIdentifier === "Control" || e.keyIdentifier === "Alt") {
            // If the user is NOT holding down AltGr key or is also pressing Ctrl key,
            // then _lastKeyIdentifier will be the same as keyIdentifier in the current
            // key event. So we need to quit AltGr mode to re-enable KBM.
            if (e.altKey && e.ctrlKey && e.keyIdentifier === _lastKeyIdentifier) {
                _quitAltGrMode();
            } else {
                _lastKeyIdentifier = e.keyIdentifier;
            }
        }
    }
Private

_getBulletList

list Array.<string>
An array of strings to be converted into a message string with a bullet list.
Returns: string
the html text version of the list
    function _getBulletList(list) {
        var message = "<ul class='dialog-list'>";
        list.forEach(function (info) {
            message += "<li>" + info + "</li>";
        });
        message += "</ul>";
        return message;
    }
Private Public API

_getDisplayKey

key string
The non-modifier key used in the shortcut. It does not need to be normalized.
Returns: string
An empty string if key is not one of those we want to show with the unicode symbol. Otherwise, the corresponding unicode symbol is returned.
    function _getDisplayKey(key) {
        var displayKey = "",
            match = key ? key.match(/(Up|Down|Left|Right|\-)$/i) : null;
        if (match && !/Page(Up|Down)/.test(key)) {
            displayKey = key.substr(0, match.index) + _displayKeyMap[match[0].toLowerCase()];
        }
        return displayKey;
    }
Private

_getUserKeyMapFilePath

Returns: string
full file path to the user key map file.
    function _getUserKeyMapFilePath() {
        if (window.isBracketsTestWindow) {
            return brackets.app.getApplicationSupportDirectory() + "/_test_/" + KEYMAP_FILENAME;
        }
        return _userKeyMapFilePath;
    }
Private

_handleCommandRegistered

Adds default key bindings when commands are registered to CommandManager

event $.Event
jQuery event
command Command
Newly registered command
    function _handleCommandRegistered(event, command) {
        var commandId   = command.getID(),
            defaults    = KeyboardPrefs[commandId];

        if (defaults) {
            addBinding(commandId, defaults);
        }
    }
Private Public API

_handleKey

Process the keybinding for the current key.

A string
key-description string.
Returns: boolean
true if the key was processed, false otherwise
    function _handleKey(key) {
        if (_enabled && _keyMap[key]) {
            // The execute() function returns a promise because some commands are async.
            // Generally, commands decide whether they can run or not synchronously,
            // and reject immediately, so we can test for that synchronously.
            var promise = CommandManager.execute(_keyMap[key].commandID);
            return (promise.state() !== "rejected");
        }
        return false;
    }
Private Public API

_handleKeyEvent

Handles a given keydown event, checking global hooks first before deciding to handle it ourselves.

The Event
keydown event to handle.
    function _handleKeyEvent(event) {
        var i, handled = false;
        for (i = _globalKeydownHooks.length - 1; i >= 0; i--) {
            if (_globalKeydownHooks[i](event)) {
                handled = true;
                break;
            }
        }
        _detectAltGrKeyDown(event);
        if (!handled && _handleKey(_translateKeyboardEvent(event))) {
            event.stopPropagation();
            event.preventDefault();
        }
    }

    AppInit.htmlReady(function () {
        // Install keydown event listener.
        window.document.body.addEventListener(
            "keydown",
            _handleKeyEvent,
            true
        );

        exports.useWindowsCompatibleBindings = (brackets.platform !== "mac") &&
            (brackets.platform !== "win");
    });
Private Public API

_initCommandAndKeyMaps

    function _initCommandAndKeyMaps() {
        _allCommands = CommandManager.getAll();
        // Keep a copy of the default key bindings before loading user key bindings.
        _defaultKeyMap = _.cloneDeep(_keyMap);
    }
Private

_isKeyAssigned

A string
normalized key-description string.
Returns: boolean
true if the key is already assigned, false otherwise.
    function _isKeyAssigned(key) {
        return (_keyMap[key] !== undefined);
    }
Private

_isReservedShortcuts

normalizedKey non-nullable string
A key combination string used for a keyboard shortcut
Returns: boolean
true if normalizedKey is a restricted shortcut, false otherwise.
    function _isReservedShortcuts(normalizedKey) {
        if (!normalizedKey) {
            return false;
        }

        if (_reservedShortcuts.indexOf(normalizedKey) > -1 ||
                _reservedShortcuts.indexOf(normalizedKey.replace("Cmd", "Ctrl")) > -1) {
            return true;
        }

        if (brackets.platform === "mac" && _macReservedShortcuts.indexOf(normalizedKey) > -1) {
            return true;
        }

        return false;
    }
Private

_isSpecialCommand

commandID non-nullable string
A string referring to a specific command
Returns: boolean
true if normalizedKey is a special command, false otherwise.
    function _isSpecialCommand(commandID) {
        if (brackets.platform === "mac" && commandID === "file.quit") {
            return true;
        }

        return (_specialCommands.indexOf(commandID) > -1);
    }
Private

_mapKeycodeToKey

The number
keycode from the keyboard event.
The string
current best guess at what the key is.
Returns: string
If the key is OS-inconsistent, the correct key; otherwise, the original key.
    function _mapKeycodeToKey(keycode, key) {
        // If keycode represents one of the digit keys (0-9), then return the corresponding digit
        // by subtracting KeyEvent.DOM_VK_0 from keycode. ie. [48-57] --> [0-9]
        if (keycode >= KeyEvent.DOM_VK_0 && keycode <= KeyEvent.DOM_VK_9) {
            return String(keycode - KeyEvent.DOM_VK_0);
        // Do the same with the numpad numbers
        // by subtracting KeyEvent.DOM_VK_NUMPAD0 from keycode. ie. [96-105] --> [0-9]
        } else if (keycode >= KeyEvent.DOM_VK_NUMPAD0 && keycode <= KeyEvent.DOM_VK_NUMPAD9) {
            return String(keycode - KeyEvent.DOM_VK_NUMPAD0);
        }


        switch (keycode) {
        case KeyEvent.DOM_VK_SEMICOLON:
            return ";";
        case KeyEvent.DOM_VK_EQUALS:
            return "=";
        case KeyEvent.DOM_VK_COMMA:
            return ",";
        case KeyEvent.DOM_VK_SUBTRACT:
        case KeyEvent.DOM_VK_DASH:
            return "-";
        case KeyEvent.DOM_VK_ADD:
            return "+";
        case KeyEvent.DOM_VK_DECIMAL:
        case KeyEvent.DOM_VK_PERIOD:
            return ".";
        case KeyEvent.DOM_VK_DIVIDE:
        case KeyEvent.DOM_VK_SLASH:
            return "/";
        case KeyEvent.DOM_VK_BACK_QUOTE:
            return "`";
        case KeyEvent.DOM_VK_OPEN_BRACKET:
            return "[";
        case KeyEvent.DOM_VK_BACK_SLASH:
            return "\\";
        case KeyEvent.DOM_VK_CLOSE_BRACKET:
            return "]";
        case KeyEvent.DOM_VK_QUOTE:
            return "'";
        default:
            return key;
        }
    }
Private

_openUserKeyMap

    function _openUserKeyMap() {
        var userKeyMapPath = _getUserKeyMapFilePath(),
            file = FileSystem.getFileForPath(userKeyMapPath);
        file.exists(function (err, doesExist) {
            if (doesExist) {
                CommandManager.execute(Commands.FILE_OPEN, { fullPath: userKeyMapPath });
            } else {
                var defaultContent = "{\n    \"documentation\": \"https://github.com/adobe/brackets/wiki/User-Key-Bindings\"," +
                                     "\n    \"overrides\": {" +
                                     "\n        \n    }\n}\n";

                FileUtils.writeText(file, defaultContent, true)
                    .done(function () {
                        CommandManager.execute(Commands.FILE_OPEN, { fullPath: userKeyMapPath });
                    });
            }
        });
    }

    // Due to circular dependencies, not safe to call on() directly
    EventDispatcher.on_duringInit(CommandManager, "commandRegistered", _handleCommandRegistered);
    CommandManager.register(Strings.CMD_OPEN_KEYMAP, Commands.FILE_OPEN_KEYMAP, _openUserKeyMap);

    // Asynchronously loading DocumentManager to avoid the circular dependency
    require(["document/DocumentManager"], function (DocumentManager) {
        DocumentManager.on("documentSaved", function checkKeyMapUpdates(e, doc) {
            if (doc && doc.file.fullPath === _userKeyMapFilePath) {
                _loadUserKeyMap();
            }
        });
    });
Private

_quitAltGrMode

    function _quitAltGrMode() {
        _enabled = true;
        _ctrlDown = CtrlDownStates.NOT_YET_DETECTED;
        _altGrDown = false;
        _lastTimeStamp = null;
        _lastKeyIdentifier = null;
        $(window).off("keyup", _onCtrlUp);
    }
Private

_readUserKeyMap

Returns: $.Promise
a jQuery promise that will be resolved with the JSON object if the user key map file has "overrides" property or an empty JSON. If the key map file cannot be read or cannot be parsed by the JSON parser, then the promise is rejected with an error.
    function _readUserKeyMap() {
        var file   = FileSystem.getFileForPath(_getUserKeyMapFilePath()),
            result = new $.Deferred();

        file.exists(function (err, doesExist) {
            if (doesExist) {
                FileUtils.readAsText(file)
                    .done(function (text) {
                        var keyMap = {};
                        try {
                            if (text) {
                                var json = JSON.parse(text);
                                // If no overrides, return an empty key map.
                                result.resolve((json && json.overrides) || keyMap);
                            } else {
                                // The file is empty, so return an empty key map.
                                result.resolve(keyMap);
                            }
                        } catch (err) {
                            // Cannot parse the text read from the key map file.
                            result.reject(err);
                        }
                    })
                    .fail(function (err) {
                        // Key map file cannot be loaded.
                        result.reject(err);
                    });
            } else {
                // Just resolve if no user key map file
                result.resolve();
            }
        });
        return result.promise();
    }
Private Public API

_reset

    function _reset() {
        _keyMap = {};
        _defaultKeyMap = {};
        _customKeyMap = {};
        _customKeyMapCache = {};
        _commandMap = {};
        _globalKeydownHooks = [];
        _userKeyMapFilePath = brackets.app.getApplicationSupportDirectory() + "/" + KEYMAP_FILENAME;
    }
Private Public API

_setUserKeyMapFilePath

fullPath string
file path to the user key map file.
    function _setUserKeyMapFilePath(fullPath) {
        _userKeyMapFilePath = fullPath;
    }

    AppInit.extensionsLoaded(function () {
        var params  = new UrlParams();
        params.parse();
        if (params.get("reloadWithoutUserExts") === "true") {
            _showErrors = false;
        }

        _initCommandAndKeyMaps();
        _loadUserKeyMap();
    });

    // unit test only
    exports._reset = _reset;
    exports._setUserKeyMapFilePath = _setUserKeyMapFilePath;
    exports._getDisplayKey = _getDisplayKey;
    exports._loadUserKeyMap = _loadUserKeyMap;
    exports._initCommandAndKeyMaps = _initCommandAndKeyMaps;
    exports._onCtrlUp = _onCtrlUp;

    // Define public API
    exports.getKeymap = getKeymap;
    exports.addBinding = addBinding;
    exports.removeBinding = removeBinding;
    exports.formatKeyDescriptor = formatKeyDescriptor;
    exports.getKeyBindings = getKeyBindings;
    exports.addGlobalKeydownHook = addGlobalKeydownHook;
    exports.removeGlobalKeydownHook = removeGlobalKeydownHook;
Private

_showErrorsAndOpenKeyMap

err nullable string
Error type returned from JSON parser or open file operation
message optional string
Error message to be displayed in the dialog
    function _showErrorsAndOpenKeyMap(err, message) {
        // Asynchronously loading Dialogs module to avoid the circular dependency
        require(["widgets/Dialogs"], function (Dialogs) {
            var errorMessage = Strings.ERROR_KEYMAP_CORRUPT;

            if (err === FileSystemError.UNSUPPORTED_ENCODING) {
                errorMessage = Strings.ERROR_LOADING_KEYMAP;
            } else if (message) {
                errorMessage = message;
            }

            Dialogs.showModalDialog(
                DefaultDialogs.DIALOG_ID_ERROR,
                Strings.ERROR_KEYMAP_TITLE,
                errorMessage
            )
                .done(function () {
                    if (err !== FileSystemError.UNSUPPORTED_ENCODING) {
                        CommandManager.execute(Commands.FILE_OPEN_KEYMAP);
                    }
                });
        });
    }
Private

_sortByPlatform

    function _sortByPlatform(a, b) {
        var a1 = (a.platform) ? 1 : 0,
            b1 = (b.platform) ? 1 : 0;
        return b1 - a1;
    }
Private

_translateKeyboardEvent

Takes a keyboard event and translates it into a key in a key map

    function _translateKeyboardEvent(event) {
        var hasMacCtrl = (brackets.platform === "mac") ? (event.ctrlKey) : false,
            hasCtrl = (brackets.platform !== "mac") ? (event.ctrlKey) : (event.metaKey),
            hasAlt = (event.altKey),
            hasShift = (event.shiftKey),
            key = String.fromCharCode(event.keyCode);

        //From the W3C, if we can get the KeyboardEvent.keyIdentifier then look here
        //As that will let us use keys like then function keys "F5" for commands. The
        //full set of values we can use is here
        //http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set
        var ident = event.keyIdentifier;
        if (ident) {
            if (ident.charAt(0) === "U" && ident.charAt(1) === "+") {
                //This is a unicode code point like "U+002A", get the 002A and use that
                key = String.fromCharCode(parseInt(ident.substring(2), 16));
            } else {
                //This is some non-character key, just use the raw identifier
                key = ident;
            }
        }

        // Translate some keys to their common names
        if (key === "\t") {
            key = "Tab";
        } else if (key === " ") {
            key = "Space";
        } else if (key === "\b") {
            key = "Backspace";
        } else if (key === "Help") {
            key = "Insert";
        } else if (event.keyCode === KeyEvent.DOM_VK_DELETE) {
            key = "Delete";
        } else {
            key = _mapKeycodeToKey(event.keyCode, key);
        }

        return _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key);
    }
Private

_undoPriorUserKeyBindings

    function _undoPriorUserKeyBindings() {
        _.forEach(_customKeyMapCache, function (commandID, key) {
            var normalizedKey  = normalizeKeyDescriptorString(key),
                defaults       = _.find(_.toArray(_defaultKeyMap), { "commandID": commandID }),
                defaultCommand = _defaultKeyMap[normalizedKey];

            // We didn't modified this before, so skip it.
            if (_isSpecialCommand(commandID) ||
                    _isReservedShortcuts(normalizedKey)) {
                return;
            }

            if (_isKeyAssigned(normalizedKey) &&
                    _customKeyMap[key] !== commandID && _customKeyMap[normalizedKey] !== commandID) {
                // Unassign the key from any command. e.g. "Cmd-W": "file.open" in _customKeyMapCache
                // will require us to remove Cmd-W shortcut from file.open command.
                removeBinding(normalizedKey);
            }

            // Reassign the default key binding. e.g. "Cmd-W": "file.open" in _customKeyMapCache
            // will require us to reassign Cmd-O shortcut to file.open command.
            if (defaults) {
                addBinding(commandID, defaults, brackets.platform);
            }

            // Reassign the default key binding of the previously modified command.
            // e.g. "Cmd-W": "file.open" in _customKeyMapCache will require us to reassign Cmd-W
            // shortcut to file.close command.
            if (defaultCommand && defaultCommand.key) {
                addBinding(defaultCommand.commandID, defaultCommand.key, brackets.platform);
            }
        });
    }
Private

_updateCommandAndKeyMaps

newBinding {commandID: string,key: string,displayKey:string,explicitPlatform: string}
    function _updateCommandAndKeyMaps(newBinding) {
        if (_allCommands.length === 0) {
            return;
        }

        if (newBinding && newBinding.commandID && _allCommands.indexOf(newBinding.commandID) === -1) {
            _defaultKeyMap[newBinding.commandID] = _.cloneDeep(newBinding);

            // Process user key map again to catch any reassignment to all new key bindings added from extensions.
            _loadUserKeyMap();
        }
    }
Public API

addBinding

Add one or more key bindings to a particular Command.

command non-nullable string, Command
A command ID or command object
keyBindings nullable ({key: string, displayKey: string}, Array.<{key: string, displayKey: string, platform: string}>)
A single key binding or an array of keybindings. Example: "Shift-Cmd-F". Mac and Win key equivalents are automatically mapped to each other. Use displayKey property to display a different string (e.g. "CMD+" instead of "CMD=").
platform nullable string
The target OS of the keyBindings either "mac", "win" or "linux". If undefined, all platforms not explicitly defined will use the key binding. NOTE: If platform is not specified, Ctrl will be replaced by Cmd for "mac" platform
Returns: {key: string,displayKey:String},Array.<{key: string,displayKey:String}>
Returns record(s) for valid key binding(s)
    function addBinding(command, keyBindings, platform) {
        var commandID = "",
            results;

        if (!command) {
            console.error("addBinding(): missing required parameter: command");
            return;
        }

        if (!keyBindings) { return; }

        if (typeof (command) === "string") {
            commandID = command;
        } else {
            commandID = command.getID();
        }

        if (Array.isArray(keyBindings)) {
            var keyBinding;
            results = [];

            // process platform-specific bindings first
            keyBindings.sort(_sortByPlatform);

            keyBindings.forEach(function addSingleBinding(keyBindingRequest) {
                // attempt to add keybinding
                keyBinding = _addBinding(commandID, keyBindingRequest, keyBindingRequest.platform);

                if (keyBinding) {
                    results.push(keyBinding);
                }
            });
        } else {
            results = _addBinding(commandID, keyBindings, platform);
        }

        return results;
    }
Public API

addGlobalKeydownHook

Adds a global keydown hook that gets first crack at keydown events before standard keybindings do. This is intended for use by modal or semi-modal UI elements like dialogs or the code hint list that should execute before normal command bindings are run.

The hook is passed one parameter, the original keyboard event. If the hook handles the event (or wants to block other global hooks from handling the event), it should return true. Note that this will only stop other global hooks and KeyBindingManager from handling the event; to prevent further event propagation, you will need to call stopPropagation(), stopImmediatePropagation(), and/or preventDefault() as usual.

Multiple keydown hooks can be registered, and are executed in order, most-recently-added first.

(We have to have a special API for this because (1) handlers are normally called in least-recently-added order, and we want most-recently-added; (2) native DOM events don't have a way for us to find out if stopImmediatePropagation()/stopPropagation() has been called on the event, so we have to have some other way for one of the hooks to indicate that it wants to block the other hooks from running.)

hook function(Event): boolean
The global hook to add.
    function addGlobalKeydownHook(hook) {
        _globalKeydownHooks.push(hook);
    }
Public API

formatKeyDescriptor

Convert normalized key representation to display appropriate for platform.

descriptor non-nullable string
Normalized key descriptor.
Returns: !string
Display/Operating system appropriate string
    function formatKeyDescriptor(descriptor) {
        var displayStr;

        if (brackets.platform === "mac") {
            displayStr = descriptor.replace(/-(?!$)/g, "");     // remove dashes
            displayStr = displayStr.replace("Ctrl", "\u2303");  // Ctrl > control symbol
            displayStr = displayStr.replace("Cmd", "\u2318");   // Cmd > command symbol
            displayStr = displayStr.replace("Shift", "\u21E7"); // Shift > shift symbol
            displayStr = displayStr.replace("Alt", "\u2325");   // Alt > option symbol
        } else {
            displayStr = descriptor.replace("Ctrl", Strings.KEYBOARD_CTRL);
            displayStr = displayStr.replace("Shift", Strings.KEYBOARD_SHIFT);
            displayStr = displayStr.replace(/-(?!$)/g, "+");
        }

        displayStr = displayStr.replace("Space", Strings.KEYBOARD_SPACE);

        displayStr = displayStr.replace("PageUp", Strings.KEYBOARD_PAGE_UP);
        displayStr = displayStr.replace("PageDown", Strings.KEYBOARD_PAGE_DOWN);
        displayStr = displayStr.replace("Home", Strings.KEYBOARD_HOME);
        displayStr = displayStr.replace("End", Strings.KEYBOARD_END);

        displayStr = displayStr.replace("Ins", Strings.KEYBOARD_INSERT);
        displayStr = displayStr.replace("Del", Strings.KEYBOARD_DELETE);

        return displayStr;
    }
Public API

getKeyBindings

Retrieve key bindings currently associated with a command

command non-nullable string, Command
A command ID or command object
Returns: !Array.<{{key: string,displayKey: string}}>
An array of associated key bindings.
    function getKeyBindings(command) {
        var bindings    = [],
            commandID   = "";

        if (!command) {
            console.error("getKeyBindings(): missing required parameter: command");
            return [];
        }

        if (typeof (command) === "string") {
            commandID = command;
        } else {
            commandID = command.getID();
        }

        bindings = _commandMap[commandID];
        return bindings || [];
    }
Public API

getKeymap

Returns a copy of the current key map. If the optional 'defaults' parameter is true, then a copy of the default key map is returned.

defaults optional boolean
true if the caller wants a copy of the default key map. Otherwise, the current active key map is returned.
Returns: !Object.<string,{commandID: string,key: string,displayKey: string}>
    function getKeymap(defaults) {
        return $.extend({}, defaults ? _defaultKeyMap : _keyMap);
    }

normalizeKeyDescriptorString

normalizes the incoming key descriptor so the modifier keys are always specified in the correct order

The string
string for a key descriptor, can be in any order, the result will be Ctrl-Alt-Shift-
Returns: string
The normalized key descriptor or null if the descriptor invalid
    function normalizeKeyDescriptorString(origDescriptor) {
        var hasMacCtrl = false,
            hasCtrl = false,
            hasAlt = false,
            hasShift = false,
            key = "",
            error = false;

        function _compareModifierString(left, right) {
            if (!left || !right) {
                return false;
            }
            left = left.trim().toLowerCase();
            right = right.trim().toLowerCase();

            return (left.length > 0 && left === right);
        }

        origDescriptor.split("-").forEach(function parseDescriptor(ele, i, arr) {
            if (_compareModifierString("ctrl", ele)) {
                if (brackets.platform === "mac") {
                    hasMacCtrl = true;
                } else {
                    hasCtrl = true;
                }
            } else if (_compareModifierString("cmd", ele)) {
                if (brackets.platform === "mac") {
                    hasCtrl = true;
                } else {
                    error = true;
                }
            } else if (_compareModifierString("alt", ele)) {
                hasAlt = true;
            } else if (_compareModifierString("opt", ele)) {
                if (brackets.platform === "mac") {
                    hasAlt = true;
                } else {
                    error = true;
                }
            } else if (_compareModifierString("shift", ele)) {
                hasShift = true;
            } else if (key.length > 0) {
                console.log("KeyBindingManager normalizeKeyDescriptorString() - Multiple keys defined. Using key: " + key + " from: " + origDescriptor);
                error = true;
            } else {
                key = ele;
            }
        });

        if (error) {
            return null;
        }

        // Check to see if the binding is for "-".
        if (key === "" && origDescriptor.search(/^.+--$/) !== -1) {
            key = "-";
        }

        // '+' char is valid if it's the only key. Keyboard shortcut strings should use
        // unicode characters (unescaped). Keyboard shortcut display strings may use
        // unicode escape sequences (e.g. \u20AC euro sign)
        if ((key.indexOf("+")) >= 0 && (key.length > 1)) {
            return null;
        }

        // Ensure that the first letter of the key name is in upper case and the rest are
        // in lower case. i.e. 'a' => 'A' and 'up' => 'Up'
        if (/^[a-z]/i.test(key)) {
            key = _.capitalize(key.toLowerCase());
        }

        // Also make sure that the second word of PageUp/PageDown has the first letter in upper case.
        if (/^Page/.test(key)) {
            key = key.replace(/(up|down)$/, function (match, p1) {
                return _.capitalize(p1);
            });
        }

        // No restriction on single character key yet, but other key names are restricted to either
        // Function keys or those listed in _keyNames array.
        if (key.length > 1 && !/F\d+/.test(key) &&
                _keyNames.indexOf(key) === -1) {
            return null;
        }

        return _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key);
    }
Public API

removeBinding

Remove a key binding from _keymap

key non-nullable string
a key-description string that may or may not be normalized.
platform nullable string
OS from which to remove the binding (all platforms if unspecified)
    function removeBinding(key, platform) {
        if (!key || ((platform !== null) && (platform !== undefined) && (platform !== brackets.platform))) {
            return;
        }

        var normalizedKey = normalizeKeyDescriptorString(key);

        if (!normalizedKey) {
            console.log("Failed to normalize " + key);
        } else if (_isKeyAssigned(normalizedKey)) {
            var binding = _keyMap[normalizedKey],
                command = CommandManager.get(binding.commandID),
                bindings = _commandMap[binding.commandID];

            // delete key binding record
            delete _keyMap[normalizedKey];

            if (bindings) {
                // delete mapping from command to key binding
                _commandMap[binding.commandID] = bindings.filter(function (b) {
                    return (b.key !== normalizedKey);
                });

                if (command) {
                    command.trigger("keyBindingRemoved", {key: normalizedKey, displayKey: binding.displayKey});
                }
            }
        }
    }
Public API

removeGlobalKeydownHook

Removes a global keydown hook added by addGlobalKeydownHook. Does not need to be the most recently added hook.

hook function(Event): boolean
The global hook to remove.
    function removeGlobalKeydownHook(hook) {
        var index = _globalKeydownHooks.indexOf(hook);
        if (index !== -1) {
            _globalKeydownHooks.splice(index, 1);
        }
    }