/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // @internal function getAccessKey(str) { var i = str.indexOf("&"); if (i == -1) return ""; return str[i + 1]; } // @internal function CommandRecord(name, func, usage, help, label, accesskey, flags, keystr, tip, format, helpUsage) { this.name = name; this.func = func; this._usage = usage; this.scanUsage(); this.help = help; this.label = label ? label : name; this.accesskey = accesskey ? accesskey : ""; this.format = format; this.helpUsage = helpUsage; this.labelstr = label.replace ("&", ""); this.tip = tip; this.flags = flags; this._enabled = true; this.keyNodes = new Array(); this.keystr = keystr; this.uiElements = new Array(); } CommandRecord.prototype.__defineGetter__ ("enabled", cr_getenable); function cr_getenable () { return this._enabled; } CommandRecord.prototype.__defineSetter__ ("enabled", cr_setenable); function cr_setenable (state) { for (var i = 0; i < this.uiElements.length; ++i) { if (state) this.uiElements[i].removeAttribute ("disabled"); else this.uiElements[i].setAttribute ("disabled", "true"); } return (this._enabled = state); } CommandRecord.prototype.__defineSetter__ ("usage", cr_setusage); function cr_setusage (usage) { this._usage = usage; this.scanUsage(); } CommandRecord.prototype.__defineGetter__ ("usage", cr_getusage); function cr_getusage() { return this._usage; } /** * @internal * * Scans the argument spec, in the format " [ ]", into an * array of strings. */ CommandRecord.prototype.scanUsage = function cr_scanusage() { var spec = this._usage; var currentName = ""; var inName = false; var len = spec.length; var capNext = false; this._usage = spec; this.argNames = new Array(); for (var i = 0; i < len; ++i) { switch (spec[i]) { case '[': this.argNames.push (":"); break; case '<': inName = true; break; case '-': capNext = true; break; case '>': inName = false; this.argNames.push (currentName); currentName = ""; capNext = false; break; default: if (inName) currentName += capNext ? spec[i].toUpperCase() : spec[i]; capNext = false; break; } } } /** * Manages commands, with accelerator keys, help text and argument processing. * * You should never need to create an instance of this prototype; access the * command manager through |client.commandManager|. * * @param defaultBundle An |nsIStringBundle| object to load command parameters, * labels a help text from. */ function CommandManager(defaultBundle) { this.commands = new Object(); this.commandHistory = new Object(); this.defaultBundle = defaultBundle; this.currentDispatchDepth = 0; this.maxDispatchDepth = 10; this.dispatchUnwinding = false; } // @undocumented CommandManager.prototype.defaultFlags = 0; /** * Adds multiple commands in a single call. * * @param cmdary |Array| containing commands to define; each item in the |Array| * is also an |Array|, with either 3 or 4 items - corresponding to * the first three or four arguments of |defineCommand|. An extra * property, |stringBundle|, may be set on the |cmdary| |Array| * to override the |defaultBundle| for all the commands. */ CommandManager.prototype.defineCommands = function cmgr_defcmds(cmdary) { var len = cmdary.length; var commands = new Object(); var bundle = "stringBundle" in cmdary ? cmdary.stringBundle : null; for (var i = 0; i < len; ++i) { let name = cmdary[i][0]; let func = cmdary[i][1]; let flags = cmdary[i][2]; let usage = (3 in cmdary[i]) ? cmdary[i][3] : ""; commands[name] = this.defineCommand(name, func, flags, usage, bundle); } return commands; } /** * Adds a single command. * * @param name The |String| name of the command to define. * @param func A |Function| to call to handle dispatch of the new command. * @param flags Optional. A |Number| indicating any special requirements for the * command. * @param usage Optional. A |String| specifying the arguments to the command. If * not specified, then it is assumed there are none. * @param bundle Optional. An |nsIStringBundle| to fetch parameters, labels, * accelerator keys and help from. If not specified, the * |defaultBundle| is used. */ CommandManager.prototype.defineCommand = function cmdmgr_defcmd(name, func, flags, usage, bundle) { if (!bundle) bundle = this.defaultBundle; var helpDefault = MSG_NO_HELP; var labelDefault = name; var aliasFor; if (typeof flags != "number") flags = this.defaultFlags; if (typeof usage != "string") usage = ""; if (typeof func == "string") { var ary = func.match(/(\S+)/); if (ary) aliasFor = ary[1]; else aliasFor = null; helpDefault = getMsg (MSG_DEFAULT_ALIAS_HELP, func); if (aliasFor) labelDefault = getMsgFrom (bundle, "cmd." + aliasFor + ".label", null, name); } var label = getMsgFrom(bundle, "cmd." + name + ".label", null, labelDefault); var accesskey = getMsgFrom(bundle, "cmd." + name + ".accesskey", null, getAccessKey(label)); var help = helpDefault; var helpUsage = ""; // Help is only shown for commands that available from the console. if (flags & CMD_CONSOLE) { help = getMsgFrom(bundle, "cmd." + name + ".help", null, helpDefault); // Only need to lookup localized helpUsage for commands that have them. if (usage) { helpUsage = getMsgFrom(bundle, "cmd." + name + ".helpUsage", null, ""); } } var keystr = getMsgFrom (bundle, "cmd." + name + ".key", null, ""); var format = getMsgFrom (bundle, "cmd." + name + ".format", null, null); var tip = getMsgFrom (bundle, "cmd." + name + ".tip", null, ""); var command = new CommandRecord(name, func, usage, help, label, accesskey, flags, keystr, tip, format, helpUsage); this.addCommand(command); if (aliasFor) command.aliasFor = aliasFor; return command; } /** * Installs accelerator keys for commands into an existing document. * * @internal * @param document An |XULDocument| within which to install the accelerator * keys. Each command's key is installed by |installKey|. * @param commands Optional. An |Array| or |Object| continaing |CommandRecord| * objects. If not specified, all commands in the * |CommandManager| are installed. */ CommandManager.prototype.installKeys = function cmgr_instkeys(document, commands) { var parentElem = document.getElementById("dynamic-keys"); if (!parentElem) { parentElem = document.createElement("keyset"); parentElem.setAttribute("id", "dynamic-keys"); document.documentElement.appendChild(parentElem); } if (!commands) commands = this.commands; for (var c in commands) this.installKey (parentElem, commands[c]); } /** * Installs the accelerator key for a single command. * * This creates a XUL element inside |parentElem|. It should usually be * called once per command, per document, so that accelerator keys work in all * application windows. * * @internal * @param parentElem An |XULElement| to add the too. * @param command The |CommandRecord| to install. */ CommandManager.prototype.installKey = function cmgr_instkey(parentElem, command) { if (!command.keystr) return; var ary = command.keystr.match (/(.*\s)?([\S]+)$/); if (!ASSERT(ary, "couldn't parse key string ``" + command.keystr + "'' for command ``" + command.name + "''")) { return; } var key = document.createElement ("key"); key.setAttribute ("id", "key:" + command.name); key.setAttribute ("oncommand", "dispatch('" + command.name + "', {isInteractive: true, source: 'keyboard'});"); if (ary[1]) key.setAttribute ("modifiers", ary[1]); if (ary[2].indexOf("VK_") == 0) key.setAttribute ("keycode", ary[2]); else key.setAttribute ("key", ary[2]); parentElem.appendChild(key); command.keyNodes.push(key); } /** * Uninstalls accelerator keys for commands from a document. * * @internal * @param commands Optional. An |Array| or |Object| continaing |CommandRecord| * objects. If not specified, all commands in the * |CommandManager| are uninstalled. */ CommandManager.prototype.uninstallKeys = function cmgr_uninstkeys(commands) { if (!commands) commands = this.commands; for (var c in commands) this.uninstallKey (commands[c]); } /** * Uninstalls the accelerator key for a single command. * * @internal * @param command The |CommandRecord| to uninstall. */ CommandManager.prototype.uninstallKey = function cmgr_uninstkey(command) { for (var i in command.keyNodes) { try { /* document may no longer exist in a useful state. */ command.keyNodes[i].parentNode.removeChild(command.keyNodes[i]); } catch (ex) { dd ("*** caught exception uninstalling key node: " + ex); } } } /** * Use |defineCommand|. * * @internal * @param command The |CommandRecord| to add to the |CommandManager|. */ CommandManager.prototype.addCommand = function cmgr_add(command) { if (objectContains(this.commands, command.name)) { /* We've already got a command with this name - invoke the history * storage so that we can undo this back to its original state. */ if (!objectContains(this.commandHistory, command.name)) this.commandHistory[command.name] = new Array(); this.commandHistory[command.name].push(this.commands[command.name]); } this.commands[command.name] = command; } /** * Removes multiple commands in a single call. * * @param cmdary An |Array| or |Object| containing |CommandRecord| objects. * Ideally use the value returned from |defineCommands|. */ CommandManager.prototype.removeCommands = function cmgr_removes(cmdary) { for (var i in cmdary) { var command = isinstance(cmdary[i], Array) ? {name: cmdary[i][0]} : cmdary[i]; this.removeCommand(command); } } /** * Removes a single command. * * @param command The |CommandRecord| to remove from the |CommandManager|. * Ideally use the value returned from |defineCommand|. */ CommandManager.prototype.removeCommand = function cmgr_remove(command) { delete this.commands[command.name]; if (objectContains(this.commandHistory, command.name)) { /* There was a previous command with this name - restore the most * recent from the history, returning the command to its former glory. */ this.commands[command.name] = this.commandHistory[command.name].pop(); if (this.commandHistory[command.name].length == 0) delete this.commandHistory[command.name]; } } /** * Registers a hook for a particular command. * * A command hook is uniquely identified by the pair |id|, |before|; only a * single hook may exist for a given pair of |id| and |before| values. It is * wise to use a unique |id|; plugins should construct an |id| using * |plugin.id|, e.g. |plugin.id + "-my-hook-1"|. * * @param commandName A |String| command name to hook. The command named must * already exist in the |CommandManager|; if it does not, no * hook is added. * @param func A |Function| to handle the hook. * @param id A |String| identifier for the hook. * @param before A |Boolean| indicating whether the hook wishes to be * called before or after the command executes. */ CommandManager.prototype.addHook = function cmgr_hook (commandName, func, id, before) { if (!ASSERT(objectContains(this.commands, commandName), "Unknown command '" + commandName + "'")) { return; } var command = this.commands[commandName]; if (before) { if (!("beforeHooks" in command)) command.beforeHooks = new Object(); command.beforeHooks[id] = func; } else { if (!("afterHooks" in command)) command.afterHooks = new Object(); command.afterHooks[id] = func; } } /** * Registers multiple hooks for commands. * * @param hooks An |Object| containing |Function| objects to call for each * hook; the key of each item is the name of the command it * wishes to hook. Optionally, the |_before| property can be * added to a |function| to override the default |before| value * of |false|. * @param prefix Optional. A |String| prefix to apply to each hook's command * name to compute an |id| for it. */ CommandManager.prototype.addHooks = function cmgr_hooks (hooks, prefix) { if (!prefix) prefix = ""; for (var h in hooks) { this.addHook(h, hooks[h], prefix + ":" + h, ("_before" in hooks[h]) ? hooks[h]._before : false); } } /** * Unregisters multiple hooks for commands. * * @param hooks An |Object| identical to the one passed to |addHooks|. * @param prefix Optional. A |String| identical to the one passed to |addHooks|. */ CommandManager.prototype.removeHooks = function cmgr_remhooks (hooks, prefix) { if (!prefix) prefix = ""; for (var h in hooks) { this.removeHook(h, prefix + ":" + h, ("before" in hooks[h]) ? hooks[h].before : false); } } /** * Unregisters a hook for a particular command. * * The arguments to |removeHook| are the same as |addHook|, but without the * hook function itself. * * @param commandName The |String| command name to unhook. * @param id The |String| identifier for the hook. * @param before A |Boolean| indicating whether the hook was to be * called before or after the command executed. */ CommandManager.prototype.removeHook = function cmgr_unhook (commandName, id, before) { var command = this.commands[commandName]; if (before) delete command.beforeHooks[id]; else delete command.afterHooks[id]; } /** * Gets a sorted |Array| of |CommandRecord| objects which match. * * After filtering by |flags| (if specified), if an exact match for * |partialName| is found, only that is returned; otherwise, all commands * starting with |partialName| are returned in alphabetical order by |label|. * * @param partialName Optional. A |String| prefix to search for. * @param flags Optional. Flags to logically AND with commands. */ CommandManager.prototype.list = function cmgr_list(partialName, flags, exact) { /* returns array of command objects which look like |partialName|, or * all commands if |partialName| is not specified */ function compare (a, b) { a = a.labelstr.toLowerCase(); b = b.labelstr.toLowerCase(); if (a == b) return 0; if (a > b) return 1; return -1; } var ary = new Array(); var commandNames = keys(this.commands); for (var name of commandNames) { let command = this.commands[name]; if ((!flags || (command.flags & flags)) && (!partialName || command.name.startsWith(partialName))) { if (exact && partialName && partialName.length == command.name.length) { /* exact match */ return [command]; } ary.push(command); } } ary.sort(compare); return ary; } /** * Gets a sorted |Array| of command names which match. * * |listNames| operates identically to |list|, except that only command names * are returned, not |CommandRecord| objects. */ CommandManager.prototype.listNames = function cmgr_listnames (partialName, flags) { var cmds = this.list(partialName, flags, false); var cmdNames = new Array(); for (var c in cmds) cmdNames.push (cmds[c].name); cmdNames.sort(); return cmdNames; } /** * Internal use only. * * Called to parse the arguments stored in |e.inputData|, as properties of |e|, * for the CommandRecord stored on |e.command|. * * @params e Event object to be processed. */ // @undocumented CommandManager.prototype.parseArguments = function cmgr_parseargs (e) { var rv = this.parseArgumentsRaw(e); //dd("parseArguments '" + e.command.usage + "' " + // (rv ? "passed" : "failed") + "\n" + dumpObjectTree(e)); delete e.currentArgIndex; return rv; } /** * Internal use only. * * Don't call parseArgumentsRaw directly, use parseArguments instead. * * Parses the arguments stored in the |inputData| property of the event object, * according to the format specified by the |command| property. * * On success this method returns true, and propery names corresponding to the * argument names used in the format spec will be created on the event object. * All optional parameters will be initialized to |null| if not already present * on the event. * * On failure this method returns false and a description of the problem * will be stored in the |parseError| property of the event. * * For example... * Given the argument spec " [ ]", and given the * input string "411 foo", stored as |e.command.usage| and |e.inputData| * respectively, this method would add the following propertys to the event * object... * -name---value--notes- * e.int 411 Parsed as an integer * e.word foo Parsed as a string * e.word2 null Optional parameters not specified will be set to null. * e.word3 null If word2 had been provided, word3 would be required too. * * Each parameter is parsed by calling the function with the same name, located * in this.argTypes. The first parameter is parsed by calling the function * this.argTypes["int"], for example. This function is expected to act on * e.unparsedData, taking it's chunk, and leaving the rest of the string. * The default parse functions are... * parses contiguous non-space characters. * parses as an int. * parses to the end of input data. * parses yes, on, true, 1, 0, false, off, no as a boolean. * parses like a , except allows "toggle" as well. * <...> parses according to the parameter type before it, until the end * of the input data. Results are stored in an array named * paramnameList, where paramname is the name of the parameter * before <...>. The value of the parameter before this will be * paramnameList[0]. * * If there is no parse function for an argument type, "word" will be used by * default. You can alias argument types with code like... * commandManager.argTypes["my-integer-name"] = commandManager.argTypes["int"]; */ // @undocumented CommandManager.prototype.parseArgumentsRaw = function parse_parseargsraw (e) { var argc = e.command.argNames.length; function initOptionals() { for (var i = 0; i < argc; ++i) { if (e.command.argNames[i] != ":" && e.command.argNames[i] != "..." && !(e.command.argNames[i] in e)) { e[e.command.argNames[i]] = null; } if (e.command.argNames[i] == "...") { var paramName = e.command.argNames[i - 1]; if (paramName == ":") paramName = e.command.argNames[i - 2]; var listName = paramName + "List"; if (!(listName in e)) e[listName] = [ e[paramName] ]; } } } if ("inputData" in e && e.inputData) { /* if data has been provided, parse it */ e.unparsedData = e.inputData; var parseResult; var currentArg; e.currentArgIndex = 0; if (argc) { currentArg = e.command.argNames[e.currentArgIndex]; while (e.unparsedData) { if (currentArg != ":") { if (!this.parseArgument (e, currentArg)) return false; } if (++e.currentArgIndex < argc) currentArg = e.command.argNames[e.currentArgIndex]; else break; } if (e.currentArgIndex < argc && currentArg != ":") { /* parse loop completed because it ran out of data. We haven't * parsed all of the declared arguments, and we're not stopped * at an optional marker, so we must be missing something * required... */ e.parseError = getMsg(MSG_ERR_REQUIRED_PARAM, e.command.argNames[e.currentArgIndex]); return false; } } if (e.unparsedData) { /* parse loop completed with unparsed data, which means we've * successfully parsed all arguments declared. Whine about the * extra data... */ display (getMsg(MSG_EXTRA_PARAMS, e.unparsedData), MT_WARN); } } var rv = this.isCommandSatisfied(e); if (rv) initOptionals(); return rv; } /** * Returns true if |e| has the properties required to call the command * |command|. * * If |command| is not provided, |e.command| is used instead. * * @param e Event object to test against the command. * @param command Command to test. */ // @undocumented CommandManager.prototype.isCommandSatisfied = function cmgr_isok (e, command) { if (typeof command == "undefined") command = e.command; else if (typeof command == "string") command = this.commands[command]; if (!command.enabled) return false; for (var i = 0; i < command.argNames.length; ++i) { if (command.argNames[i] == ":") return true; if (!(command.argNames[i] in e)) { e.parseError = getMsg(MSG_ERR_REQUIRED_PARAM, command.argNames[i]); //dd("command '" + command.name + "' unsatisfied: " + e.parseError); return false; } } //dd ("command '" + command.name + "' satisfied."); return true; } /** * Internal use only. * See parseArguments above and the |argTypes| object below. * * Parses the next argument by calling an appropriate parser function, or the * generic "word" parser if none other is found. * * @param e event object. * @param name property name to use for the parse result. */ // @undocumented CommandManager.prototype.parseArgument = function cmgr_parsearg (e, name) { var parseResult; if (name in this.argTypes) parseResult = this.argTypes[name](e, name, this); else parseResult = this.argTypes["word"](e, name, this); if (!parseResult) e.parseError = getMsg(MSG_ERR_INVALID_PARAM, [name, e.unparsedData]); return parseResult; } // @undocumented CommandManager.prototype.argTypes = new Object(); /** * Convenience function used to map a list of new types to an existing parse * function. */ // @undocumented CommandManager.prototype.argTypes.__aliasTypes__ = function at_alias (list, type) { for (var i in list) { this[list[i]] = this[type]; } } /** * Internal use only. * * Parses an integer, stores result in |e[name]|. */ // @undocumented CommandManager.prototype.argTypes["int"] = function parse_int (e, name) { var ary = e.unparsedData.match (/(\d+)(?:\s+(.*))?$/); if (!ary) return false; e[name] = Number(ary[1]); e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : ""; return true; } /** * Internal use only. * * Parses a word, which is defined as a list of nonspace characters. * * Stores result in |e[name]|. */ // @undocumented CommandManager.prototype.argTypes["word"] = function parse_word (e, name) { var ary = e.unparsedData.match (/(\S+)(?:\s+(.*))?$/); if (!ary) return false; e[name] = ary[1]; e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : ""; return true; } /** * Internal use only. * * Parses a "state" which can be "true", "on", "yes", or 1 to indicate |true|, * or "false", "off", "no", or 0 to indicate |false|. * * Stores result in |e[name]|. */ // @undocumented CommandManager.prototype.argTypes["state"] = function parse_state (e, name) { var ary = e.unparsedData.match (/(true|on|yes|1|false|off|no|0)(?:\s+(.*))?$/i); if (!ary) return false; if (ary[1].search(/true|on|yes|1/i) != -1) e[name] = true; else e[name] = false; e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : ""; return true; } /** * Internal use only. * * Parses a "toggle" which can be "true", "on", "yes", or 1 to indicate |true|, * or "false", "off", "no", or 0 to indicate |false|. In addition, the string * "toggle" is accepted, in which case |e[name]| will be the string "toggle". * * Stores result in |e[name]|. */ // @undocumented CommandManager.prototype.argTypes["toggle"] = function parse_toggle (e, name) { var ary = e.unparsedData.match (/(toggle|true|on|yes|1|false|off|no|0)(?:\s+(.*))?$/i); if (!ary) return false; if (ary[1].search(/toggle/i) != -1) e[name] = "toggle"; else if (ary[1].search(/true|on|yes|1/i) != -1) e[name] = true; else e[name] = false; e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : ""; return true; } /** * Internal use only. * * Returns all unparsed data to the end of the line. * * Stores result in |e[name]|. */ // @undocumented CommandManager.prototype.argTypes["rest"] = function parse_rest (e, name) { e[name] = e.unparsedData; e.unparsedData = ""; return true; } /** * Internal use only. * * Parses the rest of the unparsed data the same way the previous argument was * parsed. Can't be used as the first parameter. if |name| is "..." then the * name of the previous argument, plus the suffix "List" will be used instead. * * Stores result in |e[name]| or |e[lastName + "List"]|. */ // @undocumented CommandManager.prototype.argTypes["..."] = function parse_repeat (e, name, cm) { ASSERT (e.currentArgIndex > 0, "<...> can't be the first argument."); var lastArg = e.command.argNames[e.currentArgIndex - 1]; if (lastArg == ":") lastArg = e.command.argNames[e.currentArgIndex - 2]; var listName = lastArg + "List"; e[listName] = [ e[lastArg] ]; while (e.unparsedData) { if (!cm.parseArgument(e, lastArg)) return false; e[listName].push(e[lastArg]); } e[lastArg] = e[listName][0]; return true; }