]> git.donarmstrong.com Git - dactyl.git/blob - common/content/events.js
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / content / events.js
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>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 /** @scope modules */
10
11 var ProcessorStack = Class("ProcessorStack", {
12     init: function (mode, hives, builtin) {
13         this.main = mode.main;
14         this._actions = [];
15         this.actions = [];
16         this.buffer = "";
17         this.events = [];
18
19         events.dbg("STACK " + mode);
20
21         let main = { __proto__: mode.main, params: mode.params };
22         this.modes = array([mode.params.keyModes, main, mode.main.allBases.slice(1)]).flatten().compact();
23
24         if (builtin)
25             hives = hives.filter(function (h) h.name === "builtin");
26
27         this.processors = this.modes.map(function (m) hives.map(function (h) KeyProcessor(m, h)))
28                                     .flatten().array;
29         this.ownsBuffer = !this.processors.some(function (p) p.main.ownsBuffer);
30
31         for (let [i, input] in Iterator(this.processors)) {
32             let params = input.main.params;
33
34             if (params.preExecute)
35                 input.preExecute = params.preExecute;
36
37             if (params.postExecute)
38                 input.postExecute = params.postExecute;
39
40             if (params.onKeyPress && input.hive === mappings.builtin)
41                 input.fallthrough = function fallthrough(events) {
42                     return params.onKeyPress(events) === false ? Events.KILL : Events.PASS;
43                 };
44             }
45
46         let hive = options.get("passkeys")[this.main.input ? "inputHive" : "commandHive"];
47         if (!builtin && hive.active && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)))
48             this.processors.unshift(KeyProcessor(modes.BASE, hive));
49     },
50
51     passUnknown: Class.memoize(function () options.get("passunknown").getKey(this.modes)),
52
53     notify: function () {
54         events.dbg("NOTIFY()");
55         events.keyEvents = [];
56         events.processor = null;
57         if (!this.execute(undefined, true)) {
58             events.processor = this;
59             events.keyEvents = this.keyEvents;
60         }
61     },
62
63     _result: function (result) (result === Events.KILL         ? "KILL"  :
64                                 result === Events.PASS         ? "PASS"  :
65                                 result === Events.PASS_THROUGH ? "PASS_THROUGH"  :
66                                 result === Events.ABORT        ? "ABORT" :
67                                 callable(result) ? result.toSource().substr(0, 50) : result),
68
69     execute: function execute(result, force) {
70         events.dbg("EXECUTE(" + this._result(result) + ", " + force + ") events:" + this.events.length
71                    + " processors:" + this.processors.length + " actions:" + this.actions.length);
72
73         let processors = this.processors;
74         let length = 1;
75
76         if (force)
77             this.processors = [];
78
79         if (this.ownsBuffer)
80             statusline.inputBuffer = this.processors.length ? this.buffer : "";
81
82         if (!this.processors.some(function (p) !p.extended) && this.actions.length) {
83             // We have matching actions and no processors other than
84             // those waiting on further arguments. Execute actions as
85             // long as they continue to return PASS.
86
87             for (var action in values(this.actions)) {
88                 while (callable(action)) {
89                     length = action.eventLength;
90                     action = dactyl.trapErrors(action);
91                     events.dbg("ACTION RES: " + length + " " + this._result(action));
92                 }
93                 if (action !== Events.PASS)
94                     break;
95             }
96
97             // Result is the result of the last action. Unless it's
98             // PASS, kill any remaining argument processors.
99             result = action !== undefined ? action : Events.KILL;
100             if (action !== Events.PASS)
101                 this.processors.length = 0;
102         }
103         else if (this.processors.length) {
104             // We're still waiting on the longest matching processor.
105             // Kill the event, set a timeout to give up waiting if applicable.
106
107             result = Events.KILL;
108             if (options["timeout"] && (this.actions.length || events.hasNativeKey(this.events[0], this.main, this.passUnknown)))
109                 this.timer = services.Timer(this, options["timeoutlen"], services.Timer.TYPE_ONE_SHOT);
110         }
111         else if (result !== Events.KILL && !this.actions.length &&
112                  !(this.events[0].isReplay || this.passUnknown
113                    || this.modes.some(function (m) m.passEvent(this), this.events[0]))) {
114             // No patching processors, this isn't a fake, pass-through
115             // event, we're not in pass-through mode, and we're not
116             // choosing to pass unknown keys. Kill the event and beep.
117
118             result = Events.ABORT;
119             if (!Events.isEscape(this.events.slice(-1)[0]))
120                 dactyl.beep();
121             events.feedingKeys = false;
122         }
123         else if (result === undefined)
124             // No matching processors, we're willing to pass this event,
125             // and we don't have a default action from a processor. Just
126             // pass the event.
127             result = Events.PASS;
128
129         events.dbg("RESULT: " + length + " " + this._result(result) + "\n\n");
130
131         if (result !== Events.PASS || this.events.length > 1)
132             if (result !== Events.ABORT || !this.events[0].isReplay)
133                 Events.kill(this.events[this.events.length - 1]);
134
135         if (result === Events.PASS_THROUGH || result === Events.PASS && this.passUnknown)
136             events.passing = true;
137
138         if (result === Events.PASS_THROUGH && this.keyEvents.length)
139             events.dbg("PASS_THROUGH:\n\t" + this.keyEvents.map(function (e) [e.type, events.toString(e)]).join("\n\t"));
140
141         if (result === Events.PASS_THROUGH)
142             events.feedevents(null, this.keyEvents, { skipmap: true, isMacro: true, isReplay: true });
143         else {
144             let list = this.events.filter(function (e) e.getPreventDefault() && !e.dactylDefaultPrevented);
145
146             if (result === Events.PASS)
147                 events.dbg("PASS THROUGH: " + list.slice(0, length).filter(function (e) e.type === "keypress").map(events.closure.toString));
148             if (list.length > length)
149                 events.dbg("REFEED: " + list.slice(length).filter(function (e) e.type === "keypress").map(events.closure.toString));
150
151             if (result === Events.PASS)
152                 events.feedevents(null, list.slice(0, length), { skipmap: true, isMacro: true, isReplay: true });
153             if (list.length > length && this.processors.length === 0)
154                 events.feedevents(null, list.slice(length));
155         }
156
157         return this.processors.length === 0;
158     },
159
160     process: function process(event) {
161         if (this.timer)
162             this.timer.cancel();
163
164         let key = events.toString(event);
165         this.events.push(event);
166         if (this.keyEvents)
167             this.keyEvents.push(event);
168
169         this.buffer += key;
170
171         let actions = [];
172         let processors = [];
173
174         events.dbg("PROCESS(" + key + ") skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay);
175
176         for (let [i, input] in Iterator(this.processors)) {
177             let res = input.process(event);
178             if (res !== Events.ABORT)
179                 var result = res;
180
181             events.dbg("RES: " + input + " " + this._result(res));
182
183             if (res === Events.KILL)
184                 break;
185
186             if (callable(res))
187                 actions.push(res);
188
189             if (res === Events.WAIT || input.waiting)
190                 processors.push(input);
191             if (isinstance(res, KeyProcessor))
192                 processors.push(res);
193         }
194
195         events.dbg("RESULT: " + event.getPreventDefault() + " " + this._result(result));
196         events.dbg("ACTIONS: " + actions.length + " " + this.actions.length);
197         events.dbg("PROCESSORS:", processors, "\n");
198
199         this._actions = actions;
200         this.actions = actions.concat(this.actions);
201
202         for (let action in values(actions))
203             if (!("eventLength" in action))
204                 action.eventLength = this.events.length;
205
206         if (result === Events.KILL)
207             this.actions = [];
208         else if (!this.actions.length && !processors.length)
209             for (let input in values(this.processors))
210                 if (input.fallthrough) {
211                     if (result === Events.KILL)
212                         break;
213                     result = dactyl.trapErrors(input.fallthrough, input, this.events);
214                 }
215
216         this.processors = processors;
217
218         return this.execute(result, options["timeout"] && options["timeoutlen"] === 0);
219     }
220 });
221
222 var KeyProcessor = Class("KeyProcessor", {
223     init: function init(main, hive) {
224         this.main = main;
225         this.events = [];
226         this.hive = hive;
227         this.wantCount = this.main.count;
228     },
229
230     get toStringParams() [this.main.name, this.hive.name],
231
232     countStr: "",
233     command: "",
234     get count() this.countStr ? Number(this.countStr) : null,
235
236     append: function append(event) {
237         this.events.push(event);
238         let key = events.toString(event);
239
240         if (this.wantCount && !this.command &&
241                 (this.countStr ? /^[0-9]$/ : /^[1-9]$/).test(key))
242             this.countStr += key;
243         else
244             this.command += key;
245         return this.events;
246     },
247
248     process: function process(event) {
249         this.append(event);
250         this.waiting = false;
251         return this.onKeyPress(event);
252     },
253
254     execute: function execute(map, args)
255         let (self = this)
256             function execute() {
257                 if (self.preExecute)
258                     self.preExecute.apply(self, args);
259
260                 args.self = self.main.params.mappingSelf || self.main.mappingSelf || map;
261                 let res = map.execute.call(map, args);
262
263                 if (self.postExecute)
264                     self.postExecute.apply(self, args);
265                 return res;
266             },
267
268     onKeyPress: function onKeyPress(event) {
269         if (event.skipmap)
270             return Events.ABORT;
271
272         if (!this.command)
273             return Events.WAIT;
274
275         var map = this.hive.get(this.main, this.command);
276         this.waiting = this.hive.getCandidates(this.main, this.command);
277         if (map) {
278             if (map.arg)
279                 return KeyArgProcessor(this, map, false, "arg");
280             else if (map.motion)
281                 return KeyArgProcessor(this, map, true, "motion");
282
283             return this.execute(map, {
284                 keyEvents: this.keyEvents,
285                 command: this.command,
286                 count: this.count,
287                 keypressEvents: this.events
288             });
289         }
290
291         if (!this.waiting)
292             return this.main.insert ? Events.PASS : Events.ABORT;
293
294         return Events.WAIT;
295     }
296 });
297
298 var KeyArgProcessor = Class("KeyArgProcessor", KeyProcessor, {
299     init: function init(input, map, wantCount, argName) {
300         init.supercall(this, input.main, input.hive);
301         this.map = map;
302         this.parent = input;
303         this.argName = argName;
304         this.wantCount = wantCount;
305     },
306
307     extended: true,
308
309     onKeyPress: function onKeyPress(event) {
310         if (Events.isEscape(event))
311             return Events.KILL;
312         if (!this.command)
313             return Events.WAIT;
314
315         let args = {
316             command: this.parent.command,
317             count:   this.count || this.parent.count,
318             events:  this.parent.events.concat(this.events)
319         };
320         args[this.argName] = this.command;
321
322         return this.execute(this.map, args);
323     }
324 });
325
326 /**
327  * A hive used mainly for tracking event listeners and cleaning them up when a
328  * group is destroyed.
329  */
330 var EventHive = Class("EventHive", Contexts.Hive, {
331     init: function init(group) {
332         init.supercall(this, group);
333         this.sessionListeners = [];
334     },
335
336     cleanup: function cleanup() {
337         this.unlisten(null);
338     },
339
340     /**
341      * Adds an event listener for this session and removes it on
342      * dactyl shutdown.
343      *
344      * @param {Element} target The element on which to listen.
345      * @param {string} event The event to listen for.
346      * @param {function} callback The function to call when the event is received.
347      * @param {boolean} capture When true, listen during the capture
348      *      phase, otherwise during the bubbling phase.
349      * @param {boolean} allowUntrusted When true, allow capturing of
350      *      untrusted events.
351      */
352     listen: function (target, event, callback, capture, allowUntrusted) {
353         if (!isObject(event))
354             var [self, events] = [null, array.toObject([[event, callback]])];
355         else {
356             [self, events] = [event, event[callback || "events"]];
357             [, , capture, allowUntrusted] = arguments;
358         }
359
360         if (Set.has(events, "input") && !Set.has(events, "dactyl-input"))
361             events["dactyl-input"] = events.input;
362
363         for (let [event, callback] in Iterator(events)) {
364             let args = [Cu.getWeakReference(target),
365                         event,
366                         this.wrapListener(callback, self),
367                         capture,
368                         allowUntrusted];
369
370             target.addEventListener.apply(target, args.slice(1));
371             this.sessionListeners.push(args);
372         }
373     },
374
375     /**
376      * Remove an event listener.
377      *
378      * @param {Element} target The element on which to listen.
379      * @param {string} event The event to listen for.
380      * @param {function} callback The function to call when the event is received.
381      * @param {boolean} capture When true, listen during the capture
382      *      phase, otherwise during the bubbling phase.
383      */
384     unlisten: function (target, event, callback, capture) {
385         this.sessionListeners = this.sessionListeners.filter(function (args) {
386             if (target == null || args[0].get() == target && args[1] == event && args[2] == callback && args[3] == capture) {
387                 args[0].get().removeEventListener.apply(args[0].get(), args.slice(1));
388                 return false;
389             }
390             return !args[0].get();
391         });
392     }
393 });
394
395 /**
396  * @instance events
397  */
398 var Events = Module("events", {
399     dbg: function () {},
400
401     init: function () {
402         this.keyEvents = [];
403
404         update(this, {
405             hives: contexts.Hives("events", EventHive),
406             user: contexts.hives.events.user,
407             builtin: contexts.hives.events.builtin
408         });
409
410         EventHive.prototype.wrapListener = this.closure.wrapListener;
411
412         XML.ignoreWhitespace = true;
413         util.overlayWindow(window, {
414             append: <e4x xmlns={XUL}>
415                 <window id={document.documentElement.id}>
416                     <!-- http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands -->
417                     <commandset id="dactyl-onfocus" commandupdater="true" events="focus"
418                                 oncommandupdate="dactyl.modules.events.onFocusChange(event);"/>
419                     <commandset id="dactyl-onselect" commandupdater="true" events="select"
420                                 oncommandupdate="dactyl.modules.events.onSelectionChange(event);"/>
421                 </window>
422             </e4x>.elements()
423         });
424
425         this._fullscreen = window.fullScreen;
426         this._lastFocus = null;
427         this._macroKeys = [];
428         this._lastMacro = "";
429
430         this._macros = storage.newMap("macros", { privateData: true, store: true });
431         for (let [k, m] in this._macros)
432             if (isString(m))
433                 m = { keys: m, timeRecorded: Date.now() };
434
435         // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
436         //       matters, so use that string as the first item, that you
437         //       want to refer to within dactyl's source code for
438         //       comparisons like if (key == "<Esc>") { ... }
439         this._keyTable = {
440             add: ["Plus", "Add"],
441             back_space: ["BS"],
442             count: ["count"],
443             delete: ["Del"],
444             escape: ["Esc", "Escape"],
445             insert: ["Insert", "Ins"],
446             leader: ["Leader"],
447             left_shift: ["LT", "<"],
448             nop: ["Nop"],
449             pass: ["Pass"],
450             return: ["Return", "CR", "Enter"],
451             right_shift: [">"],
452             space: ["Space", " "],
453             subtract: ["Minus", "Subtract"]
454         };
455
456         this._pseudoKeys = Set(["count", "leader", "nop", "pass"]);
457
458         this._key_key = {};
459         this._code_key = {};
460         this._key_code = {};
461         this._code_nativeKey = {};
462
463         for (let list in values(this._keyTable))
464             for (let v in values(list)) {
465                 if (v.length == 1)
466                     v = v.toLowerCase();
467                 this._key_key[v.toLowerCase()] = v;
468             }
469
470         for (let [k, v] in Iterator(KeyEvent)) {
471             this._code_nativeKey[v] = k.substr(4);
472
473             k = k.substr(7).toLowerCase();
474             let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
475                           .replace(/^NUMPAD/, "k")];
476
477             if (names[0].length == 1)
478                 names[0] = names[0].toLowerCase();
479
480             if (k in this._keyTable)
481                 names = this._keyTable[k];
482             this._code_key[v] = names[0];
483             for (let [, name] in Iterator(names)) {
484                 this._key_key[name.toLowerCase()] = name;
485                 this._key_code[name.toLowerCase()] = v;
486             }
487         }
488
489         // HACK: as Gecko does not include an event for <, we must add this in manually.
490         if (!("<" in this._key_code)) {
491             this._key_code["<"] = 60;
492             this._key_code["lt"] = 60;
493             this._code_key[60] = "lt";
494         }
495
496         this._activeMenubar = false;
497         this.listen(window, this, "events");
498     },
499
500     signals: {
501         "browser.locationChange": function (webProgress, request, uri) {
502             options.get("passkeys").flush();
503         },
504         "modes.change": function (oldMode, newMode) {
505             delete this.processor;
506         }
507     },
508
509     get listen() this.builtin.closure.listen,
510     addSessionListener: deprecated("events.listen", { get: function addSessionListener() this.listen }),
511
512     /**
513      * Wraps an event listener to ensure that errors are reported.
514      */
515     wrapListener: function wrapListener(method, self) {
516         self = self || this;
517         method.wrapped = wrappedListener;
518         function wrappedListener(event) {
519             try {
520                 method.apply(self, arguments);
521             }
522             catch (e) {
523                 dactyl.reportError(e);
524                 if (e.message == "Interrupted")
525                     dactyl.echoerr(_("error.interrupted"), commandline.FORCE_SINGLELINE);
526                 else
527                     dactyl.echoerr(_("event.error", event.type, e.echoerr || e),
528                                    commandline.FORCE_SINGLELINE);
529             }
530         };
531         return wrappedListener;
532     },
533
534     /**
535      * @property {boolean} Whether synthetic key events are currently being
536      *     processed.
537      */
538     feedingKeys: false,
539
540     /**
541      * Initiates the recording of a key event macro.
542      *
543      * @param {string} macro The name for the macro.
544      */
545     _recording: null,
546     get recording() this._recording,
547
548     set recording(macro) {
549         dactyl.assert(macro == null || /[a-zA-Z0-9]/.test(macro),
550                       _("macro.invalid", macro));
551
552         modes.recording = !!macro;
553
554         if (/[A-Z]/.test(macro)) { // uppercase (append)
555             macro = macro.toLowerCase();
556             this._macroKeys = events.fromString((this._macros.get(macro) || { keys: "" }).keys, true)
557                                     .map(events.closure.toString);
558         }
559         else if (macro) {
560             this._macroKeys = [];
561         }
562         else {
563             this._macros.set(this.recording, {
564                 keys: this._macroKeys.join(""),
565                 timeRecorded: Date.now()
566             });
567
568             dactyl.log(_("macro.recorded", this.recording, this._macroKeys.join("")), 9);
569             dactyl.echomsg(_("macro.recorded", this.recording));
570         }
571         this._recording = macro || null;
572     },
573
574     /**
575      * Replays a macro.
576      *
577      * @param {string} The name of the macro to replay.
578      * @returns {boolean}
579      */
580     playMacro: function (macro) {
581         let res = false;
582         dactyl.assert(/^[a-zA-Z0-9@]$/.test(macro), _("macro.invalid", macro));
583
584         if (macro == "@")
585             dactyl.assert(this._lastMacro, _("macro.noPrevious"));
586         else
587             this._lastMacro = macro.toLowerCase(); // XXX: sets last played macro, even if it does not yet exist
588
589         if (this._macros.get(this._lastMacro)) {
590             try {
591                 modes.replaying = true;
592                 res = events.feedkeys(this._macros.get(this._lastMacro).keys, { noremap: true });
593             }
594             finally {
595                 modes.replaying = false;
596             }
597         }
598         else
599             // TODO: ignore this like Vim?
600             dactyl.echoerr(_("macro.noSuch", this._lastMacro));
601         return res;
602     },
603
604     /**
605      * Returns all macros matching *filter*.
606      *
607      * @param {string} filter A regular expression filter string. A null
608      *     filter selects all macros.
609      */
610     getMacros: function (filter) {
611         let re = RegExp(filter || "");
612         return ([k, m.keys] for ([k, m] in events._macros) if (re.test(k)));
613     },
614
615     /**
616      * Deletes all macros matching *filter*.
617      *
618      * @param {string} filter A regular expression filter string. A null
619      *     filter deletes all macros.
620      */
621     deleteMacros: function (filter) {
622         let re = RegExp(filter || "");
623         for (let [item, ] in this._macros) {
624             if (!filter || re.test(item))
625                 this._macros.remove(item);
626         }
627     },
628
629     /**
630      * Feeds a list of events to *target* or the originalTarget member
631      * of each event if *target* is null.
632      *
633      * @param {EventTarget} target The destination node for the events.
634      *      @optional
635      * @param {[Event]} list The events to dispatch.
636      * @param {object} extra Extra properties for processing by dactyl.
637      *      @optional
638      */
639     feedevents: function feedevents(target, list, extra) {
640         list.forEach(function _feedevent(event, i) {
641             let elem = target || event.originalTarget;
642             if (elem) {
643                 let doc = elem.ownerDocument || elem.document || elem;
644                 let evt = events.create(doc, event.type, event);
645                 events.dispatch(elem, evt, extra);
646             }
647             else if (i > 0 && event.type === "keypress")
648                 events.events.keypress.call(events, event);
649         });
650     },
651
652     /**
653      * Pushes keys onto the event queue from dactyl. It is similar to
654      * Vim's feedkeys() method, but cannot cope with 2 partially-fed
655      * strings, you have to feed one parseable string.
656      *
657      * @param {string} keys A string like "2<C-f>" to push onto the event
658      *     queue. If you want "<" to be taken literally, prepend it with a
659      *     "\\".
660      * @param {boolean} noremap Whether recursive mappings should be
661      *     disallowed.
662      * @param {boolean} silent Whether the command should be echoed to the
663      *     command line.
664      * @returns {boolean}
665      */
666     feedkeys: function (keys, noremap, quiet, mode) {
667         try {
668             var savedEvents = this._processor && this._processor.keyEvents;
669
670             var wasFeeding = this.feedingKeys;
671             this.feedingKeys = true;
672
673             var wasQuiet = commandline.quiet;
674             if (quiet)
675                 commandline.quiet = quiet;
676
677             keys = mappings.expandLeader(keys);
678
679             for (let [, evt_obj] in Iterator(events.fromString(keys))) {
680                 let now = Date.now();
681                 let key = events.toString(evt_obj);
682                 for (let type in values(["keydown", "keypress", "keyup"])) {
683                     let evt = update({}, evt_obj, { type: type });
684                     if (type !== "keypress" && !evt.keyCode)
685                         evt.keyCode = evt._keyCode || 0;
686
687                     if (isObject(noremap))
688                         update(evt, noremap);
689                     else
690                         evt.noremap = !!noremap;
691                     evt.isMacro = true;
692                     evt.dactylMode = mode;
693                     evt.dactylSavedEvents = savedEvents;
694                     this.feedingEvent = evt;
695
696                     let doc = document.commandDispatcher.focusedWindow.document;
697                     let event = events.create(doc, type, evt);
698                     let target = dactyl.focusedElement
699                               || ["complete", "interactive"].indexOf(doc.readyState) >= 0 && doc.documentElement
700                               || doc.defaultView;
701
702                     if (target instanceof Element && !Events.isInputElement(target) &&
703                         ["<Return>", "<Space>"].indexOf(key) == -1)
704                         target = target.ownerDocument.documentElement;
705
706                     if (!evt_obj.dactylString && !mode)
707                         events.dispatch(target, event, evt);
708                     else if (type === "keypress")
709                         events.events.keypress.call(events, event);
710                 }
711
712                 if (!this.feedingKeys)
713                     return false;
714             }
715         }
716         catch (e) {
717             util.reportError(e);
718         }
719         finally {
720             this.feedingEvent = null;
721             this.feedingKeys = wasFeeding;
722             if (quiet)
723                 commandline.quiet = wasQuiet;
724             dactyl.triggerObserver("events.doneFeeding");
725         }
726         return true;
727     },
728
729     /**
730      * Creates an actual event from a pseudo-event object.
731      *
732      * The pseudo-event object (such as may be retrieved from events.fromString)
733      * should have any properties you want the event to have.
734      *
735      * @param {Document} doc The DOM document to associate this event with
736      * @param {Type} type The type of event (keypress, click, etc.)
737      * @param {Object} opts The pseudo-event. @optional
738      */
739     create: function (doc, type, opts) {
740         const DEFAULTS = {
741             HTML: {
742                 type: type, bubbles: true, cancelable: false
743             },
744             Key: {
745                 type: type,
746                 bubbles: true, cancelable: true,
747                 view: doc.defaultView,
748                 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
749                 keyCode: 0, charCode: 0
750             },
751             Mouse: {
752                 type: type,
753                 bubbles: true, cancelable: true,
754                 view: doc.defaultView,
755                 detail: 1,
756                 screenX: 0, screenY: 0,
757                 clientX: 0, clientY: 0,
758                 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
759                 button: 0,
760                 relatedTarget: null
761             }
762         };
763
764         opts = opts || {};
765
766         var t = this._create_types[type];
767         var evt = doc.createEvent((t || "HTML") + "Events");
768
769         let defaults = DEFAULTS[t || "HTML"];
770
771         let args = Object.keys(defaults)
772                          .map(function (k) k in opts ? opts[k] : defaults[k]);
773
774         evt["init" + t + "Event"].apply(evt, args);
775         return evt;
776     },
777
778     _create_types: Class.memoize(function () iter(
779         {
780             Mouse: "click mousedown mouseout mouseover mouseup",
781             Key:   "keydown keypress keyup",
782             "":    "change dactyl-input input submit"
783         }
784     ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
785      .flatten()
786      .toObject()),
787
788     /**
789      * Converts a user-input string of keys into a canonical
790      * representation.
791      *
792      * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
793      * <C- > maps to <C-Space>, <S-a> maps to A
794      * << maps to <lt><lt>
795      *
796      * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
797      * in macros.
798      *
799      * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
800      * of x.
801      *
802      * @param {string} keys Messy form.
803      * @param {boolean} unknownOk Whether unknown keys are passed
804      *     through rather than being converted to <lt>keyname>.
805      *     @default false
806      * @returns {string} Canonical form.
807      */
808     canonicalKeys: function (keys, unknownOk) {
809         if (arguments.length === 1)
810             unknownOk = true;
811         return events.fromString(keys, unknownOk).map(events.closure.toString).join("");
812     },
813
814     iterKeys: function (keys) iter(function () {
815         let match, re = /<.*?>?>|[^<]/g;
816         while (match = re.exec(keys))
817             yield match[0];
818     }()),
819
820     /**
821      * Dispatches an event to an element as if it were a native event.
822      *
823      * @param {Node} target The DOM node to which to dispatch the event.
824      * @param {Event} event The event to dispatch.
825      */
826     dispatch: Class.memoize(function ()
827         util.haveGecko("2b")
828             ? function dispatch(target, event, extra) {
829                 try {
830                     this.feedingEvent = extra;
831                     if (target instanceof Element)
832                         // This causes a crash on Gecko<2.0, it seems.
833                         return (target.ownerDocument || target.document || target).defaultView
834                                .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
835                                .dispatchDOMEventViaPresShell(target, event, true);
836                     else {
837                         target.dispatchEvent(event);
838                         return !event.getPreventDefault();
839                     }
840                 }
841                 catch (e) {
842                     util.reportError(e);
843                 }
844                 finally {
845                     this.feedingEvent = null;
846                 }
847             }
848             : function dispatch(target, event, extra) {
849                 try {
850                     this.feedingEvent = extra;
851                     target.dispatchEvent(update(event, extra));
852                 }
853                 finally {
854                     this.feedingEvent = null;
855                 }
856             }),
857
858     get defaultTarget() dactyl.focusedElement || content.document.body || document.documentElement,
859
860     /**
861      * Converts an event string into an array of pseudo-event objects.
862      *
863      * These objects can be used as arguments to events.toString or
864      * events.create, though they are unlikely to be much use for other
865      * purposes. They have many of the properties you'd expect to find on a
866      * real event, but none of the methods.
867      *
868      * Also may contain two "special" parameters, .dactylString and
869      * .dactylShift these are set for characters that can never by
870      * typed, but may appear in mappings, for example <Nop> is passed as
871      * dactylString, and dactylShift is set when a user specifies
872      * <S-@> where @ is a non-case-changeable, non-space character.
873      *
874      * @param {string} keys The string to parse.
875      * @param {boolean} unknownOk Whether unknown keys are passed
876      *     through rather than being converted to <lt>keyname>.
877      *     @default false
878      * @returns {Array[Object]}
879      */
880     fromString: function (input, unknownOk) {
881
882         if (arguments.length === 1)
883             unknownOk = true;
884
885         let out = [];
886         for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
887             let evt_str = match[0];
888
889             let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
890                             keyCode: 0, charCode: 0, type: "keypress" };
891
892             if (evt_str.length == 1) {
893                 evt_obj.charCode = evt_str.charCodeAt(0);
894                 evt_obj._keyCode = this._key_code[evt_str[0].toLowerCase()];
895                 evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
896             }
897             else {
898                 let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
899                 modifier = Set(modifier.toUpperCase());
900                 keyname = keyname.toLowerCase();
901                 evt_obj.dactylKeyname = keyname;
902                 if (/^u[0-9a-f]+$/.test(keyname))
903                     keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
904
905                 if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
906                                 this._key_code[keyname] || Set.has(this._pseudoKeys, keyname))) {
907                     evt_obj.globKey  ="*" in modifier;
908                     evt_obj.ctrlKey  ="C" in modifier;
909                     evt_obj.altKey   ="A" in modifier;
910                     evt_obj.shiftKey ="S" in modifier;
911                     evt_obj.metaKey  ="M" in modifier || "⌘" in modifier;
912                     evt_obj.dactylShift = evt_obj.shiftKey;
913
914                     if (keyname.length == 1) { // normal characters
915                         if (evt_obj.shiftKey)
916                             keyname = keyname.toUpperCase();
917
918                         evt_obj.charCode = keyname.charCodeAt(0);
919                         evt_obj._keyCode = this._key_code[keyname.toLowerCase()];
920                     }
921                     else if (Set.has(this._pseudoKeys, keyname)) {
922                         evt_obj.dactylString = "<" + this._key_key[keyname] + ">";
923                     }
924                     else if (/mouse$/.test(keyname)) { // mouse events
925                         evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
926                         evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
927                         delete evt_obj.keyCode;
928                         delete evt_obj.charCode;
929                     }
930                     else { // spaces, control characters, and <
931                         evt_obj.keyCode = this._key_code[keyname];
932                         evt_obj.charCode = 0;
933                     }
934                 }
935                 else { // an invalid sequence starting with <, treat as a literal
936                     out = out.concat(events.fromString("<lt>" + evt_str.substr(1)));
937                     continue;
938                 }
939             }
940
941             // TODO: make a list of characters that need keyCode and charCode somewhere
942             if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
943                 evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
944             if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
945                 evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
946
947             evt_obj.modifiers = (evt_obj.ctrlKey  && Ci.nsIDOMNSEvent.CONTROL_MASK)
948                               | (evt_obj.altKey   && Ci.nsIDOMNSEvent.ALT_MASK)
949                               | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
950                               | (evt_obj.metaKey  && Ci.nsIDOMNSEvent.META_MASK);
951
952             out.push(evt_obj);
953         }
954         return out;
955     },
956
957     /**
958      * Converts the specified event to a string in dactyl key-code
959      * notation. Returns null for an unknown event.
960      *
961      * @param {Event} event
962      * @returns {string}
963      */
964     toString: function toString(event) {
965         if (!event)
966             return toString.supercall(this);
967
968         if (event.dactylString)
969             return event.dactylString;
970
971         let key = null;
972         let modifier = "";
973
974         if (event.globKey)
975             modifier += "*-";
976         if (event.ctrlKey)
977             modifier += "C-";
978         if (event.altKey)
979             modifier += "A-";
980         if (event.metaKey)
981             modifier += "M-";
982
983         if (/^key/.test(event.type)) {
984             let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
985             if (charCode == 0) {
986                 if (event.keyCode in this._code_key) {
987                     key = this._code_key[event.keyCode];
988
989                     if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
990                         modifier += "S-";
991                     else if (!modifier && key.length === 1)
992                         if (event.shiftKey)
993                             key = key.toUpperCase();
994                         else
995                             key = key.toLowerCase();
996
997                     if (!modifier && /^[a-z0-9]$/i.test(key))
998                         return key;
999                 }
1000             }
1001             // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1002             //            (i.e., cntrl codes 27--31)
1003             // ---
1004             // For more information, see:
1005             //     [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
1006             //     [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
1007             //         https://bugzilla.mozilla.org/show_bug.cgi?id=416227
1008             //     [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
1009             //         https://bugzilla.mozilla.org/show_bug.cgi?id=432951
1010             // ---
1011             //
1012             // The following fixes are only activated if util.OS.isMacOSX.
1013             // Technically, they prevent mappings from <C-Esc> (and
1014             // <C-C-]> if your fancy keyboard permits such things<?>), but
1015             // these <C-control> mappings are probably pathological (<C-Esc>
1016             // certainly is on Windows), and so it is probably
1017             // harmless to remove the util.OS.isMacOSX if desired.
1018             //
1019             else if (util.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1020                 if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1021                     key = "Esc";
1022                     modifier = modifier.replace("C-", "");
1023                 }
1024                 else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1025                     key = String.fromCharCode(charCode + 64);
1026             }
1027             // a normal key like a, b, c, 0, etc.
1028             else if (charCode > 0) {
1029                 key = String.fromCharCode(charCode);
1030
1031                 if (!/^[a-z0-9]$/i.test(key) && key in this._key_code) {
1032                     // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
1033                     if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
1034                         modifier += "S-";
1035
1036                     key = this._code_key[this._key_code[key]];
1037                 }
1038                 else {
1039                     // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
1040                     // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
1041                     if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
1042                         modifier += "S-";
1043                     if (/^\s$/.test(key))
1044                         key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
1045                     else if (modifier.length == 0)
1046                         return key;
1047                 }
1048             }
1049             if (key == null) {
1050                 if (event.shiftKey)
1051                     modifier += "S-";
1052                 key = this._key_key[event.dactylKeyname] || event.dactylKeyname;
1053             }
1054             if (key == null)
1055                 return null;
1056         }
1057         else if (event.type == "click" || event.type == "dblclick") {
1058             if (event.shiftKey)
1059                 modifier += "S-";
1060             if (event.type == "dblclick")
1061                 modifier += "2-";
1062             // TODO: triple and quadruple click
1063
1064             switch (event.button) {
1065             case 0:
1066                 key = "LeftMouse";
1067                 break;
1068             case 1:
1069                 key = "MiddleMouse";
1070                 break;
1071             case 2:
1072                 key = "RightMouse";
1073                 break;
1074             }
1075         }
1076
1077         if (key == null)
1078             return null;
1079
1080         return "<" + modifier + key + ">";
1081     },
1082
1083     /**
1084      * Returns true if there's a known native key handler for the given
1085      * event in the given mode.
1086      *
1087      * @param {Event} event A keypress event.
1088      * @param {Modes.Mode} mode The main mode.
1089      * @param {boolean} passUnknown Whether unknown keys should be passed.
1090      */
1091     hasNativeKey: function hasNativeKey(event, mode, passUnknown) {
1092         if (mode.input && event.charCode && !(event.ctrlKey || event.metaKey))
1093             return true;
1094
1095         if (!passUnknown)
1096             return false;
1097
1098         var elements = document.getElementsByTagNameNS(XUL, "key");
1099         var filters = [];
1100
1101         if (event.keyCode)
1102             filters.push(["keycode", this._code_nativeKey[event.keyCode]]);
1103         if (event.charCode) {
1104             let key = String.fromCharCode(event.charCode);
1105             filters.push(["key", key.toUpperCase()],
1106                          ["key", key.toLowerCase()]);
1107         }
1108
1109         let accel = util.OS.isMacOSX ? "metaKey" : "ctrlKey";
1110
1111         let access = iter({ 1: "shiftKey", 2: "ctrlKey", 4: "altKey", 8: "metaKey" })
1112                         .filter(function ([k, v]) this & k, prefs.get("ui.key.chromeAccess"))
1113                         .map(function ([k, v]) [v, true])
1114                         .toObject();
1115
1116     outer:
1117         for (let [, key] in iter(elements))
1118             if (filters.some(function ([k, v]) key.getAttribute(k) == v)) {
1119                 let keys = { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false };
1120                 let needed = { ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey };
1121
1122                 let modifiers = (key.getAttribute("modifiers") || "").trim().split(/[\s,]+/);
1123                 for (let modifier in values(modifiers))
1124                     switch (modifier) {
1125                         case "access": update(keys, access); break;
1126                         case "accel":  keys[accel] = true; break;
1127                         default:       keys[modifier + "Key"] = true; break;
1128                         case "any":
1129                             if (!iter.some(keys, function ([k, v]) v && needed[k]))
1130                                 continue outer;
1131                             for (let [k, v] in iter(keys)) {
1132                                 if (v)
1133                                     needed[k] = false;
1134                                 keys[k] = false;
1135                             }
1136                             break;
1137                     }
1138
1139                 if (iter(needed).every(function ([k, v]) v == keys[k]))
1140                     return key;
1141             }
1142
1143         return false;
1144     },
1145
1146     /**
1147      * Returns true if *key* is a key code defined to accept/execute input on
1148      * the command line.
1149      *
1150      * @param {string} key The key code to test.
1151      * @returns {boolean}
1152      */
1153     isAcceptKey: function (key) key == "<Return>" || key == "<C-j>" || key == "<C-m>",
1154
1155     /**
1156      * Returns true if *key* is a key code defined to reject/cancel input on
1157      * the command line.
1158      *
1159      * @param {string} key The key code to test.
1160      * @returns {boolean}
1161      */
1162     isCancelKey: function (key) key == "<Esc>" || key == "<C-[>" || key == "<C-c>",
1163
1164     /**
1165      * Returns true if *node* belongs to the current content document or any
1166      * sub-frame thereof.
1167      *
1168      * @param {Node|Document|Window} node The node to test.
1169      * @returns {boolean}
1170      */
1171     isContentNode: function isContentNode(node) {
1172         let win = (node.ownerDocument || node).defaultView || node;
1173         return XPCNativeWrapper(win).top == content;
1174     },
1175
1176     /**
1177      * Waits for the current buffer to successfully finish loading. Returns
1178      * true for a successful page load otherwise false.
1179      *
1180      * @returns {boolean}
1181      */
1182     waitForPageLoad: function (time) {
1183         if (buffer.loaded)
1184             return true;
1185
1186         dactyl.echo(_("macro.loadWaiting"), commandline.FORCE_SINGLELINE);
1187
1188         const maxWaitTime = (time || 25);
1189         util.waitFor(function () buffer.loaded, this, maxWaitTime * 1000, true);
1190
1191         dactyl.echo("", commandline.FORCE_SINGLELINE);
1192         if (!buffer.loaded)
1193             dactyl.echoerr(_("macro.loadFailed", maxWaitTime));
1194
1195         return buffer.loaded;
1196     },
1197
1198     /**
1199      * Ensures that the currently focused element is visible and blurs
1200      * it if it's not.
1201      */
1202     checkFocus: function () {
1203         if (dactyl.focusedElement) {
1204             let rect = dactyl.focusedElement.getBoundingClientRect();
1205             if (!rect.width || !rect.height) {
1206                 services.focus.clearFocus(window);
1207                 document.commandDispatcher.focusedWindow = content;
1208                 // onFocusChange needs to die.
1209                 this.onFocusChange();
1210             }
1211         }
1212     },
1213
1214     events: {
1215         DOMMenuBarActive: function () {
1216             this._activeMenubar = true;
1217             if (modes.main != modes.MENU)
1218                 modes.push(modes.MENU);
1219         },
1220
1221         DOMMenuBarInactive: function () {
1222             this._activeMenubar = false;
1223             modes.remove(modes.MENU, true);
1224         },
1225
1226         blur: function onBlur(event) {
1227             let elem = event.originalTarget;
1228             if (elem instanceof Window && services.focus.activeWindow == null
1229                 && document.commandDispatcher.focusedWindow !== window) {
1230                 // Deals with circumstances where, after the main window
1231                 // blurs while a collapsed frame has focus, re-activating
1232                 // the main window does not restore focus and we lose key
1233                 // input.
1234                 services.focus.clearFocus(window);
1235                 document.commandDispatcher.focusedWindow = Editor.getEditor(content) ? window : content;
1236             }
1237
1238             let hold = modes.topOfStack.params.holdFocus;
1239             if (elem == hold) {
1240                 dactyl.focus(hold);
1241                 this.timeout(function () { dactyl.focus(hold); });
1242             }
1243         },
1244
1245         // TODO: Merge with onFocusChange
1246         focus: function onFocus(event) {
1247             let elem = event.originalTarget;
1248
1249             if (event.target instanceof Ci.nsIDOMXULTextBoxElement)
1250                 if (Events.isHidden(elem, true))
1251                     elem.blur();
1252
1253             let win = (elem.ownerDocument || elem).defaultView || elem;
1254
1255             if (!(services.focus.getLastFocusMethod(win) & 0x7000)
1256                 && events.isContentNode(elem)
1257                 && !buffer.focusAllowed(elem)
1258                 && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window])) {
1259
1260                 if (elem.frameElement)
1261                     dactyl.focusContent(true);
1262                 else if (!(elem instanceof Window) || Editor.getEditor(elem))
1263                     dactyl.focus(window);
1264             }
1265
1266             if (elem instanceof Element)
1267                 elem.dactylFocusAllowed = undefined;
1268         },
1269
1270         /*
1271         onFocus: function onFocus(event) {
1272             let elem = event.originalTarget;
1273             if (!(elem instanceof Element))
1274                 return;
1275             let win = elem.ownerDocument.defaultView;
1276
1277             try {
1278                 util.dump(elem, services.focus.getLastFocusMethod(win) & (0x7000));
1279                 if (buffer.focusAllowed(win))
1280                     win.dactylLastFocus = elem;
1281                 else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement])) {
1282                     if (win.dactylLastFocus)
1283                         dactyl.focus(win.dactylLastFocus);
1284                     else
1285                         elem.blur();
1286                 }
1287             }
1288             catch (e) {
1289                 util.dump(win, String(elem.ownerDocument), String(elem.ownerDocument && elem.ownerDocument.defaultView));
1290                 util.reportError(e);
1291             }
1292         },
1293         */
1294
1295         input: function onInput(event) {
1296             event.originalTarget.dactylKeyPress = undefined;
1297         },
1298
1299         // this keypress handler gets always called first, even if e.g.
1300         // the command-line has focus
1301         // TODO: ...help me...please...
1302         keypress: function onKeyPress(event) {
1303             event.dactylDefaultPrevented = event.getPreventDefault();
1304
1305             let duringFeed = this.duringFeed || [];
1306             this.duringFeed = [];
1307             try {
1308                 if (this.feedingEvent)
1309                     for (let [k, v] in Iterator(this.feedingEvent))
1310                         if (!(k in event))
1311                             event[k] = v;
1312                 this.feedingEvent = null;
1313
1314                 let key = events.toString(event);
1315
1316                 // Hack to deal with <BS> and so forth not dispatching input
1317                 // events
1318                 if (key && event.originalTarget instanceof HTMLInputElement && !modes.main.passthrough) {
1319                     let elem = event.originalTarget;
1320                     elem.dactylKeyPress = elem.value;
1321                     util.timeout(function () {
1322                         if (elem.dactylKeyPress !== undefined && elem.value !== elem.dactylKeyPress)
1323                             events.dispatch(elem, events.create(elem.ownerDocument, "dactyl-input"));
1324                         elem.dactylKeyPress = undefined;
1325                     });
1326                 }
1327
1328                 if (!key)
1329                      return null;
1330
1331                 if (modes.recording && !event.isReplay)
1332                     events._macroKeys.push(key);
1333
1334                 // feedingKeys needs to be separate from interrupted so
1335                 // we can differentiate between a recorded <C-c>
1336                 // interrupting whatever it's started and a real <C-c>
1337                 // interrupting our playback.
1338                 if (events.feedingKeys && !event.isMacro) {
1339                     if (key == "<C-c>") {
1340                         events.feedingKeys = false;
1341                         if (modes.replaying) {
1342                             modes.replaying = false;
1343                             this.timeout(function () { dactyl.echomsg(_("macro.canceled", this._lastMacro)); }, 100);
1344                         }
1345                     }
1346                     else
1347                         duringFeed.push(event);
1348
1349                     return Events.kill(event);
1350                 }
1351
1352                 if (!this.processor) {
1353                     let mode = modes.getStack(0);
1354                     if (event.dactylMode)
1355                         mode = Modes.StackElement(event.dactylMode);
1356
1357                     let ignore = false;
1358
1359                     if (mode.main == modes.PASS_THROUGH)
1360                         ignore = !Events.isEscape(key) && key != "<C-v>";
1361                     else if (mode.main == modes.QUOTE) {
1362                         if (modes.getStack(1).main == modes.PASS_THROUGH) {
1363                             mode = Modes.StackElement(modes.getStack(2).main);
1364                             ignore = Events.isEscape(key);
1365                         }
1366                         else if (events.shouldPass(event))
1367                             mode = Modes.StackElement(modes.getStack(1).main);
1368                         else
1369                             ignore = true;
1370
1371                         modes.pop();
1372                     }
1373                     else if (!event.isMacro && !event.noremap && events.shouldPass(event))
1374                         ignore = true;
1375
1376                     events.dbg("\n\n");
1377                     events.dbg("ON KEYPRESS " + key + " ignore: " + ignore,
1378                                event.originalTarget instanceof Element ? event.originalTarget : String(event.originalTarget));
1379
1380                     if (ignore)
1381                         return null;
1382
1383                     // FIXME: Why is this hard coded? --Kris
1384                     if (key == "<C-c>")
1385                         util.interrupted = true;
1386
1387                     this.processor = ProcessorStack(mode, mappings.hives.array, event.noremap);
1388                     this.processor.keyEvents = this.keyEvents;
1389                 }
1390
1391                 let { keyEvents, processor } = this;
1392                 this._processor = processor;
1393                 this.processor = null;
1394                 this.keyEvents = [];
1395
1396                 if (!processor.process(event)) {
1397                     this.keyEvents = keyEvents;
1398                     this.processor = processor;
1399                 }
1400
1401             }
1402             catch (e) {
1403                 dactyl.reportError(e);
1404             }
1405             finally {
1406                 [duringFeed, this.duringFeed] = [this.duringFeed, duringFeed];
1407                 if (this.feedingKeys)
1408                     this.duringFeed = this.duringFeed.concat(duringFeed);
1409                 else
1410                     for (let event in values(duringFeed))
1411                         try {
1412                             this.dispatch(event.originalTarget, event, event);
1413                         }
1414                         catch (e) {
1415                             util.reportError(e);
1416                         }
1417             }
1418         },
1419
1420         keyup: function onKeyUp(event) {
1421             if (event.type == "keydown")
1422                 this.keyEvents.push(event);
1423             else if (!this.processor)
1424                 this.keyEvents = [];
1425
1426             let pass = this.passing && !event.isMacro ||
1427                     this.feedingEvent && this.feedingEvent.isReplay ||
1428                     event.isReplay ||
1429                     modes.main == modes.PASS_THROUGH ||
1430                     modes.main == modes.QUOTE
1431                         && modes.getStack(1).main !== modes.PASS_THROUGH
1432                         && !this.shouldPass(event) ||
1433                     !modes.passThrough && this.shouldPass(event) ||
1434                     !this.processor && event.type === "keydown"
1435                         && options.get("passunknown").getKey(modes.main.allBases)
1436                         && let (key = events.toString(event))
1437                             !modes.main.allBases.some(
1438                                 function (mode) mappings.hives.some(
1439                                     function (hive) hive.get(mode, key) || hive.getCandidates(mode, key)));
1440
1441             if (event.type === "keydown")
1442                 this.passing = pass;
1443
1444             events.dbg("ON " + event.type.toUpperCase() + " " + this.toString(event) + " pass: " + pass + " replay: " + event.isReplay + " macro: " + event.isMacro);
1445
1446             // Prevents certain sites from transferring focus to an input box
1447             // before we get a chance to process our key bindings on the
1448             // "keypress" event.
1449             if (!pass)
1450                 event.stopPropagation();
1451         },
1452         keydown: function onKeyDown(event) {
1453             if (!event.isMacro)
1454                 this.passing = false;
1455             this.events.keyup.call(this, event);
1456         },
1457
1458         mousedown: function onMouseDown(event) {
1459             let elem = event.target;
1460             let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
1461
1462             for (; win; win = win != win.parent && win.parent) {
1463                 for (; elem instanceof Element; elem = elem.parentNode)
1464                     elem.dactylFocusAllowed = true;
1465                 win.document.dactylFocusAllowed = true;
1466             }
1467         },
1468
1469         popupshown: function onPopupShown(event) {
1470             let elem = event.originalTarget;
1471             if (elem instanceof Ci.nsIAutoCompletePopup) {
1472                 if (modes.main != modes.AUTOCOMPLETE)
1473                     modes.push(modes.AUTOCOMPLETE);
1474             }
1475             else if (elem.localName !== "tooltip")
1476                 if (Events.isHidden(elem)) {
1477                     if (elem.hidePopup && Events.isHidden(elem.parentNode))
1478                         elem.hidePopup();
1479                 }
1480                 else if (modes.main != modes.MENU)
1481                     modes.push(modes.MENU);
1482         },
1483
1484         popuphidden: function onPopupHidden(event) {
1485             if (window.gContextMenu == null && !this._activeMenubar)
1486                 modes.remove(modes.MENU, true);
1487             modes.remove(modes.AUTOCOMPLETE);
1488         },
1489
1490         resize: function onResize(event) {
1491             if (window.fullScreen != this._fullscreen) {
1492                 statusline.statusBar.removeAttribute("moz-collapsed");
1493                 this._fullscreen = window.fullScreen;
1494                 dactyl.triggerObserver("fullscreen", this._fullscreen);
1495                 autocommands.trigger("Fullscreen", { url: this._fullscreen ? "on" : "off", state: this._fullscreen });
1496             }
1497         }
1498     },
1499
1500     // argument "event" is deliberately not used, as i don't seem to have
1501     // access to the real focus target
1502     // Huh? --djk
1503     onFocusChange: function onFocusChange(event) {
1504         function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument
1505         if (dactyl.ignoreFocus)
1506             return;
1507
1508         let win  = window.document.commandDispatcher.focusedWindow;
1509         let elem = window.document.commandDispatcher.focusedElement;
1510
1511         if (elem == null && Editor.getEditor(win))
1512             elem = win;
1513
1514         if (win && win.top == content && dactyl.has("tabs"))
1515             buffer.focusedFrame = win;
1516
1517         try {
1518             if (elem && elem.readOnly)
1519                 return;
1520
1521             if (isinstance(elem, [HTMLEmbedElement, HTMLEmbedElement])) {
1522                 modes.push(modes.EMBED);
1523                 return;
1524             }
1525
1526             let haveInput = modes.stack.some(function (m) m.main.input);
1527
1528             if (elem instanceof HTMLTextAreaElement
1529                || elem instanceof Element && util.computedStyle(elem).MozUserModify === "read-write"
1530                || elem == null && win && Editor.getEditor(win)) {
1531
1532                 if (modes.main == modes.VISUAL && elem.selectionEnd == elem.selectionStart)
1533                     modes.pop();
1534
1535                 if (!haveInput)
1536                     if (options["insertmode"])
1537                         modes.push(modes.INSERT);
1538                     else {
1539                         modes.push(modes.TEXT_EDIT);
1540                         if (elem.selectionEnd - elem.selectionStart > 0)
1541                             modes.push(modes.VISUAL);
1542                     }
1543
1544                 if (hasHTMLDocument(win))
1545                     buffer.lastInputField = elem;
1546                 return;
1547             }
1548
1549             if (Events.isInputElement(elem)) {
1550                 if (!haveInput)
1551                     modes.push(modes.INSERT);
1552
1553                 if (hasHTMLDocument(win))
1554                     buffer.lastInputField = elem;
1555                 return;
1556             }
1557
1558             if (config.focusChange) {
1559                 config.focusChange(win);
1560                 return;
1561             }
1562
1563             let urlbar = document.getElementById("urlbar");
1564             if (elem == null && urlbar && urlbar.inputField == this._lastFocus)
1565                 util.threadYield(true); // Why? --Kris
1566
1567             while (modes.main.ownsFocus && modes.topOfStack.params.ownsFocus != elem
1568                     && !modes.topOfStack.params.holdFocus)
1569                  modes.pop(null, { fromFocus: true });
1570         }
1571         finally {
1572             this._lastFocus = elem;
1573
1574             if (modes.main.ownsFocus)
1575                 modes.topOfStack.params.ownsFocus = elem;
1576         }
1577     },
1578
1579     onSelectionChange: function onSelectionChange(event) {
1580         let controller = document.commandDispatcher.getControllerForCommand("cmd_copy");
1581         let couldCopy = controller && controller.isCommandEnabled("cmd_copy");
1582
1583         if (modes.main == modes.VISUAL) {
1584             if (!couldCopy)
1585                 modes.pop(); // Really not ideal.
1586         }
1587         else if (couldCopy) {
1588             if (modes.main == modes.TEXT_EDIT && !options["insertmode"])
1589                 modes.push(modes.VISUAL);
1590             else if (modes.main == modes.CARET)
1591                 modes.push(modes.VISUAL);
1592         }
1593     },
1594
1595     shouldPass: function shouldPass(event)
1596         !event.noremap && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) &&
1597         options.get("passkeys").has(events.toString(event))
1598 }, {
1599     ABORT: {},
1600     KILL: true,
1601     PASS: false,
1602     PASS_THROUGH: {},
1603     WAIT: null,
1604
1605     isEscape: function isEscape(event)
1606         let (key = isString(event) ? event : events.toString(event))
1607             key === "<Esc>" || key === "<C-[>",
1608
1609     isHidden: function isHidden(elem, aggressive) {
1610         if (util.computedStyle(elem).visibility !== "visible")
1611             return true;
1612
1613         if (aggressive)
1614             for (let e = elem; e instanceof Element; e = e.parentNode) {
1615                 if (!/set$/.test(e.localName) && e.boxObject && e.boxObject.height === 0)
1616                     return true;
1617                 else if (e.namespaceURI == XUL && e.localName === "panel")
1618                     break;
1619             }
1620         return false;
1621     },
1622
1623     isInputElement: function isInputElement(elem) {
1624         return elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type) ||
1625                isinstance(elem, [HTMLEmbedElement,
1626                                  HTMLObjectElement, HTMLSelectElement,
1627                                  HTMLTextAreaElement,
1628                                  Ci.nsIDOMXULTextBoxElement]) ||
1629                elem instanceof Window && Editor.getEditor(elem);
1630     },
1631
1632     kill: function kill(event) {
1633         event.stopPropagation();
1634         event.preventDefault();
1635     }
1636 }, {
1637     commands: function () {
1638         commands.add(["delmac[ros]"],
1639             "Delete macros",
1640             function (args) {
1641                 dactyl.assert(!args.bang || !args[0], _("error.invalidArgument"));
1642
1643                 if (args.bang)
1644                     events.deleteMacros();
1645                 else if (args[0])
1646                     events.deleteMacros(args[0]);
1647                 else
1648                     dactyl.echoerr(_("error.argumentRequired"));
1649             }, {
1650                 argCount: "?",
1651                 bang: true,
1652                 completer: function (context) completion.macro(context),
1653                 literal: 0
1654             });
1655
1656         commands.add(["mac[ros]"],
1657             "List all macros",
1658             function (args) { completion.listCompleter("macro", args[0]); }, {
1659                 argCount: "?",
1660                 completer: function (context) completion.macro(context)
1661             });
1662     },
1663     completion: function () {
1664         completion.macro = function macro(context) {
1665             context.title = ["Macro", "Keys"];
1666             context.completions = [item for (item in events.getMacros())];
1667         };
1668     },
1669     mappings: function () {
1670
1671         mappings.add([modes.MAIN],
1672             ["<A-b>", "<pass-next-key-builtin>"], "Process the next key as a builtin mapping",
1673             function () {
1674                 events.processor = ProcessorStack(modes.getStack(0), mappings.hives.array, true);
1675                 events.processor.keyEvents = events.keyEvents;
1676             });
1677
1678         mappings.add([modes.MAIN],
1679             ["<C-z>", "<pass-all-keys>"], "Temporarily ignore all " + config.appName + " key bindings",
1680             function () { modes.push(modes.PASS_THROUGH); });
1681
1682         mappings.add([modes.MAIN, modes.PASS_THROUGH, modes.QUOTE],
1683             ["<C-v>", "<pass-next-key>"], "Pass through next key",
1684             function () {
1685                 if (modes.main == modes.QUOTE)
1686                     return Events.PASS;
1687                 modes.push(modes.QUOTE);
1688             });
1689
1690         mappings.add([modes.BASE],
1691             ["<CapsLock>"], "Do Nothing",
1692             function () {});
1693
1694         mappings.add([modes.BASE],
1695             ["<Nop>"], "Do nothing",
1696             function () {});
1697
1698         mappings.add([modes.BASE],
1699             ["<Pass>"], "Pass the events consumed by the last executed mapping",
1700             function ({ keypressEvents: [event] }) {
1701                 dactyl.assert(event.dactylSavedEvents,
1702                               _("event.nothingToPass"));
1703                 return function () {
1704                     events.feedevents(null, event.dactylSavedEvents,
1705                                       { skipmap: true, isMacro: true, isReplay: true });
1706                 };
1707             });
1708
1709         // macros
1710         mappings.add([modes.COMMAND],
1711             ["q", "<record-macro>"], "Record a key sequence into a macro",
1712             function ({ arg }) {
1713                 events._macroKeys.pop();
1714                 events.recording = arg;
1715             },
1716             { get arg() !modes.recording });
1717
1718         mappings.add([modes.COMMAND],
1719             ["@", "<play-macro>"], "Play a macro",
1720             function ({ arg, count }) {
1721                 count = Math.max(count, 1);
1722                 while (count--)
1723                     events.playMacro(arg);
1724             },
1725             { arg: true, count: true });
1726
1727         mappings.add([modes.COMMAND],
1728             ["<A-m>s", "<sleep>"], "Sleep for {count} milliseconds before continuing macro playback",
1729             function ({ command, count }) {
1730                 let now = Date.now();
1731                 dactyl.assert(count, _("error.countRequired", command));
1732                 if (events.feedingKeys)
1733                     util.sleep(count);
1734             },
1735             { count: true });
1736
1737         mappings.add([modes.COMMAND],
1738             ["<A-m>l", "<wait-for-page-load>"], "Wait for the current page to finish loading before continuing macro playback",
1739             function ({ count }) {
1740                 if (events.feedingKeys && !events.waitForPageLoad(count)) {
1741                     util.interrupted = true;
1742                     throw Error("Interrupted");
1743                 }
1744             },
1745             { count: true });
1746     },
1747     options: function () {
1748         const Hive = Class("Hive", {
1749             init: function init(values, map) {
1750                 this.name = "passkeys:" + map;
1751                 this.stack = MapHive.Stack(values.map(function (v) Map(v[map + "Keys"])));
1752                 function Map(keys) ({
1753                     execute: function () Events.PASS_THROUGH,
1754                     keys: keys
1755                 });
1756             },
1757
1758             get active() this.stack.length,
1759
1760             get: function get(mode, key) this.stack.mappings[key],
1761
1762             getCandidates: function getCandidates(mode, key) this.stack.candidates[key]
1763         });
1764         options.add(["passkeys", "pk"],
1765             "Pass certain keys through directly for the given URLs",
1766             "sitemap", "", {
1767                 flush: function flush() {
1768                     memoize(this, "filters", function () this.value.filter(function (f) f(buffer.documentURI)));
1769                     memoize(this, "pass", function () Set(array.flatten(this.filters.map(function (f) f.keys))));
1770                     memoize(this, "commandHive", function hive() Hive(this.filters, "command"));
1771                     memoize(this, "inputHive", function hive() Hive(this.filters, "input"));
1772                 },
1773
1774                 has: function (key) Set.has(this.pass, key) || Set.has(this.commandHive.stack.mappings, key),
1775
1776                 get pass() (this.flush(), this.pass),
1777
1778                 keepQuotes: true,
1779
1780                 setter: function (values) {
1781                     values.forEach(function (filter) {
1782                         let vals = Option.splitList(filter.result);
1783                         filter.keys = events.fromString(vals[0]).map(events.closure.toString);
1784
1785                         filter.commandKeys = vals.slice(1).map(events.closure.canonicalKeys);
1786                         filter.inputKeys = filter.commandKeys.filter(bind("test", /^<[ACM]-/));
1787                     });
1788                     this.flush();
1789                     return values;
1790                 }
1791             });
1792
1793         options.add(["strictfocus", "sf"],
1794             "Prevent scripts from focusing input elements without user intervention",
1795             "sitemap", "'chrome:*':laissez-faire,*:moderate",
1796             {
1797                 values: {
1798                     despotic: "Only allow focus changes when explicitly requested by the user",
1799                     moderate: "Allow focus changes after user-initiated focus change",
1800                     "laissez-faire": "Always allow focus changes"
1801                 }
1802             });
1803
1804         options.add(["timeout", "tmo"],
1805             "Whether to execute a shorter key command after a timeout when a longer command exists",
1806             "boolean", true);
1807
1808         options.add(["timeoutlen", "tmol"],
1809             "Maximum time (milliseconds) to wait for a longer key command when a shorter one exists",
1810             "number", 1000);
1811     },
1812     sanitizer: function () {
1813         sanitizer.addItem("macros", {
1814             description: "Saved macros",
1815             persistent: true,
1816             action: function (timespan, host) {
1817                 if (!host)
1818                     for (let [k, m] in events._macros)
1819                         if (timespan.contains(m.timeRecorded * 1000))
1820                             events._macros.remove(k);
1821             }
1822         });
1823     }
1824 });
1825
1826 // vim: set fdm=marker sw=4 ts=4 et: