ace/plugins/pilot/lib/keyboard/index.js

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();
});