1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
11 var ProcessorStack = Class("ProcessorStack", {
12 init: function (mode, hives, builtin) {
13 this.main = mode.main;
19 let main = { __proto__: mode.main, params: mode.params };
20 let keyModes = array([mode.params.keyModes, main, mode.main.allBases]).flatten().compact();
23 hives = hives.filter(function (h) h.name === "builtin");
25 this.processors = keyModes.map(function (m) hives.map(function (h) KeyProcessor(m, h)))
27 this.ownsBuffer = !this.processors.some(function (p) p.main.ownsBuffer);
29 for (let [i, input] in Iterator(this.processors)) {
30 let params = input.main.params;
31 if (params.preExecute)
32 input.preExecute = params.preExecute;
33 if (params.postExecute)
34 input.postExecute = params.postExecute;
35 if (params.onKeyPress && input.hive === mappings.builtin)
36 input.fallthrough = function fallthrough(events) {
37 return params.onKeyPress(events) === false ? Events.KILL : Events.PASS;
41 let hive = options.get("passkeys")[this.main.input ? "inputHive" : "commandHive"];
42 if (!builtin && hive.active
43 && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)))
44 this.processors.unshift(KeyProcessor(modes.BASE, hive));
48 events.keyEvents = [];
49 events.processor = null;
50 if (!this.execute(Events.KILL, true)) {
51 events.processor = this;
52 events.keyEvents = this.keyEvents;
56 _result: function (result) (result === Events.KILL ? "KILL" :
57 result === Events.PASS ? "PASS" :
58 result === Events.PASS_THROUGH ? "PASS_THROUGH" :
59 result === Events.ABORT ? "ABORT" :
60 callable(result) ? result.toSource().substr(0, 50) : result),
62 execute: function execute(result, force) {
64 if (force && this.actions.length)
65 this.processors.length = 0;
68 statusline.inputBuffer = this.processors.length ? this.buffer : "";
70 if (!this.processors.some(function (p) !p.extended) && this.actions.length) {
71 if (this._actions.length == 0) {
73 events.feedingKeys = false;
76 for (var action in values(this.actions)) {
77 while (callable(action)) {
78 action = dactyl.trapErrors(action);
79 events.dbg("ACTION RES: " + this._result(action));
81 if (action !== Events.PASS)
85 result = action !== undefined ? action : Events.KILL;
86 if (action !== Events.PASS)
87 this.processors.length = 0;
89 else if (this.processors.length) {
91 if (this.actions.length && options["timeout"])
92 this.timer = services.Timer(this, options["timeoutlen"], services.Timer.TYPE_ONE_SHOT);
94 else if (result !== Events.KILL && !this.actions.length &&
95 (this.events.length > 1 ||
96 this.processors.some(function (p) !p.main.passUnknown))) {
98 if (!Events.isEscape(this.events.slice(-1)[0]))
100 events.feedingKeys = false;
102 else if (result === undefined)
103 result = Events.PASS;
105 events.dbg("RESULT: " + this._result(result));
107 if (result === Events.PASS || result === Events.PASS_THROUGH)
108 if (this.events[0].originalTarget)
109 this.events[0].originalTarget.dactylKeyPress = undefined;
111 if (result !== Events.PASS || this.events.length > 1)
112 Events.kill(this.events[this.events.length - 1]);
114 if (result === Events.PASS_THROUGH)
115 events.feedevents(null, this.keyEvents, { skipmap: true, isMacro: true, isReplay: true });
116 else if (result === Events.PASS || result === Events.ABORT) {
117 let list = this.events.filter(function (e) e.getPreventDefault() && !e.dactylDefaultPrevented);
119 events.dbg("REFEED: " + list.map(events.closure.toString).join(""));
120 events.feedevents(null, list, { skipmap: true, isMacro: true, isReplay: true });
123 return this.processors.length === 0;
126 process: function process(event) {
130 let key = events.toString(event);
131 this.events.push(event);
133 this.keyEvents.push(event);
140 events.dbg("KEY: " + key + " skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay);
142 for (let [i, input] in Iterator(this.processors)) {
143 let res = input.process(event);
144 if (res !== Events.ABORT)
147 events.dbg("RES: " + input + " " + this._result(res));
149 if (res === Events.KILL)
152 buffer = buffer || input.inputBuffer;
157 if (res === Events.WAIT || input.waiting)
158 processors.push(input);
159 if (isinstance(res, KeyProcessor))
160 processors.push(res);
163 events.dbg("RESULT: " + event.getPreventDefault() + " " + this._result(result));
164 events.dbg("ACTIONS: " + actions.length + " " + this.actions.length);
165 events.dbg("PROCESSORS:", processors);
167 this._actions = actions;
168 this.actions = actions.concat(this.actions);
170 if (result === Events.KILL)
172 else if (!this.actions.length && !processors.length)
173 for (let input in values(this.processors))
174 if (input.fallthrough) {
175 if (result === Events.KILL)
177 result = dactyl.trapErrors(input.fallthrough, input, this.events);
180 this.processors = processors;
182 return this.execute(result, options["timeout"] && options["timeoutlen"] === 0);
186 var KeyProcessor = Class("KeyProcessor", {
187 init: function init(main, hive) {
191 this.wantCount = this.main.count;
194 get toStringParams() [this.main.name, this.hive.name],
198 get count() this.countStr ? Number(this.countStr) : null,
200 append: function append(event) {
201 this.events.push(event);
202 let key = events.toString(event);
204 if (this.wantCount && !this.command &&
205 (this.countStr ? /^[0-9]$/ : /^[1-9]$/).test(key))
206 this.countStr += key;
212 process: function process(event) {
214 this.waiting = false;
215 return this.onKeyPress(event);
218 execute: function execute(map, args)
222 self.preExecute.apply(self, args);
223 let res = map.execute.call(map, update({ self: self.main.params.mappingSelf || self.main.mappingSelf || map },
225 if (self.postExecute)
226 self.postExecute.apply(self, args);
230 onKeyPress: function onKeyPress(event) {
237 var map = this.hive.get(this.main, this.command);
238 this.waiting = this.hive.getCandidates(this.main, this.command);
241 return KeyArgProcessor(this, map, false, "arg");
243 return KeyArgProcessor(this, map, true, "motion");
245 return this.execute(map, {
246 keyEvents: this.keyEvents,
247 command: this.command,
249 keypressEvents: this.events
254 return this.main.insert ? Events.PASS : Events.ABORT;
260 var KeyArgProcessor = Class("KeyArgProcessor", KeyProcessor, {
261 init: function init(input, map, wantCount, argName) {
262 init.supercall(this, input.main, input.hive);
265 this.argName = argName;
266 this.wantCount = wantCount;
271 onKeyPress: function onKeyPress(event) {
272 if (Events.isEscape(event))
278 command: this.parent.command,
279 count: this.count || this.parent.count,
280 events: this.parent.events.concat(this.events)
282 args[this.argName] = this.command;
284 return this.execute(this.map, args);
288 var EventHive = Class("EventHive", Contexts.Hive, {
289 init: function init(group) {
290 init.supercall(this, group);
291 this.sessionListeners = [];
294 cleanup: function cleanup() {
299 * Adds an event listener for this session and removes it on
302 * @param {Element} target The element on which to listen.
303 * @param {string} event The event to listen for.
304 * @param {function} callback The function to call when the event is received.
305 * @param {boolean} capture When true, listen during the capture
306 * phase, otherwise during the bubbling phase.
307 * @param {boolean} allowUntrusted When true, allow capturing of
310 listen: function (target, event, callback, capture, allowUntrusted) {
311 if (!isObject(event))
312 var [self, events] = [null, array.toObject([[event, callback]])];
314 [self, events] = [event, event[callback || "events"]];
315 [,, capture, allowUntrusted] = arguments;
318 for (let [event, callback] in Iterator(events)) {
319 let args = [Cu.getWeakReference(target),
321 this.wrapListener(callback, self),
325 target.addEventListener.apply(target, args.slice(1));
326 this.sessionListeners.push(args);
331 * Remove an event listener.
333 * @param {Element} target The element on which to listen.
334 * @param {string} event The event to listen for.
335 * @param {function} callback The function to call when the event is received.
336 * @param {boolean} capture When true, listen during the capture
337 * phase, otherwise during the bubbling phase.
339 unlisten: function (target, event, callback, capture) {
340 this.sessionListeners = this.sessionListeners.filter(function (args) {
341 if (target == null || args[0].get() == target && args[1] == event && args[2] == callback && args[3] == capture) {
342 args[0].get().removeEventListener.apply(args[0].get(), args.slice(1));
345 return !args[0].get();
353 var Events = Module("events", {
361 hives: contexts.Hives("events", EventHive),
362 user: contexts.hives.events.user,
363 builtin: contexts.hives.events.builtin
366 EventHive.prototype.wrapListener = this.closure.wrapListener;
368 XML.ignoreWhitespace = true;
369 util.overlayWindow(window, {
370 append: <e4x xmlns={XUL}>
371 <window id={document.documentElement.id}>
372 <!--this notifies us also of focus events in the XUL
373 from: http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands !-->
374 <!-- I don't think we really need this. ––Kris -->
375 <commandset id="dactyl-onfocus" commandupdater="true" events="focus"
376 oncommandupdate="dactyl.modules.events.onFocusChange(event);"/>
377 <commandset id="dactyl-onselect" commandupdater="true" events="select"
378 oncommandupdate="dactyl.modules.events.onSelectionChange(event);"/>
383 this._fullscreen = window.fullScreen;
384 this._lastFocus = null;
385 this._macroKeys = [];
386 this._lastMacro = "";
388 this._macros = storage.newMap("macros", { privateData: true, store: true });
389 for (let [k, m] in this._macros)
391 m = { keys: m, timeRecorded: Date.now() };
393 // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
394 // matters, so use that string as the first item, that you
395 // want to refer to within dactyl's source code for
396 // comparisons like if (key == "<Esc>") { ... }
398 add: ["Plus", "Add"],
402 escape: ["Esc", "Escape"],
403 insert: ["Insert", "Ins"],
405 left_shift: ["LT", "<"],
408 return: ["Return", "CR", "Enter"],
410 space: ["Space", " "],
411 subtract: ["Minus", "Subtract"]
414 this._pseudoKeys = set(["count", "leader", "nop", "pass"]);
420 for (let list in values(this._keyTable))
421 for (let v in values(list)) {
424 this._key_key[v.toLowerCase()] = v;
427 for (let [k, v] in Iterator(KeyEvent)) {
428 k = k.substr(7).toLowerCase();
429 let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
430 .replace(/^NUMPAD/, "k")];
432 if (names[0].length == 1)
433 names[0] = names[0].toLowerCase();
435 if (k in this._keyTable)
436 names = this._keyTable[k];
437 this._code_key[v] = names[0];
438 for (let [, name] in Iterator(names)) {
439 this._key_key[name.toLowerCase()] = name;
440 this._key_code[name.toLowerCase()] = v;
444 // HACK: as Gecko does not include an event for <, we must add this in manually.
445 if (!("<" in this._key_code)) {
446 this._key_code["<"] = 60;
447 this._key_code["lt"] = 60;
448 this._code_key[60] = "lt";
451 this._activeMenubar = false;
452 this.listen(window, this, "events", true);
454 dactyl.registerObserver("modeChange", function () {
455 delete self.processor;
460 "browser.locationChange": function (webProgress, request, uri) {
461 options.get("passkeys").flush();
466 * Adds an event listener for this session and removes it on
469 * @param {Element} target The element on which to listen.
470 * @param {string} event The event to listen for.
471 * @param {function} callback The function to call when the event is received.
472 * @param {boolean} capture When true, listen during the capture
473 * phase, otherwise during the bubbling phase.
475 get listen() this.builtin.closure.listen,
476 addSessionListener: deprecated("events.listen", { get: function addSessionListener() this.listen }),
479 * Wraps an event listener to ensure that errors are reported.
481 wrapListener: function wrapListener(method, self) {
483 method.wrapped = wrappedListener;
484 function wrappedListener(event) {
486 method.apply(self, arguments);
489 dactyl.reportError(e);
490 if (e.message == "Interrupted")
491 dactyl.echoerr(_("error.interrupted"), commandline.FORCE_SINGLELINE);
493 dactyl.echoerr(_("event.error", event.type, e.echoerr || e),
494 commandline.FORCE_SINGLELINE);
497 return wrappedListener;
501 * @property {boolean} Whether synthetic key events are currently being
507 * Initiates the recording of a key event macro.
509 * @param {string} macro The name for the macro.
512 get recording() this._recording,
514 set recording(macro) {
515 dactyl.assert(macro == null || /[a-zA-Z0-9]/.test(macro),
516 _("macro.invalid", macro));
518 modes.recording = !!macro;
520 if (/[A-Z]/.test(macro)) { // uppercase (append)
521 macro = macro.toLowerCase();
522 this._macroKeys = events.fromString((this._macros.get(macro) || { keys: "" }).keys, true)
523 .map(events.closure.toString);
526 this._macroKeys = [];
529 this._macros.set(this.recording, {
530 keys: this._macroKeys.join(""),
531 timeRecorded: Date.now()
534 dactyl.log("Recorded " + this.recording + ": " + this._macroKeys.join(""), 9);
535 dactyl.echomsg(_("macro.recorded", this.recording));
537 this._recording = macro || null;
543 * @param {string} The name of the macro to replay.
546 playMacro: function (macro) {
548 dactyl.assert(/^[a-zA-Z0-9@]$/.test(macro), _("macro.invalid", macro));
551 dactyl.assert(this._lastMacro, _("macro.noPrevious"));
553 this._lastMacro = macro.toLowerCase(); // XXX: sets last played macro, even if it does not yet exist
555 if (this._macros.get(this._lastMacro)) {
557 modes.replaying = true;
558 res = events.feedkeys(this._macros.get(this._lastMacro).keys, { noremap: true });
561 modes.replaying = false;
565 // TODO: ignore this like Vim?
566 dactyl.echoerr(_("macro.noSuch", this._lastMacro));
571 * Returns all macros matching *filter*.
573 * @param {string} filter A regular expression filter string. A null
574 * filter selects all macros.
576 getMacros: function (filter) {
577 let re = RegExp(filter || "");
578 return ([k, m.keys] for ([k, m] in events._macros) if (re.test(k)));
582 * Deletes all macros matching *filter*.
584 * @param {string} filter A regular expression filter string. A null
585 * filter deletes all macros.
587 deleteMacros: function (filter) {
588 let re = RegExp(filter || "");
589 for (let [item, ] in this._macros) {
590 if (!filter || re.test(item))
591 this._macros.remove(item);
596 * Feeds a list of events to *target* or the originalTarget member
597 * of each event if *target* is null.
599 * @param {EventTarget} target The destination node for the events.
601 * @param {[Event]} list The events to dispatch.
602 * @param {object} extra Extra properties for processing by dactyl.
605 feedevents: function feedevents(target, list, extra) {
606 list.forEach(function _feedevent(event, i) {
607 let elem = target || event.originalTarget;
609 let doc = elem.ownerDocument || elem.document || elem;
610 let evt = events.create(doc, event.type, event);
611 events.dispatch(elem, evt, extra);
613 else if (i > 0 && event.type === "keypress")
614 events.events.keypress.call(events, event);
619 * Pushes keys onto the event queue from dactyl. It is similar to
620 * Vim's feedkeys() method, but cannot cope with 2 partially-fed
621 * strings, you have to feed one parseable string.
623 * @param {string} keys A string like "2<C-f>" to push onto the event
624 * queue. If you want "<" to be taken literally, prepend it with a
626 * @param {boolean} noremap Whether recursive mappings should be
628 * @param {boolean} silent Whether the command should be echoed to the
632 feedkeys: function (keys, noremap, quiet, mode) {
634 var savedEvents = this._processor && this._processor.keyEvents;
636 var wasFeeding = this.feedingKeys;
637 this.feedingKeys = true;
639 var wasQuiet = commandline.quiet;
641 commandline.quiet = quiet;
643 for (let [, evt_obj] in Iterator(events.fromString(keys))) {
644 let now = Date.now();
645 for (let type in values(["keydown", "keyup", "keypress"])) {
646 let evt = update({}, evt_obj, { type: type });
648 if (isObject(noremap))
649 update(evt, noremap);
651 evt.noremap = !!noremap;
653 evt.dactylMode = mode;
654 evt.dactylSavedEvents = savedEvents;
655 this.feedingEvent = evt;
657 let event = events.create(document.commandDispatcher.focusedWindow.document, type, evt);
658 if (!evt_obj.dactylString && !evt_obj.dactylShift && !mode)
659 events.dispatch(dactyl.focusedElement || buffer.focusedFrame, event, evt);
660 else if (type === "keypress")
661 events.events.keypress.call(events, event);
664 if (!this.feedingKeys)
672 this.feedingEvent = null;
673 this.feedingKeys = wasFeeding;
675 commandline.quiet = wasQuiet;
676 dactyl.triggerObserver("events.doneFeeding");
682 * Creates an actual event from a pseudo-event object.
684 * The pseudo-event object (such as may be retrieved from events.fromString)
685 * should have any properties you want the event to have.
687 * @param {Document} doc The DOM document to associate this event with
688 * @param {Type} type The type of event (keypress, click, etc.)
689 * @param {Object} opts The pseudo-event. @optional
691 create: function (doc, type, opts) {
695 type: type, bubbles: true, cancelable: false
699 bubbles: true, cancelable: true,
700 view: doc.defaultView,
701 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
702 keyCode: 0, charCode: 0
706 bubbles: true, cancelable: true,
707 view: doc.defaultView,
709 screenX: 0, screenY: 0,
710 clientX: 0, clientY: 0,
711 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
717 change: "", input: "", submit: "",
718 click: "Mouse", mousedown: "Mouse", mouseup: "Mouse",
719 mouseover: "Mouse", mouseout: "Mouse",
720 keypress: "Key", keyup: "Key", keydown: "Key"
723 var evt = doc.createEvent((t || "HTML") + "Events");
725 let defaults = DEFAULTS[t || "HTML"];
726 evt["init" + t + "Event"].apply(evt, Object.keys(defaults)
727 .map(function (k) k in opts ? opts[k]
733 * Converts a user-input string of keys into a canonical
736 * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
737 * <C- > maps to <C-Space>, <S-a> maps to A
738 * << maps to <lt><lt>
740 * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
743 * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
746 * @param {string} keys Messy form.
747 * @param {boolean} unknownOk Whether unknown keys are passed
748 * through rather than being converted to <lt>keyname>.
750 * @returns {string} Canonical form.
752 canonicalKeys: function (keys, unknownOk) {
753 if (arguments.length === 1)
755 return events.fromString(keys, unknownOk).map(events.closure.toString).join("");
758 iterKeys: function (keys) {
759 let match, re = /<.*?>?>|[^<]/g;
760 while (match = re.exec(keys))
765 * Dispatches an event to an element as if it were a native event.
767 * @param {Node} target The DOM node to which to dispatch the event.
768 * @param {Event} event The event to dispatch.
770 dispatch: Class.memoize(function ()
772 ? function dispatch(target, event, extra) {
774 this.feedingEvent = extra;
775 if (target instanceof Element)
776 // This causes a crash on Gecko<2.0, it seems.
777 return (target.ownerDocument || target.document || target).defaultView
778 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
779 .dispatchDOMEventViaPresShell(target, event, true);
781 target.dispatchEvent(event);
782 return !event.getPreventDefault();
789 this.feedingEvent = null;
792 : function dispatch(target, event, extra) {
794 this.feedingEvent = extra;
795 target.dispatchEvent(update(event, extra));
798 this.feedingEvent = null;
802 get defaultTarget() dactyl.focusedElement || content.document.body || document.documentElement,
805 * Converts an event string into an array of pseudo-event objects.
807 * These objects can be used as arguments to events.toString or
808 * events.create, though they are unlikely to be much use for other
809 * purposes. They have many of the properties you'd expect to find on a
810 * real event, but none of the methods.
812 * Also may contain two "special" parameters, .dactylString and
813 * .dactylShift these are set for characters that can never by
814 * typed, but may appear in mappings, for example <Nop> is passed as
815 * dactylString, and dactylShift is set when a user specifies
816 * <S-@> where @ is a non-case-changeable, non-space character.
818 * @param {string} keys The string to parse.
819 * @param {boolean} unknownOk Whether unknown keys are passed
820 * through rather than being converted to <lt>keyname>.
822 * @returns {Array[Object]}
824 fromString: function (input, unknownOk) {
826 if (arguments.length === 1)
830 for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
831 let evt_str = match[0];
832 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
833 keyCode: 0, charCode: 0, type: "keypress" };
835 if (evt_str.length > 1) { // <.*?>
836 let [match, modifier, keyname] = evt_str.match(/^<((?:[CSMA]-)*)(.+?)>$/i) || [false, '', ''];
837 modifier = modifier.toUpperCase();
838 keyname = keyname.toLowerCase();
839 evt_obj.dactylKeyname = keyname;
840 if (/^u[0-9a-f]+$/.test(keyname))
841 keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
843 if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
844 this._key_code[keyname] || set.has(this._pseudoKeys, keyname))) {
845 evt_obj.ctrlKey = /C-/.test(modifier);
846 evt_obj.altKey = /A-/.test(modifier);
847 evt_obj.shiftKey = /S-/.test(modifier);
848 evt_obj.metaKey = /M-/.test(modifier);
850 if (keyname.length == 1) { // normal characters
851 if (evt_obj.shiftKey) {
852 keyname = keyname.toUpperCase();
853 if (keyname == keyname.toLowerCase())
854 evt_obj.dactylShift = true;
857 evt_obj.charCode = keyname.charCodeAt(0);
859 else if (set.has(this._pseudoKeys, keyname)) {
860 evt_obj.dactylString = "<" + this._key_key[keyname] + ">";
862 else if (/mouse$/.test(keyname)) { // mouse events
863 evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
864 evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
865 delete evt_obj.keyCode;
866 delete evt_obj.charCode;
868 else { // spaces, control characters, and <
869 evt_obj.keyCode = this._key_code[keyname];
870 evt_obj.charCode = 0;
873 else { // an invalid sequence starting with <, treat as a literal
874 out = out.concat(events.fromString("<lt>" + evt_str.substr(1)));
878 else // a simple key (no <...>)
879 evt_obj.charCode = evt_str.charCodeAt(0);
881 // TODO: make a list of characters that need keyCode and charCode somewhere
882 if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
883 evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
884 if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
885 evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
887 evt_obj.modifiers = (evt_obj.ctrlKey && Ci.nsIDOMNSEvent.CONTROL_MASK)
888 | (evt_obj.altKey && Ci.nsIDOMNSEvent.ALT_MASK)
889 | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
890 | (evt_obj.metaKey && Ci.nsIDOMNSEvent.META_MASK);
898 * Converts the specified event to a string in dactyl key-code
899 * notation. Returns null for an unknown event.
901 * @param {Event} event
904 toString: function toString(event) {
906 return toString.supercall(this);
908 if (event.dactylString)
909 return event.dactylString;
921 if (/^key/.test(event.type)) {
922 let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
924 if (event.keyCode in this._code_key) {
925 key = this._code_key[event.keyCode];
927 if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
929 else if (!modifier && key.length === 1)
931 key = key.toUpperCase();
933 key = key.toLowerCase();
934 if (!modifier && /^[a-z0-9]$/i.test(key))
938 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
939 // (i.e., cntrl codes 27--31)
941 // For more information, see:
942 // [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
943 // [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
944 // https://bugzilla.mozilla.org/show_bug.cgi?id=416227
945 // [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
946 // https://bugzilla.mozilla.org/show_bug.cgi?id=432951
949 // The following fixes are only activated if util.OS.isMacOSX.
950 // Technically, they prevent mappings from <C-Esc> (and
951 // <C-C-]> if your fancy keyboard permits such things<?>), but
952 // these <C-control> mappings are probably pathological (<C-Esc>
953 // certainly is on Windows), and so it is probably
954 // harmless to remove the util.OS.isMacOSX if desired.
956 else if (util.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
957 if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
959 modifier = modifier.replace("C-", "");
961 else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
962 key = String.fromCharCode(charCode + 64);
964 // a normal key like a, b, c, 0, etc.
965 else if (charCode > 0) {
966 key = String.fromCharCode(charCode);
968 if (!/^[a-z0-9]$/i.test(key) && key in this._key_code) {
969 // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
970 if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
973 key = this._code_key[this._key_code[key]];
976 // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
977 // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
978 if (key != key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
980 if (/^\s$/.test(key))
981 key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
982 else if (modifier.length == 0)
987 key = this._key_key[event.dactylKeyname] || event.dactylKeyname;
991 else if (event.type == "click" || event.type == "dblclick") {
994 if (event.type == "dblclick")
996 // TODO: triple and quadruple click
998 switch (event.button) {
1003 key = "MiddleMouse";
1014 return "<" + modifier + key + ">";
1018 * Whether *key* is a key code defined to accept/execute input on the
1021 * @param {string} key The key code to test.
1022 * @returns {boolean}
1024 isAcceptKey: function (key) key == "<Return>" || key == "<C-j>" || key == "<C-m>",
1027 * Whether *key* is a key code defined to reject/cancel input on the
1030 * @param {string} key The key code to test.
1031 * @returns {boolean}
1033 isCancelKey: function (key) key == "<Esc>" || key == "<C-[>" || key == "<C-c>",
1035 isContentNode: function isContentNode(node) {
1036 let win = (node.ownerDocument || node).defaultView || node;
1037 return XPCNativeWrapper(win).top == content;
1041 * Waits for the current buffer to successfully finish loading. Returns
1042 * true for a successful page load otherwise false.
1044 * @returns {boolean}
1046 waitForPageLoad: function (time) {
1050 dactyl.echo(_("macro.loadWaiting"), commandline.DISALLOW_MULTILINE);
1052 const maxWaitTime = (time || 25);
1053 util.waitFor(function () !events.feedingKeys || buffer.loaded, this, maxWaitTime * 1000, true);
1056 dactyl.echoerr(_("macro.loadFailed", maxWaitTime));
1058 return buffer.loaded;
1062 * Ensures that the currently focused element is visible and blurs
1065 checkFocus: function () {
1066 if (dactyl.focusedElement) {
1067 let rect = dactyl.focusedElement.getBoundingClientRect();
1068 if (!rect.width || !rect.height) {
1069 services.focus.clearFocus(window);
1070 document.commandDispatcher.focusedWindow = content;
1071 // onFocusChange needs to die.
1072 this.onFocusChange();
1078 DOMMenuBarActive: function () {
1079 this._activeMenubar = true;
1080 if (modes.main != modes.MENU)
1081 modes.push(modes.MENU);
1084 DOMMenuBarInactive: function () {
1085 this._activeMenubar = false;
1086 modes.remove(modes.MENU, true);
1089 blur: function onBlur(event) {
1090 let elem = event.originalTarget;
1091 if (elem instanceof Window && services.focus.activeWindow == null
1092 && document.commandDispatcher.focusedWindow !== window) {
1093 // Deals with circumstances where, after the main window
1094 // blurs while a collapsed frame has focus, re-activating
1095 // the main window does not restore focus and we lose key
1097 services.focus.clearFocus(window);
1098 document.commandDispatcher.focusedWindow = Editor.getEditor(content) ? window : content;
1101 let hold = modes.topOfStack.params.holdFocus;
1104 this.timeout(function () { dactyl.focus(hold); });
1108 // TODO: Merge with onFocusChange
1109 focus: function onFocus(event) {
1110 let elem = event.originalTarget;
1112 if (event.target instanceof Ci.nsIDOMXULTextBoxElement)
1113 if (Events.isHidden(elem, true))
1116 let win = (elem.ownerDocument || elem).defaultView || elem;
1118 if (events.isContentNode(elem) && !buffer.focusAllowed(elem)
1119 && !(services.focus.getLastFocusMethod(win) & 0x7000)
1120 && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window])) {
1121 if (elem.frameElement)
1122 dactyl.focusContent(true);
1123 else if (!(elem instanceof Window) || Editor.getEditor(elem))
1124 dactyl.focus(window);
1129 onFocus: function onFocus(event) {
1130 let elem = event.originalTarget;
1131 if (!(elem instanceof Element))
1133 let win = elem.ownerDocument.defaultView;
1136 util.dump(elem, services.focus.getLastFocusMethod(win) & (0x7000));
1137 if (buffer.focusAllowed(win))
1138 win.dactylLastFocus = elem;
1139 else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement])) {
1140 if (win.dactylLastFocus)
1141 dactyl.focus(win.dactylLastFocus);
1147 util.dump(win, String(elem.ownerDocument), String(elem.ownerDocument && elem.ownerDocument.defaultView));
1148 util.reportError(e);
1153 input: function onInput(event) {
1154 event.originalTarget.dactylKeyPress = undefined;
1157 // this keypress handler gets always called first, even if e.g.
1158 // the command-line has focus
1159 // TODO: ...help me...please...
1160 keypress: function onKeyPress(event) {
1161 event.dactylDefaultPrevented = event.getPreventDefault();
1163 let duringFeed = this.duringFeed || [];
1164 this.duringFeed = [];
1166 if (this.feedingEvent)
1167 for (let [k, v] in Iterator(this.feedingEvent))
1170 this.feedingEvent = null;
1172 let key = events.toString(event);
1174 // Hack to deal with <BS> and so forth not dispatching input
1176 if (key && event.originalTarget instanceof HTMLInputElement && !modes.main.passthrough) {
1177 let elem = event.originalTarget;
1178 elem.dactylKeyPress = elem.value;
1179 util.timeout(function () {
1180 if (elem.dactylKeyPress !== undefined && elem.value !== elem.dactylKeyPress)
1181 events.dispatch(elem, events.create(elem.ownerDocument, "input"));
1182 elem.dactylKeyPress = undefined;
1189 if (modes.recording && !event.isReplay)
1190 events._macroKeys.push(key);
1192 // feedingKeys needs to be separate from interrupted so
1193 // we can differentiate between a recorded <C-c>
1194 // interrupting whatever it's started and a real <C-c>
1195 // interrupting our playback.
1196 if (events.feedingKeys && !event.isMacro) {
1197 if (key == "<C-c>") {
1198 events.feedingKeys = false;
1199 if (modes.replaying) {
1200 modes.replaying = false;
1201 this.timeout(function () { dactyl.echomsg(_("macro.canceled", this._lastMacro)); }, 100);
1205 duringFeed.push(event);
1207 return Events.kill(event);
1210 if (!this.processor) {
1211 let mode = modes.getStack(0);
1212 if (event.dactylMode)
1213 mode = Modes.StackElement(event.dactylMode);
1217 if (modes.main == modes.PASS_THROUGH)
1218 ignore = !Events.isEscape(key) && key != "<C-v>";
1219 else if (modes.main == modes.QUOTE) {
1220 if (modes.getStack(1).main == modes.PASS_THROUGH) {
1221 mode.params.mainMode = modes.getStack(2).main;
1222 ignore = Events.isEscape(key);
1224 else if (events.shouldPass(event))
1225 mode.params.mainMode = modes.getStack(1).main;
1229 if (ignore && !Events.isEscape(key))
1232 else if (!event.isMacro && !event.noremap && events.shouldPass(event))
1236 events.dbg("ON KEYPRESS " + key + " ignore: " + ignore,
1237 event.originalTarget instanceof Element ? event.originalTarget : String(event.originalTarget));
1242 // FIXME: Why is this hard coded? --Kris
1244 util.interrupted = true;
1246 this.processor = ProcessorStack(mode, mappings.hives.array, event.noremap);
1247 this.processor.keyEvents = this.keyEvents;
1250 let { keyEvents, processor } = this;
1251 this._processor = processor;
1252 this.processor = null;
1253 this.keyEvents = [];
1255 if (!processor.process(event)) {
1256 this.keyEvents = keyEvents;
1257 this.processor = processor;
1262 dactyl.reportError(e);
1265 [duringFeed, this.duringFeed] = [this.duringFeed, duringFeed];
1266 if (this.feedingKeys)
1267 this.duringFeed = this.duringFeed.concat(duringFeed);
1269 for (let event in values(duringFeed))
1271 this.dispatch(event.originalTarget, event, event);
1274 util.reportError(e);
1279 keyup: function onKeyUp(event) {
1280 this.keyEvents.push(event);
1282 let pass = this.feedingEvent && this.feedingEvent.isReplay ||
1284 modes.main == modes.PASS_THROUGH ||
1285 modes.main == modes.QUOTE
1286 && modes.getStack(1).main !== modes.PASS_THROUGH
1287 && !this.shouldPass(event) ||
1288 !modes.passThrough && this.shouldPass(event);
1290 events.dbg("ON " + event.type.toUpperCase() + " " + this.toString(event) + " pass: " + pass);
1292 // Prevents certain sites from transferring focus to an input box
1293 // before we get a chance to process our key bindings on the
1294 // "keypress" event.
1295 if (!pass && !Events.isInputElement(dactyl.focusedElement))
1296 event.stopPropagation();
1298 keydown: function onKeyDown(event) {
1299 this.events.keyup.call(this, event);
1302 mousedown: function onMouseDown(event) {
1303 let elem = event.target;
1304 let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
1306 for (; win; win = win != win.parent && win.parent)
1307 win.document.dactylFocusAllowed = true;
1310 popupshown: function onPopupShown(event) {
1311 let elem = event.originalTarget;
1312 if (elem instanceof Ci.nsIAutoCompletePopup) {
1313 if (modes.main != modes.AUTOCOMPLETE)
1314 modes.push(modes.AUTOCOMPLETE);
1316 else if (elem.localName !== "tooltip")
1317 if (Events.isHidden(elem)) {
1318 if (elem.hidePopup && Events.isHidden(elem.parentNode))
1321 else if (modes.main != modes.MENU)
1322 modes.push(modes.MENU);
1325 popuphidden: function onPopupHidden() {
1326 // gContextMenu is set to NULL, when a context menu is closed
1327 if (window.gContextMenu == null && !this._activeMenubar)
1328 modes.remove(modes.MENU, true);
1329 modes.remove(modes.AUTOCOMPLETE);
1332 resize: function onResize(event) {
1333 if (window.fullScreen != this._fullscreen) {
1334 statusline.statusBar.removeAttribute("moz-collapsed");
1335 this._fullscreen = window.fullScreen;
1336 dactyl.triggerObserver("fullscreen", this._fullscreen);
1337 autocommands.trigger("Fullscreen", { url: this._fullscreen ? "on" : "off", state: this._fullscreen });
1342 // argument "event" is deliberately not used, as i don't seem to have
1343 // access to the real focus target
1345 onFocusChange: function onFocusChange(event) {
1346 function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument
1347 if (dactyl.ignoreFocus)
1350 let win = window.document.commandDispatcher.focusedWindow;
1351 let elem = window.document.commandDispatcher.focusedElement;
1353 if (elem == null && Editor.getEditor(win))
1356 if (win && win.top == content && dactyl.has("tabs"))
1357 buffer.focusedFrame = win;
1360 if (elem && elem.readOnly)
1363 if (isinstance(elem, [HTMLEmbedElement, HTMLEmbedElement])) {
1364 modes.push(modes.EMBED);
1368 let haveInput = modes.stack.some(function (m) m.main.input);
1370 if (elem instanceof HTMLTextAreaElement
1371 || elem instanceof Element && util.computedStyle(elem).MozUserModify === "read-write"
1372 || elem == null && win && Editor.getEditor(win)) {
1374 if (modes.main == modes.VISUAL && elem.selectionEnd == elem.selectionStart)
1378 if (options["insertmode"])
1379 modes.push(modes.INSERT);
1381 modes.push(modes.TEXT_EDIT);
1382 if (elem.selectionEnd - elem.selectionStart > 0)
1383 modes.push(modes.VISUAL);
1386 if (hasHTMLDocument(win))
1387 buffer.lastInputField = elem;
1391 if (Events.isInputElement(elem)) {
1393 modes.push(modes.INSERT);
1395 if (hasHTMLDocument(win))
1396 buffer.lastInputField = elem;
1400 if (config.focusChange) {
1401 config.focusChange(win);
1405 let urlbar = document.getElementById("urlbar");
1406 if (elem == null && urlbar && urlbar.inputField == this._lastFocus)
1407 util.threadYield(true); // Why? --Kris
1409 while (modes.main.ownsFocus && !modes.topOfStack.params.holdFocus)
1410 modes.pop(null, { fromFocus: true });
1413 this._lastFocus = elem;
1417 onSelectionChange: function onSelectionChange(event) {
1418 let controller = document.commandDispatcher.getControllerForCommand("cmd_copy");
1419 let couldCopy = controller && controller.isCommandEnabled("cmd_copy");
1421 if (modes.main == modes.VISUAL) {
1423 modes.pop(); // Really not ideal.
1425 else if (couldCopy) {
1426 if (modes.main == modes.TEXT_EDIT && !options["insertmode"])
1427 modes.push(modes.VISUAL);
1428 else if (modes.main == modes.CARET)
1429 modes.push(modes.VISUAL);
1433 shouldPass: function shouldPass(event)
1434 !event.noremap && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) &&
1435 options.get("passkeys").has(events.toString(event))
1443 isEscape: function isEscape(event)
1444 let (key = isString(event) ? event : events.toString(event))
1445 key === "<Esc>" || key === "<C-[>",
1447 isHidden: function isHidden(elem, aggressive) {
1448 for (let e = elem; e instanceof Element; e = e.parentNode) {
1449 if (util.computedStyle(e).visibility !== "visible" ||
1450 aggressive && e.boxObject && e.boxObject.height === 0)
1456 isInputElement: function isInputElement(elem) {
1457 return elem instanceof HTMLInputElement && set.has(util.editableInputs, elem.type) ||
1458 isinstance(elem, [HTMLIsIndexElement, HTMLEmbedElement,
1459 HTMLObjectElement, HTMLSelectElement,
1460 HTMLTextAreaElement,
1461 Ci.nsIDOMXULTreeElement, Ci.nsIDOMXULTextBoxElement]) ||
1462 elem instanceof Window && Editor.getEditor(elem);
1465 kill: function kill(event) {
1466 event.stopPropagation();
1467 event.preventDefault();
1470 commands: function () {
1471 commands.add(["delmac[ros]"],
1474 dactyl.assert(!args.bang || !args[0], _("error.invalidArgument"));
1477 events.deleteMacros();
1479 events.deleteMacros(args[0]);
1481 dactyl.echoerr(_("error.argumentRequired"));
1484 completer: function (context) completion.macro(context),
1488 commands.add(["macros"],
1490 function (args) { completion.listCompleter("macro", args[0]); }, {
1492 completer: function (context) completion.macro(context)
1495 completion: function () {
1496 completion.macro = function macro(context) {
1497 context.title = ["Macro", "Keys"];
1498 context.completions = [item for (item in events.getMacros())];
1501 mappings: function () {
1503 mappings.add([modes.MAIN],
1504 ["<A-b>"], "Process the next key as a builtin mapping",
1506 events.processor = ProcessorStack(modes.getStack(0), mappings.hives.array, true);
1507 events.processor.keyEvents = events.keyEvents;
1510 mappings.add([modes.MAIN],
1511 ["<C-z>", "<pass-all-keys>"], "Temporarily ignore all " + config.appName + " key bindings",
1512 function () { modes.push(modes.PASS_THROUGH); });
1514 mappings.add([modes.MAIN],
1515 ["<C-v>", "<pass-next-key>"], "Pass through next key",
1517 if (modes.main == modes.QUOTE)
1519 modes.push(modes.QUOTE);
1522 mappings.add([modes.BASE],
1523 ["<Nop>"], "Do nothing",
1526 mappings.add([modes.BASE],
1527 ["<Pass>"], "Pass the events consumed by the last executed mapping",
1528 function ({ keypressEvents: [event] }) {
1529 dactyl.assert(event.dactylSavedEvents,
1530 _("event.nothingToPass"));
1531 return function () {
1532 events.feedevents(null, event.dactylSavedEvents,
1533 { skipmap: true, isMacro: true, isReplay: true });
1538 mappings.add([modes.COMMAND],
1539 ["q", "<record-macro>"], "Record a key sequence into a macro",
1540 function ({ arg }) {
1541 events._macroKeys.pop();
1542 events.recording = arg;
1544 { get arg() !modes.recording });
1546 mappings.add([modes.COMMAND],
1547 ["@", "<play-macro>"], "Play a macro",
1548 function ({ arg, count }) {
1549 count = Math.max(count, 1);
1551 events.playMacro(arg);
1553 { arg: true, count: true });
1555 mappings.add([modes.COMMAND],
1556 ["<A-m>s", "<sleep>"], "Sleep for {count} milliseconds before continuing macro playback",
1557 function ({ command, count }) {
1558 let now = Date.now();
1559 dactyl.assert(count, _("error.countRequired", command));
1560 if (events.feedingKeys)
1565 mappings.add([modes.COMMAND],
1566 ["<A-m>l", "<wait-for-page-load>"], "Wait for the current page to finish loading before continuing macro playback",
1567 function ({ count }) {
1568 if (events.feedingKeys && !events.waitForPageLoad(count)) {
1569 util.interrupted = true;
1570 throw Error("Interrupted");
1575 options: function () {
1576 const Hive = Class("Hive", {
1577 init: function init(values, map) {
1578 this.name = "passkeys:" + map;
1579 this.stack = MapHive.Stack(values.map(function (v) Map(v[map + "Keys"])));
1580 function Map(keys) ({
1581 execute: function () Events.PASS_THROUGH,
1586 get active() this.stack.length,
1588 get: function get(mode, key) this.stack.mappings[key],
1590 getCandidates: function getCandidates(mode, key) this.stack.candidates[key]
1592 options.add(["passkeys", "pk"],
1593 "Pass certain keys through directly for the given URLs",
1595 flush: function flush() {
1596 memoize(this, "filters", function () this.value.filter(function (f) f(buffer.documentURI)));
1597 memoize(this, "pass", function () set(array.flatten(this.filters.map(function (f) f.keys))));
1598 memoize(this, "commandHive", function hive() Hive(this.filters, "command"));
1599 memoize(this, "inputHive", function hive() Hive(this.filters, "input"));
1602 has: function (key) set.has(this.pass, key) || set.has(this.commandHive.stack.mappings, key),
1604 get pass() (this.flush(), this.pass),
1608 setter: function (values) {
1609 values.forEach(function (filter) {
1610 let vals = Option.splitList(filter.result);
1611 filter.keys = events.fromString(vals[0]).map(events.closure.toString);
1613 filter.commandKeys = vals.slice(1).map(events.closure.canonicalKeys);
1614 filter.inputKeys = filter.commandKeys.filter(/^<[ACM]-/);
1621 options.add(["strictfocus", "sf"],
1622 "Prevent scripts from focusing input elements without user intervention",
1625 options.add(["timeout", "tmo"],
1626 "Whether to execute a shorter key command after a timeout when a longer command exists",
1629 options.add(["timeoutlen", "tmol"],
1630 "Maximum time (milliseconds) to wait for a longer key command when a shorter one exists",
1633 sanitizer: function () {
1634 sanitizer.addItem("macros", {
1635 description: "Saved macros",
1637 action: function (timespan, host) {
1639 for (let [k, m] in events._macros)
1640 if (timespan.contains(m.timeRecorded * 1000))
1641 events._macros.remove(k);
1647 // vim: set fdm=marker sw=4 ts=4 et: