459 lines
15 KiB
JavaScript
459 lines
15 KiB
JavaScript
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Skywriter.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2009
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Skywriter Team (skywriter@mozilla.com)
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
define(function(request, exports, module) {
|
|
|
|
var console = require('pilot/console');
|
|
var Trace = require('pilot/stacktrace').Trace;
|
|
var keyutil = require('pilot/keyboard/keyutil');
|
|
var history = require('canon/history');
|
|
var Request = require('canon/request').Request;
|
|
var env = require('environment').env;
|
|
|
|
exports.keymappings = {};
|
|
|
|
exports.addKeymapping = function(mapping) {
|
|
exports.keymappings[mapping.name] = mapping;
|
|
};
|
|
|
|
exports.removeKeymapping = function(name) {
|
|
delete exports.keymapping[name];
|
|
};
|
|
|
|
exports.startup = function(data, reason) {
|
|
var settings = data.env.settings;
|
|
// TODO register this
|
|
// catalog.addExtensionSpec("keymapping", {
|
|
// "description": "A keymapping defines how keystrokes are interpreted.",
|
|
// "params": [
|
|
// {
|
|
// "name": "states",
|
|
// "required": true,
|
|
// "description":
|
|
// "Holds the states and all the informations about the keymapping. See docs: pluginguide/keymapping"
|
|
// }
|
|
// ]
|
|
// });
|
|
settings.settingChange.add({
|
|
match: "customKeymapping",
|
|
ref: exports.keyboardManager,
|
|
func: exports.keyboardManager._customKeymappingChanged
|
|
.bind(exports.keyboardManager)
|
|
});
|
|
};
|
|
|
|
exports.shutdown = function(data, reason) {
|
|
var settings = data.env.settings;
|
|
settings.settingChange.remove(exports.keyboardManager);
|
|
};
|
|
|
|
|
|
/*
|
|
* Things to do to sanitize this code:
|
|
* - 'no command' is a bizarre special value at the very least it should be a
|
|
* constant to make typos more obvious, but it would be better to refactor
|
|
* so that a natural value like null worked.
|
|
* - sender seems to be totally customized to the editor case, and the functions
|
|
* that we assume that it has make no sense for the commandLine case. We
|
|
* should either document and implement the same function set for both cases
|
|
* or admit that the cases are different enough to have separate
|
|
* implementations.
|
|
* - remove remaining sproutcore-isms
|
|
* - fold buildFlags into processKeyEvent or something better, preferably the
|
|
* latter. We don't want the environment to become a singleton
|
|
*/
|
|
|
|
/**
|
|
* Every time we call processKeyEvent, we pass in some flags that require the
|
|
* same processing to set them up. This function can be called to do that
|
|
* setup.
|
|
* @param env Probably environment.env
|
|
* @param flags Probably {} (but check other places where this is called)
|
|
*/
|
|
exports.buildFlags = function(flags) {
|
|
flags.context = env.contexts[0];
|
|
return flags;
|
|
};
|
|
|
|
/**
|
|
* The canon, or the repository of commands, contains functions to process
|
|
* events and dispatch command messages to targets.
|
|
* @class
|
|
*/
|
|
var KeyboardManager = function() { };
|
|
|
|
KeyboardManager.prototype = {
|
|
_customKeymappingCache: { states: {} },
|
|
|
|
/**
|
|
* Searches through the command canon for an event matching the given flags
|
|
* with a key equivalent matching the given SproutCore event, and, if the
|
|
* command is found, sends a message to the appropriate target.
|
|
*
|
|
* This will get a couple of upgrades in the not-too-distant future:
|
|
* 1. caching in the Canon for fast lookup based on key
|
|
* 2. there will be an extra layer in between to allow remapping via
|
|
* user preferences and keyboard mapping plugins
|
|
*
|
|
* @return True if a matching command was found, false otherwise.
|
|
*/
|
|
processKeyEvent: function(evt, sender, flags) {
|
|
// Use our modified commandCodes function to detect the meta key in
|
|
// more circumstances than SproutCore alone does.
|
|
var symbolicName = keyutil.commandCodes(evt, true)[0];
|
|
if (util.none(symbolicName)) {
|
|
return false;
|
|
}
|
|
|
|
// TODO: Maybe it should be the job of our caller to do this?
|
|
exports.buildFlags(flags);
|
|
|
|
flags.isCommandKey = true;
|
|
return this._matchCommand(symbolicName, sender, flags);
|
|
},
|
|
|
|
_matchCommand: function(symbolicName, sender, flags) {
|
|
var match = this._findCommandExtension(symbolicName, sender, flags);
|
|
if (match && match.commandExt !== 'no command') {
|
|
if (flags.isTextView) {
|
|
sender.resetKeyBuffers();
|
|
}
|
|
|
|
var commandExt = match.commandExt;
|
|
commandExt.load(function(command) {
|
|
var request = new Request({
|
|
command: command,
|
|
commandExt: commandExt
|
|
});
|
|
history.execute(match.args, request);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// 'no command' is returned if a keyevent is handled but there is no
|
|
// command executed (for example when switchting the keyboard state).
|
|
if (match && match.commandExt === 'no command') {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
_buildBindingsRegex: function(bindings) {
|
|
// Escape a given Regex string.
|
|
bindings.forEach(function(binding) {
|
|
if (!util.none(binding.key)) {
|
|
binding.key = new RegExp('^' + binding.key + '$');
|
|
} else if (Array.isArray(binding.regex)) {
|
|
binding.key = new RegExp('^' + binding.regex[1] + '$');
|
|
binding.regex = new RegExp(binding.regex.join('') + '$');
|
|
} else {
|
|
binding.regex = new RegExp(binding.regex + '$');
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Build the RegExp from the keymapping as RegExp can't stored directly
|
|
* in the metadata JSON and as the RegExp used to match the keys/buffer
|
|
* need to be adapted.
|
|
*/
|
|
_buildKeymappingRegex: function(keymapping) {
|
|
for (state in keymapping.states) {
|
|
this._buildBindingsRegex(keymapping.states[state]);
|
|
}
|
|
keymapping._convertedRegExp = true;
|
|
},
|
|
|
|
/**
|
|
* Loop through the commands in the canon, looking for something that
|
|
* matches according to #_commandMatches, and return that.
|
|
*/
|
|
_findCommandExtension: function(symbolicName, sender, flags) {
|
|
// If the flags indicate that we handle the textView's input then take
|
|
// a look at keymappings as well.
|
|
if (flags.isTextView) {
|
|
var currentState = sender._keyState;
|
|
|
|
// Don't add the symbolic name to the key buffer if the alt_ key is
|
|
// part of the symbolic name. If it starts with alt_, this means
|
|
// that the user hit an alt keycombo and there will be a single,
|
|
// new character detected after this event, which then will be
|
|
// added to the buffer (e.g. alt_j will result in ∆).
|
|
if (!flags.isCommandKey || symbolicName.indexOf('alt_') === -1) {
|
|
sender._keyBuffer +=
|
|
symbolicName.replace(/ctrl_meta|meta/,'ctrl');
|
|
sender._keyMetaBuffer += symbolicName;
|
|
}
|
|
|
|
// List of all the keymappings to look at.
|
|
var ak = [ this._customKeymappingCache ];
|
|
|
|
// Get keymapping extension points.
|
|
ak = ak.concat(catalog.getExtensions('keymapping'));
|
|
|
|
for (var i = 0; i < ak.length; i++) {
|
|
// Check if the keymapping has the current state.
|
|
if (util.none(ak[i].states[currentState])) {
|
|
continue;
|
|
}
|
|
|
|
if (util.none(ak[i]._convertedRegExp)) {
|
|
this._buildKeymappingRegex(ak[i]);
|
|
}
|
|
|
|
// Try to match the current mapping.
|
|
var result = this._bindingsMatch(
|
|
symbolicName,
|
|
flags,
|
|
sender,
|
|
ak[i]);
|
|
|
|
if (!util.none(result)) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
var commandExts = catalog.getExtensions('command');
|
|
var reply = null;
|
|
var args = {};
|
|
|
|
symbolicName = symbolicName.replace(/ctrl_meta|meta/,'ctrl');
|
|
|
|
commandExts.some(function(commandExt) {
|
|
if (this._commandMatches(commandExt, symbolicName, flags)) {
|
|
reply = commandExt;
|
|
return true;
|
|
}
|
|
return false;
|
|
}.bind(this));
|
|
|
|
return util.none(reply) ? null : { commandExt: reply, args: args };
|
|
},
|
|
|
|
|
|
/**
|
|
* Checks if the given parameters fit to one binding in the given bindings.
|
|
* Returns the command and arguments if a command was matched.
|
|
*/
|
|
_bindingsMatch: function(symbolicName, flags, sender, keymapping) {
|
|
var match;
|
|
var commandExt = null;
|
|
var args = {};
|
|
var bufferToUse;
|
|
|
|
if (!util.none(keymapping.hasMetaKey)) {
|
|
bufferToUse = sender._keyBuffer;
|
|
} else {
|
|
bufferToUse = sender._keyMetaBuffer;
|
|
}
|
|
|
|
// Add the alt_key to the buffer as we don't want it to be in the buffer
|
|
// that is saved but for matching, it needs to be there.
|
|
if (symbolicName.indexOf('alt_') === 0 && flags.isCommandKey) {
|
|
bufferToUse += symbolicName;
|
|
}
|
|
|
|
// Loop over all the bindings of the keymapp until a match is found.
|
|
keymapping.states[sender._keyState].some(function(binding) {
|
|
// Check if the key matches.
|
|
if (binding.key && !binding.key.test(symbolicName)) {
|
|
return false;
|
|
}
|
|
|
|
// Check if the regex matches.
|
|
if (binding.regex && !(match = binding.regex.exec(bufferToUse))) {
|
|
return false;
|
|
}
|
|
|
|
// Check for disallowed matches.
|
|
if (binding.disallowMatches) {
|
|
for (var i = 0; i < binding.disallowMatches.length; i++) {
|
|
if (!!match[binding.disallowMatches[i]]) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check predicates.
|
|
if (!exports.flagsMatch(binding.predicates, flags)) {
|
|
return false;
|
|
}
|
|
|
|
// If there is a command to execute, then figure out the
|
|
// comand and the arguments.
|
|
if (binding.exec) {
|
|
// Get the command.
|
|
commandExt = catalog.getExtensionByKey('command', binding.exec);
|
|
if (util.none(commandExt)) {
|
|
throw new Error('Can\'t find command ' + binding.exec +
|
|
' in state=' + sender._keyState +
|
|
', symbolicName=' + symbolicName);
|
|
}
|
|
|
|
// Bulid the arguments.
|
|
if (binding.params) {
|
|
var value;
|
|
binding.params.forEach(function(param) {
|
|
if (!util.none(param.match) && !util.none(match)) {
|
|
value = match[param.match] || param.defaultValue;
|
|
} else {
|
|
value = param.defaultValue;
|
|
}
|
|
|
|
if (param.type === 'number') {
|
|
value = parseInt(value, 10);
|
|
}
|
|
|
|
args[param.name] = value;
|
|
});
|
|
}
|
|
sender.resetKeyBuffers();
|
|
}
|
|
|
|
// Handle the 'then' property.
|
|
if (binding.then) {
|
|
sender._keyState = binding.then;
|
|
sender.resetKeyBuffers();
|
|
}
|
|
|
|
// If there is no command matched now, then return a 'false'
|
|
// command to stop matching.
|
|
if (util.none(commandExt)) {
|
|
commandExt = 'no command';
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
if (util.none(commandExt)) {
|
|
return null;
|
|
}
|
|
|
|
return { commandExt: commandExt, args: args };
|
|
},
|
|
|
|
/**
|
|
* Check that the given command fits the given key name and flags.
|
|
*/
|
|
_commandMatches: function(commandExt, symbolicName, flags) {
|
|
var mappedKeys = commandExt.key;
|
|
if (!mappedKeys) {
|
|
return false;
|
|
}
|
|
|
|
// Check predicates
|
|
if (!exports.flagsMatch(commandExt.predicates, flags)) {
|
|
return false;
|
|
}
|
|
|
|
if (typeof(mappedKeys) === 'string') {
|
|
if (mappedKeys != symbolicName) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (!Array.isArray(mappedKeys)) {
|
|
mappedKeys = [mappedKeys];
|
|
commandExt.key = mappedKeys;
|
|
}
|
|
|
|
for (var i = 0; i < mappedKeys.length; i++) {
|
|
var keymap = mappedKeys[i];
|
|
if (typeof(keymap) === 'string') {
|
|
if (keymap == symbolicName) {
|
|
return true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (keymap.key != symbolicName) {
|
|
continue;
|
|
}
|
|
|
|
return exports.flagsMatch(keymap.predicates, flags);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Build a cache of custom keymappings whenever the associated setting
|
|
* changes.
|
|
*/
|
|
_customKeymappingChanged: function(settingName, value) {
|
|
var ckc = this._customKeymappingCache =
|
|
JSON.parse(value);
|
|
|
|
ckc.states = ckc.states || {};
|
|
|
|
for (state in ckc.states) {
|
|
this._buildBindingsRegex(ckc.states[state]);
|
|
}
|
|
ckc._convertedRegExp = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
*
|
|
*/
|
|
exports.flagsMatch = function(predicates, flags) {
|
|
if (util.none(predicates)) {
|
|
return true;
|
|
}
|
|
|
|
if (!flags) {
|
|
return false;
|
|
}
|
|
|
|
for (var flagName in predicates) {
|
|
if (flags[flagName] !== predicates[flagName]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* The global exported KeyboardManager
|
|
*/
|
|
exports.keyboardManager = new KeyboardManager();
|
|
|
|
|
|
});
|