adobe/brackets

View on GitHub
src/command/CommandManager.js

Summary

Maintainability
B
5 hrs
Test Coverage
/*
 * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

 /**
  * Manages global application commands that can be called from menu items, key bindings, or subparts
  * of the application.
  *
  * This module dispatches these event(s):
  *    - commandRegistered  -- when a new command is registered
  *    - beforeExecuteCommand -- before dispatching a command
  */
define(function (require, exports, module) {
    "use strict";

    var EventDispatcher = require("utils/EventDispatcher");


    /**
     * Map of all registered global commands
     * @type {Object.<commandID: string, Command>}
     */
    var _commands = {};

    /**
     * Temporary copy of commands map for restoring after testing
     * TODO (issue #1039): implement separate require contexts for unit tests
     * @type {Object.<commandID: string, Command>}
     */
    var _commandsOriginal = {};

    /**
     * Events:
     * - enabledStateChange
     * - checkedStateChange
     * - keyBindingAdded
     * - keyBindingRemoved
     *
     * @constructor
     * @private
     * @param {string} name - text that will be displayed in the UI to represent command
     * @param {string} id
     * @param {function} commandFn - the function that is called when the command is executed.
     *
     * TODO: where should this be triggered, The Command or Exports?
     */
    function Command(name, id, commandFn) {
        this._name = name;
        this._id = id;
        this._commandFn = commandFn;
        this._checked = undefined;
        this._enabled = true;
    }
    EventDispatcher.makeEventDispatcher(Command.prototype);

    /**
     * Get command id
     * @return {string}
     */
    Command.prototype.getID = function () {
        return this._id;
    };

    /**
     * Executes the command. Additional arguments are passed to the executing function
     *
     * @return {$.Promise} a jQuery promise that will be resolved when the command completes.
     */
    Command.prototype.execute = function () {
        if (!this._enabled) {
            return (new $.Deferred()).reject().promise();
        }

        var result = this._commandFn.apply(this, arguments);
        if (!result) {
            // If command does not return a promise, assume that it handled the
            // command and return a resolved promise
            return (new $.Deferred()).resolve().promise();
        } else {
            return result;
        }
    };

    /**
     * Is command enabled?
     * @return {boolean}
     */
    Command.prototype.getEnabled = function () {
        return this._enabled;
    };

    /**
     * Sets enabled state of Command and dispatches "enabledStateChange"
     * when the enabled state changes.
     * @param {boolean} enabled
     */
    Command.prototype.setEnabled = function (enabled) {
        var changed = this._enabled !== enabled;
        this._enabled = enabled;

        if (changed) {
            this.trigger("enabledStateChange");
        }
    };

    /**
     * Sets enabled state of Command and dispatches "checkedStateChange"
     * when the enabled state changes.
     * @param {boolean} checked
     */
    Command.prototype.setChecked = function (checked) {
        var changed = this._checked !== checked;
        this._checked = checked;

        if (changed) {
            this.trigger("checkedStateChange");
        }
    };

    /**
     * Is command checked?
     * @return {boolean}
     */
    Command.prototype.getChecked = function () {
        return this._checked;
    };

    /**
     * Sets the name of the Command and dispatches "nameChange" so that
     * UI that reflects the command name can update.
     *
     * Note, a Command name can appear in either HTML or native UI
     * so HTML tags should not be used. To add a Unicode character,
     * use \uXXXX instead of an HTML entity.
     *
     * @param {string} name
     */
    Command.prototype.setName = function (name) {
        var changed = this._name !== name;
        this._name = name;

        if (changed) {
            this.trigger("nameChange");
        }
    };

    /**
     * Get command name
     * @return {string}
     */
    Command.prototype.getName = function () {
        return this._name;
    };



    /**
     * Registers a global command.
     * @param {string} name - text that will be displayed in the UI to represent command
     * @param {string} id - unique identifier for command.
     *      Core commands in Brackets use a simple command title as an id, for example "open.file".
     *      Extensions should use the following format: "author.myextension.mycommandname".
     *      For example, "lschmitt.csswizard.format.css".
     * @param {function(...)} commandFn - the function to call when the command is executed. Any arguments passed to
     *     execute() (after the id) are passed as arguments to the function. If the function is asynchronous,
     *     it must return a jQuery promise that is resolved when the command completes. Otherwise, the
     *     CommandManager will assume it is synchronous, and return a promise that is already resolved.
     * @return {?Command}
     */
    function register(name, id, commandFn) {
        if (_commands[id]) {
            console.log("Attempting to register an already-registered command: " + id);
            return null;
        }
        if (!name || !id || !commandFn) {
            console.error("Attempting to register a command with a missing name, id, or command function:" + name + " " + id);
            return null;
        }

        var command = new Command(name, id, commandFn);
        _commands[id] = command;

        exports.trigger("commandRegistered", command);

        return command;
    }

    /**
     * Registers a global internal only command.
     * @param {string} id - unique identifier for command.
     *      Core commands in Brackets use a simple command title as an id, for example "app.abort_quit".
     *      Extensions should use the following format: "author.myextension.mycommandname".
     *      For example, "lschmitt.csswizard.format.css".
     * @param {function(...)} commandFn - the function to call when the command is executed. Any arguments passed to
     *     execute() (after the id) are passed as arguments to the function. If the function is asynchronous,
     *     it must return a jQuery promise that is resolved when the command completes. Otherwise, the
     *     CommandManager will assume it is synchronous, and return a promise that is already resolved.
     * @return {?Command}
     */
    function registerInternal(id, commandFn) {
        if (_commands[id]) {
            console.log("Attempting to register an already-registered command: " + id);
            return null;
        }
        if (!id || !commandFn) {
            console.error("Attempting to register an internal command with a missing id, or command function: " + id);
            return null;
        }

        var command = new Command(null, id, commandFn);
        _commands[id] = command;

        exports.trigger("commandRegistered", command);

        return command;
    }

    /**
     * Clear all commands for unit testing, but first make copy of commands so that
     * they can be restored afterward
     */
    function _testReset() {
        _commandsOriginal = _commands;
        _commands = {};
    }

    /**
     * Restore original commands after test and release copy
     */
    function _testRestore() {
        _commands = _commandsOriginal;
        _commandsOriginal = {};
    }

    /**
     * Retrieves a Command object by id
     * @param {string} id
     * @return {Command}
     */
    function get(id) {
        return _commands[id];
    }

    /**
     * Returns the ids of all registered commands
     * @return {Array.<string>}
     */
    function getAll() {
        return Object.keys(_commands);
    }

    /**
     * Looks up and runs a global command. Additional arguments are passed to the command.
     *
     * @param {string} id The ID of the command to run.
     * @return {$.Promise} a jQuery promise that will be resolved when the command completes.
     */
    function execute(id) {
        var command = _commands[id];

        if (command) {
            try {
                exports.trigger("beforeExecuteCommand", id);
            } catch (err) {
                console.error(err);
            }

            return command.execute.apply(command, Array.prototype.slice.call(arguments, 1));
        } else {
            return (new $.Deferred()).reject().promise();
        }
    }

    EventDispatcher.makeEventDispatcher(exports);

    // Define public API
    exports.register            = register;
    exports.registerInternal    = registerInternal;
    exports.execute             = execute;
    exports.get                 = get;
    exports.getAll              = getAll;
    exports._testReset          = _testReset;
    exports._testRestore        = _testRestore;
});