]> git.donarmstrong.com Git - dactyl.git/blob - common/content/events.js
0b279fd275757fc435ee6643507ea4ba4080d672
[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         let main = { __proto__: mode.main, params: mode.params };
20         let keyModes = array([mode.params.keyModes, main, mode.main.allBases]).flatten().compact();
21
22         if (builtin)
23             hives = hives.filter(function (h) h.name === "builtin");
24
25         this.processors = keyModes.map(function (m) hives.map(function (h) KeyProcessor(m, h)))
26                                   .flatten().array;
27         this.ownsBuffer = !this.processors.some(function (p) p.main.ownsBuffer);
28
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;
38                 };
39             }
40
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));
45     },
46
47     notify: function () {
48         events.keyEvents = [];
49         events.processor = null;
50         if (!this.execute(Events.KILL, true)) {
51             events.processor = this;
52             events.keyEvents = this.keyEvents;
53         }
54     },
55
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),
61
62     execute: function execute(result, force) {
63
64         if (force && this.actions.length)
65             this.processors.length = 0;
66
67         if (this.ownsBuffer)
68             statusline.inputBuffer = this.processors.length ? this.buffer : "";
69
70         if (!this.processors.some(function (p) !p.extended) && this.actions.length) {
71             if (this._actions.length == 0) {
72                 dactyl.beep();
73                 events.feedingKeys = false;
74             }
75
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));
80                 }
81                 if (action !== Events.PASS)
82                     break;
83             }
84
85             result = action !== undefined ? action : Events.KILL;
86             if (action !== Events.PASS)
87                 this.processors.length = 0;
88         }
89         else if (this.processors.length) {
90             result = Events.KILL;
91             if (this.actions.length && options["timeout"])
92                 this.timer = services.Timer(this, options["timeoutlen"], services.Timer.TYPE_ONE_SHOT);
93         }
94         else if (result !== Events.KILL && !this.actions.length &&
95                  (this.events.length > 1 ||
96                      this.processors.some(function (p) !p.main.passUnknown))) {
97             result = Events.KILL;
98             if (!Events.isEscape(this.events.slice(-1)[0]))
99                 dactyl.beep();
100             events.feedingKeys = false;
101         }
102         else if (result === undefined)
103             result = Events.PASS;
104
105         events.dbg("RESULT: " + this._result(result));
106
107         if (result === Events.PASS || result === Events.PASS_THROUGH)
108             if (this.events[0].originalTarget)
109                 this.events[0].originalTarget.dactylKeyPress = undefined;
110
111         if (result !== Events.PASS || this.events.length > 1)
112             Events.kill(this.events[this.events.length - 1]);
113
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);
118             if (list.length)
119                 events.dbg("REFEED: " + list.map(events.closure.toString).join(""));
120             events.feedevents(null, list, { skipmap: true, isMacro: true, isReplay: true });
121         }
122
123         return this.processors.length === 0;
124     },
125
126     process: function process(event) {
127         if (this.timer)
128             this.timer.cancel();
129
130         let key = events.toString(event);
131         this.events.push(event);
132         if (this.keyEvents)
133             this.keyEvents.push(event);
134
135         this.buffer += key;
136
137         let actions = [];
138         let processors = [];
139
140         events.dbg("KEY: " + key + " skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay);
141
142         for (let [i, input] in Iterator(this.processors)) {
143             let res = input.process(event);
144             if (res !== Events.ABORT)
145                 var result = res;
146
147             events.dbg("RES: " + input + " " + this._result(res));
148
149             if (res === Events.KILL)
150                 break;
151
152             buffer = buffer || input.inputBuffer;
153
154             if (callable(res))
155                 actions.push(res);
156
157             if (res === Events.WAIT || input.waiting)
158                 processors.push(input);
159             if (isinstance(res, KeyProcessor))
160                 processors.push(res);
161         }
162
163         events.dbg("RESULT: " + event.getPreventDefault() + " " + this._result(result));
164         events.dbg("ACTIONS: " + actions.length + " " + this.actions.length);
165         events.dbg("PROCESSORS:", processors);
166
167         this._actions = actions;
168         this.actions = actions.concat(this.actions);
169
170         if (result === Events.KILL)
171             this.actions = [];
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)
176                         break;
177                     result = dactyl.trapErrors(input.fallthrough, input, this.events);
178                 }
179
180         this.processors = processors;
181
182         return this.execute(result, options["timeout"] && options["timeoutlen"] === 0);
183     }
184 });
185
186 var KeyProcessor = Class("KeyProcessor", {
187     init: function init(main, hive) {
188         this.main = main;
189         this.events = [];
190         this.hive = hive;
191         this.wantCount = this.main.count;
192     },
193
194     get toStringParams() [this.main.name, this.hive.name],
195
196     countStr: "",
197     command: "",
198     get count() this.countStr ? Number(this.countStr) : null,
199
200     append: function append(event) {
201         this.events.push(event);
202         let key = events.toString(event);
203
204         if (this.wantCount && !this.command &&
205                 (this.countStr ? /^[0-9]$/ : /^[1-9]$/).test(key))
206             this.countStr += key;
207         else
208             this.command += key;
209         return this.events;
210     },
211
212     process: function process(event) {
213         this.append(event);
214         this.waiting = false;
215         return this.onKeyPress(event);
216     },
217
218     execute: function execute(map, args)
219         let (self = this)
220             function execute() {
221                 if (self.preExecute)
222                     self.preExecute.apply(self, args);
223                 let res = map.execute.call(map, update({ self: self.main.params.mappingSelf || self.main.mappingSelf || map },
224                                                        args));
225                 if (self.postExecute)
226                     self.postExecute.apply(self, args);
227                 return res;
228             },
229
230     onKeyPress: function onKeyPress(event) {
231         if (event.skipmap)
232             return Events.ABORT;
233
234         if (!this.command)
235             return Events.WAIT;
236
237         var map = this.hive.get(this.main, this.command);
238         this.waiting = this.hive.getCandidates(this.main, this.command);
239         if (map) {
240             if (map.arg)
241                 return KeyArgProcessor(this, map, false, "arg");
242             else if (map.motion)
243                 return KeyArgProcessor(this, map, true, "motion");
244
245             return this.execute(map, {
246                 keyEvents: this.keyEvents,
247                 command: this.command,
248                 count: this.count,
249                 keypressEvents: this.events
250             });
251         }
252
253         if (!this.waiting)
254             return this.main.insert ? Events.PASS : Events.ABORT;
255
256         return Events.WAIT;
257     }
258 });
259
260 var KeyArgProcessor = Class("KeyArgProcessor", KeyProcessor, {
261     init: function init(input, map, wantCount, argName) {
262         init.supercall(this, input.main, input.hive);
263         this.map = map;
264         this.parent = input;
265         this.argName = argName;
266         this.wantCount = wantCount;
267     },
268
269     extended: true,
270
271     onKeyPress: function onKeyPress(event) {
272         if (Events.isEscape(event))
273             return Events.KILL;
274         if (!this.command)
275             return Events.WAIT;
276
277         let args = {
278             command: this.parent.command,
279             count:   this.count || this.parent.count,
280             events:  this.parent.events.concat(this.events)
281         };
282         args[this.argName] = this.command;
283
284         return this.execute(this.map, args);
285     }
286 });
287
288 var EventHive = Class("EventHive", Contexts.Hive, {
289     init: function init(group) {
290         init.supercall(this, group);
291         this.sessionListeners = [];
292     },
293
294     cleanup: function cleanup() {
295         this.unlisten(null);
296     },
297
298     /**
299      * Adds an event listener for this session and removes it on
300      * dactyl shutdown.
301      *
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
308      *      untrusted events.
309      */
310     listen: function (target, event, callback, capture, allowUntrusted) {
311         if (!isObject(event))
312             var [self, events] = [null, array.toObject([[event, callback]])];
313         else {
314             [self, events] = [event, event[callback || "events"]];
315             [,, capture, allowUntrusted] = arguments;
316         }
317
318         for (let [event, callback] in Iterator(events)) {
319             let args = [Cu.getWeakReference(target),
320                         event,
321                         this.wrapListener(callback, self),
322                         capture,
323                         allowUntrusted];
324
325             target.addEventListener.apply(target, args.slice(1));
326             this.sessionListeners.push(args);
327         }
328     },
329
330     /**
331      * Remove an event listener.
332      *
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.
338      */
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));
343                 return false;
344             }
345             return !args[0].get();
346         });
347     }
348 });
349
350 /**
351  * @instance events
352  */
353 var Events = Module("events", {
354     dbg: function () {},
355
356     init: function () {
357         const self = this;
358         this.keyEvents = [];
359
360         update(this, {
361             hives: contexts.Hives("events", EventHive),
362             user: contexts.hives.events.user,
363             builtin: contexts.hives.events.builtin
364         });
365
366         EventHive.prototype.wrapListener = this.closure.wrapListener;
367
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);"/>
379                 </window>
380             </e4x>.elements()
381         });
382
383         this._fullscreen = window.fullScreen;
384         this._lastFocus = null;
385         this._macroKeys = [];
386         this._lastMacro = "";
387
388         this._macros = storage.newMap("macros", { privateData: true, store: true });
389         for (let [k, m] in this._macros)
390             if (isString(m))
391                 m = { keys: m, timeRecorded: Date.now() };
392
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>") { ... }
397         this._keyTable = {
398             add: ["Plus", "Add"],
399             back_space: ["BS"],
400             count: ["count"],
401             delete: ["Del"],
402             escape: ["Esc", "Escape"],
403             insert: ["Insert", "Ins"],
404             leader: ["Leader"],
405             left_shift: ["LT", "<"],
406             nop: ["Nop"],
407             pass: ["Pass"],
408             return: ["Return", "CR", "Enter"],
409             right_shift: [">"],
410             space: ["Space", " "],
411             subtract: ["Minus", "Subtract"]
412         };
413
414         this._pseudoKeys = set(["count", "leader", "nop", "pass"]);
415
416         this._key_key = {};
417         this._code_key = {};
418         this._key_code = {};
419
420         for (let list in values(this._keyTable))
421             for (let v in values(list)) {
422                 if (v.length == 1)
423                     v = v.toLowerCase();
424                 this._key_key[v.toLowerCase()] = v;
425             }
426
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")];
431
432             if (names[0].length == 1)
433                 names[0] = names[0].toLowerCase();
434
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;
441             }
442         }
443
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";
449         }
450
451         this._activeMenubar = false;
452         this.listen(window, this, "events", true);
453
454         dactyl.registerObserver("modeChange", function () {
455             delete self.processor;
456         });
457     },
458
459     signals: {
460         "browser.locationChange": function (webProgress, request, uri) {
461             options.get("passkeys").flush();
462         }
463     },
464
465     /**
466      * Adds an event listener for this session and removes it on
467      * dactyl shutdown.
468      *
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.
474      */
475     get listen() this.builtin.closure.listen,
476     addSessionListener: deprecated("events.listen", { get: function addSessionListener() this.listen }),
477
478     /**
479      * Wraps an event listener to ensure that errors are reported.
480      */
481     wrapListener: function wrapListener(method, self) {
482         self = self || this;
483         method.wrapped = wrappedListener;
484         function wrappedListener(event) {
485             try {
486                 method.apply(self, arguments);
487             }
488             catch (e) {
489                 dactyl.reportError(e);
490                 if (e.message == "Interrupted")
491                     dactyl.echoerr(_("error.interrupted"), commandline.FORCE_SINGLELINE);
492                 else
493                     dactyl.echoerr(_("event.error", event.type, e.echoerr || e),
494                                    commandline.FORCE_SINGLELINE);
495             }
496         };
497         return wrappedListener;
498     },
499
500     /**
501      * @property {boolean} Whether synthetic key events are currently being
502      *     processed.
503      */
504     feedingKeys: false,
505
506     /**
507      * Initiates the recording of a key event macro.
508      *
509      * @param {string} macro The name for the macro.
510      */
511     _recording: null,
512     get recording() this._recording,
513
514     set recording(macro) {
515         dactyl.assert(macro == null || /[a-zA-Z0-9]/.test(macro),
516                       _("macro.invalid", macro));
517
518         modes.recording = !!macro;
519
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);
524         }
525         else if (macro) {
526             this._macroKeys = [];
527         }
528         else {
529             this._macros.set(this.recording, {
530                 keys: this._macroKeys.join(""),
531                 timeRecorded: Date.now()
532             });
533
534             dactyl.log("Recorded " + this.recording + ": " + this._macroKeys.join(""), 9);
535             dactyl.echomsg(_("macro.recorded", this.recording));
536         }
537         this._recording = macro || null;
538     },
539
540     /**
541      * Replays a macro.
542      *
543      * @param {string} The name of the macro to replay.
544      * @returns {boolean}
545      */
546     playMacro: function (macro) {
547         let res = false;
548         dactyl.assert(/^[a-zA-Z0-9@]$/.test(macro), _("macro.invalid", macro));
549
550         if (macro == "@")
551             dactyl.assert(this._lastMacro, _("macro.noPrevious"));
552         else
553             this._lastMacro = macro.toLowerCase(); // XXX: sets last played macro, even if it does not yet exist
554
555         if (this._macros.get(this._lastMacro)) {
556             try {
557                 modes.replaying = true;
558                 res = events.feedkeys(this._macros.get(this._lastMacro).keys, { noremap: true });
559             }
560             finally {
561                 modes.replaying = false;
562             }
563         }
564         else
565             // TODO: ignore this like Vim?
566             dactyl.echoerr(_("macro.noSuch", this._lastMacro));
567         return res;
568     },
569
570     /**
571      * Returns all macros matching *filter*.
572      *
573      * @param {string} filter A regular expression filter string. A null
574      *     filter selects all macros.
575      */
576     getMacros: function (filter) {
577         let re = RegExp(filter || "");
578         return ([k, m.keys] for ([k, m] in events._macros) if (re.test(k)));
579     },
580
581     /**
582      * Deletes all macros matching *filter*.
583      *
584      * @param {string} filter A regular expression filter string. A null
585      *     filter deletes all macros.
586      */
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);
592         }
593     },
594
595     /**
596      * Feeds a list of events to *target* or the originalTarget member
597      * of each event if *target* is null.
598      *
599      * @param {EventTarget} target The destination node for the events.
600      *      @optional
601      * @param {[Event]} list The events to dispatch.
602      * @param {object} extra Extra properties for processing by dactyl.
603      *      @optional
604      */
605     feedevents: function feedevents(target, list, extra) {
606         list.forEach(function _feedevent(event, i) {
607             let elem = target || event.originalTarget;
608             if (elem) {
609                 let doc = elem.ownerDocument || elem.document || elem;
610                 let evt = events.create(doc, event.type, event);
611                 events.dispatch(elem, evt, extra);
612             }
613             else if (i > 0 && event.type === "keypress")
614                 events.events.keypress.call(events, event);
615         });
616     },
617
618     /**
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.
622      *
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
625      *     "\\".
626      * @param {boolean} noremap Whether recursive mappings should be
627      *     disallowed.
628      * @param {boolean} silent Whether the command should be echoed to the
629      *     command line.
630      * @returns {boolean}
631      */
632     feedkeys: function (keys, noremap, quiet, mode) {
633         try {
634             var savedEvents = this._processor && this._processor.keyEvents;
635
636             var wasFeeding = this.feedingKeys;
637             this.feedingKeys = true;
638
639             var wasQuiet = commandline.quiet;
640             if (quiet)
641                 commandline.quiet = quiet;
642
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 });
647
648                     if (isObject(noremap))
649                         update(evt, noremap);
650                     else
651                         evt.noremap = !!noremap;
652                     evt.isMacro = true;
653                     evt.dactylMode = mode;
654                     evt.dactylSavedEvents = savedEvents;
655                     this.feedingEvent = evt;
656
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);
662                 }
663
664                 if (!this.feedingKeys)
665                     return false;
666             }
667         }
668         catch (e) {
669             util.reportError(e);
670         }
671         finally {
672             this.feedingEvent = null;
673             this.feedingKeys = wasFeeding;
674             if (quiet)
675                 commandline.quiet = wasQuiet;
676             dactyl.triggerObserver("events.doneFeeding");
677         }
678         return true;
679     },
680
681     /**
682      * Creates an actual event from a pseudo-event object.
683      *
684      * The pseudo-event object (such as may be retrieved from events.fromString)
685      * should have any properties you want the event to have.
686      *
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
690      */
691     create: function (doc, type, opts) {
692         opts = opts || {};
693         var DEFAULTS = {
694             HTML: {
695                 type: type, bubbles: true, cancelable: false
696             },
697             Key: {
698                 type: type,
699                 bubbles: true, cancelable: true,
700                 view: doc.defaultView,
701                 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
702                 keyCode: 0, charCode: 0
703             },
704             Mouse: {
705                 type: type,
706                 bubbles: true, cancelable: true,
707                 view: doc.defaultView,
708                 detail: 1,
709                 screenX: 0, screenY: 0,
710                 clientX: 0, clientY: 0,
711                 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
712                 button: 0,
713                 relatedTarget: null
714             }
715         };
716         const TYPES = {
717             change: "", input: "", submit: "",
718             click: "Mouse", mousedown: "Mouse", mouseup: "Mouse",
719             mouseover: "Mouse", mouseout: "Mouse",
720             keypress: "Key", keyup: "Key", keydown: "Key"
721         };
722         var t = TYPES[type];
723         var evt = doc.createEvent((t || "HTML") + "Events");
724
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]
728                                                                                : defaults[k]));
729         return evt;
730     },
731
732     /**
733      * Converts a user-input string of keys into a canonical
734      * representation.
735      *
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>
739      *
740      * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
741      * in macros.
742      *
743      * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
744      * of x.
745      *
746      * @param {string} keys Messy form.
747      * @param {boolean} unknownOk Whether unknown keys are passed
748      *     through rather than being converted to <lt>keyname>.
749      *     @default false
750      * @returns {string} Canonical form.
751      */
752     canonicalKeys: function (keys, unknownOk) {
753         if (arguments.length === 1)
754             unknownOk = true;
755         return events.fromString(keys, unknownOk).map(events.closure.toString).join("");
756     },
757
758     iterKeys: function (keys) {
759         let match, re = /<.*?>?>|[^<]/g;
760         while (match = re.exec(keys))
761             yield match[0];
762     },
763
764     /**
765      * Dispatches an event to an element as if it were a native event.
766      *
767      * @param {Node} target The DOM node to which to dispatch the event.
768      * @param {Event} event The event to dispatch.
769      */
770     dispatch: Class.memoize(function ()
771         util.haveGecko("2b")
772             ? function dispatch(target, event, extra) {
773                 try {
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);
780                     else {
781                         target.dispatchEvent(event);
782                         return !event.getPreventDefault();
783                     }
784                 }
785                 catch (e) {
786                     util.reportError(e);
787                 }
788                 finally {
789                     this.feedingEvent = null;
790                 }
791             }
792             : function dispatch(target, event, extra) {
793                 try {
794                     this.feedingEvent = extra;
795                     target.dispatchEvent(update(event, extra));
796                 }
797                 finally {
798                     this.feedingEvent = null;
799                 }
800             }),
801
802     get defaultTarget() dactyl.focusedElement || content.document.body || document.documentElement,
803
804     /**
805      * Converts an event string into an array of pseudo-event objects.
806      *
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.
811      *
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.
817      *
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>.
821      *     @default false
822      * @returns {Array[Object]}
823      */
824     fromString: function (input, unknownOk) {
825
826         if (arguments.length === 1)
827             unknownOk = true;
828
829         let out = [];
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" };
834
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));
842
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);
849
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;
855                         }
856
857                         evt_obj.charCode = keyname.charCodeAt(0);
858                     }
859                     else if (set.has(this._pseudoKeys, keyname)) {
860                         evt_obj.dactylString = "<" + this._key_key[keyname] + ">";
861                     }
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;
867                     }
868                     else { // spaces, control characters, and <
869                         evt_obj.keyCode = this._key_code[keyname];
870                         evt_obj.charCode = 0;
871                     }
872                 }
873                 else { // an invalid sequence starting with <, treat as a literal
874                     out = out.concat(events.fromString("<lt>" + evt_str.substr(1)));
875                     continue;
876                 }
877             }
878             else // a simple key (no <...>)
879                 evt_obj.charCode = evt_str.charCodeAt(0);
880
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>
886
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);
891
892             out.push(evt_obj);
893         }
894         return out;
895     },
896
897     /**
898      * Converts the specified event to a string in dactyl key-code
899      * notation. Returns null for an unknown event.
900      *
901      * @param {Event} event
902      * @returns {string}
903      */
904     toString: function toString(event) {
905         if (!event)
906             return toString.supercall(this);
907
908         if (event.dactylString)
909             return event.dactylString;
910
911         let key = null;
912         let modifier = "";
913
914         if (event.ctrlKey)
915             modifier += "C-";
916         if (event.altKey)
917             modifier += "A-";
918         if (event.metaKey)
919             modifier += "M-";
920
921         if (/^key/.test(event.type)) {
922             let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
923             if (charCode == 0) {
924                 if (event.keyCode in this._code_key) {
925                     key = this._code_key[event.keyCode];
926
927                     if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
928                         modifier += "S-";
929                     else if (!modifier && key.length === 1)
930                         if (event.shiftKey)
931                             key = key.toUpperCase();
932                         else
933                             key = key.toLowerCase();
934                     if (!modifier && /^[a-z0-9]$/i.test(key))
935                         return key;
936                 }
937             }
938             // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
939             //            (i.e., cntrl codes 27--31)
940             // ---
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
947             // ---
948             //
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.
955             //
956             else if (util.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
957                 if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
958                     key = "Esc";
959                     modifier = modifier.replace("C-", "");
960                 }
961                 else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
962                     key = String.fromCharCode(charCode + 64);
963             }
964             // a normal key like a, b, c, 0, etc.
965             else if (charCode > 0) {
966                 key = String.fromCharCode(charCode);
967
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)
971                         modifier += "S-";
972
973                     key = this._code_key[this._key_code[key]];
974                 }
975                 else {
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)
979                         modifier += "S-";
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)
983                         return key;
984                 }
985             }
986             if (key == null)
987                 key = this._key_key[event.dactylKeyname] || event.dactylKeyname;
988             if (key == null)
989                 return null;
990         }
991         else if (event.type == "click" || event.type == "dblclick") {
992             if (event.shiftKey)
993                 modifier += "S-";
994             if (event.type == "dblclick")
995                 modifier += "2-";
996             // TODO: triple and quadruple click
997
998             switch (event.button) {
999             case 0:
1000                 key = "LeftMouse";
1001                 break;
1002             case 1:
1003                 key = "MiddleMouse";
1004                 break;
1005             case 2:
1006                 key = "RightMouse";
1007                 break;
1008             }
1009         }
1010
1011         if (key == null)
1012             return null;
1013
1014         return "<" + modifier + key + ">";
1015     },
1016
1017     /**
1018      * Whether *key* is a key code defined to accept/execute input on the
1019      * command line.
1020      *
1021      * @param {string} key The key code to test.
1022      * @returns {boolean}
1023      */
1024     isAcceptKey: function (key) key == "<Return>" || key == "<C-j>" || key == "<C-m>",
1025
1026     /**
1027      * Whether *key* is a key code defined to reject/cancel input on the
1028      * command line.
1029      *
1030      * @param {string} key The key code to test.
1031      * @returns {boolean}
1032      */
1033     isCancelKey: function (key) key == "<Esc>" || key == "<C-[>" || key == "<C-c>",
1034
1035     isContentNode: function isContentNode(node) {
1036         let win = (node.ownerDocument || node).defaultView || node;
1037         return XPCNativeWrapper(win).top == content;
1038     },
1039
1040     /**
1041      * Waits for the current buffer to successfully finish loading. Returns
1042      * true for a successful page load otherwise false.
1043      *
1044      * @returns {boolean}
1045      */
1046     waitForPageLoad: function (time) {
1047         if (buffer.loaded)
1048             return true;
1049
1050         dactyl.echo(_("macro.loadWaiting"), commandline.DISALLOW_MULTILINE);
1051
1052         const maxWaitTime = (time || 25);
1053         util.waitFor(function () !events.feedingKeys || buffer.loaded, this, maxWaitTime * 1000, true);
1054
1055         if (!buffer.loaded)
1056             dactyl.echoerr(_("macro.loadFailed", maxWaitTime));
1057
1058         return buffer.loaded;
1059     },
1060
1061     /**
1062      * Ensures that the currently focused element is visible and blurs
1063      * it if it's not.
1064      */
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();
1073             }
1074         }
1075     },
1076
1077     events: {
1078         DOMMenuBarActive: function () {
1079             this._activeMenubar = true;
1080             if (modes.main != modes.MENU)
1081                 modes.push(modes.MENU);
1082         },
1083
1084         DOMMenuBarInactive: function () {
1085             this._activeMenubar = false;
1086             modes.remove(modes.MENU, true);
1087         },
1088
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
1096                 // input.
1097                 services.focus.clearFocus(window);
1098                 document.commandDispatcher.focusedWindow = Editor.getEditor(content) ? window : content;
1099             }
1100
1101             let hold = modes.topOfStack.params.holdFocus;
1102             if (elem == hold) {
1103                 dactyl.focus(hold);
1104                 this.timeout(function () { dactyl.focus(hold); });
1105             }
1106         },
1107
1108         // TODO: Merge with onFocusChange
1109         focus: function onFocus(event) {
1110             let elem = event.originalTarget;
1111
1112             if (event.target instanceof Ci.nsIDOMXULTextBoxElement)
1113                 if (Events.isHidden(elem, true))
1114                     elem.blur();
1115
1116             let win = (elem.ownerDocument || elem).defaultView || elem;
1117
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);
1125             }
1126         },
1127
1128         /*
1129         onFocus: function onFocus(event) {
1130             let elem = event.originalTarget;
1131             if (!(elem instanceof Element))
1132                 return;
1133             let win = elem.ownerDocument.defaultView;
1134
1135             try {
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);
1142                     else
1143                         elem.blur();
1144                 }
1145             }
1146             catch (e) {
1147                 util.dump(win, String(elem.ownerDocument), String(elem.ownerDocument && elem.ownerDocument.defaultView));
1148                 util.reportError(e);
1149             }
1150         },
1151         */
1152
1153         input: function onInput(event) {
1154             event.originalTarget.dactylKeyPress = undefined;
1155         },
1156
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();
1162
1163             let duringFeed = this.duringFeed || [];
1164             this.duringFeed = [];
1165             try {
1166                 if (this.feedingEvent)
1167                     for (let [k, v] in Iterator(this.feedingEvent))
1168                         if (!(k in event))
1169                             event[k] = v;
1170                 this.feedingEvent = null;
1171
1172                 let key = events.toString(event);
1173
1174                 // Hack to deal with <BS> and so forth not dispatching input
1175                 // events
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;
1183                     });
1184                 }
1185
1186                 if (!key)
1187                      return null;
1188
1189                 if (modes.recording && !event.isReplay)
1190                     events._macroKeys.push(key);
1191
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);
1202                         }
1203                     }
1204                     else
1205                         duringFeed.push(event);
1206
1207                     return Events.kill(event);
1208                 }
1209
1210                 if (!this.processor) {
1211                     let mode = modes.getStack(0);
1212                     if (event.dactylMode)
1213                         mode = Modes.StackElement(event.dactylMode);
1214
1215                     let ignore = false;
1216
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);
1223                         }
1224                         else if (events.shouldPass(event))
1225                             mode.params.mainMode = modes.getStack(1).main;
1226                         else
1227                             ignore = true;
1228
1229                         if (ignore && !Events.isEscape(key))
1230                             modes.pop();
1231                     }
1232                     else if (!event.isMacro && !event.noremap && events.shouldPass(event))
1233                         ignore = true;
1234
1235                     events.dbg("\n\n");
1236                     events.dbg("ON KEYPRESS " + key + " ignore: " + ignore,
1237                                event.originalTarget instanceof Element ? event.originalTarget : String(event.originalTarget));
1238
1239                     if (ignore)
1240                         return null;
1241
1242                     // FIXME: Why is this hard coded? --Kris
1243                     if (key == "<C-c>")
1244                         util.interrupted = true;
1245
1246                     this.processor = ProcessorStack(mode, mappings.hives.array, event.noremap);
1247                     this.processor.keyEvents = this.keyEvents;
1248                 }
1249
1250                 let { keyEvents, processor } = this;
1251                 this._processor = processor;
1252                 this.processor = null;
1253                 this.keyEvents = [];
1254
1255                 if (!processor.process(event)) {
1256                     this.keyEvents = keyEvents;
1257                     this.processor = processor;
1258                 }
1259
1260             }
1261             catch (e) {
1262                 dactyl.reportError(e);
1263             }
1264             finally {
1265                 [duringFeed, this.duringFeed] = [this.duringFeed, duringFeed];
1266                 if (this.feedingKeys)
1267                     this.duringFeed = this.duringFeed.concat(duringFeed);
1268                 else
1269                     for (let event in values(duringFeed))
1270                         try {
1271                             this.dispatch(event.originalTarget, event, event);
1272                         }
1273                         catch (e) {
1274                             util.reportError(e);
1275                         }
1276             }
1277         },
1278
1279         keyup: function onKeyUp(event) {
1280             this.keyEvents.push(event);
1281
1282             let pass = this.feedingEvent && this.feedingEvent.isReplay ||
1283                     event.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);
1289
1290             events.dbg("ON " + event.type.toUpperCase() + " " + this.toString(event) + " pass: " + pass);
1291
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();
1297         },
1298         keydown: function onKeyDown(event) {
1299             this.events.keyup.call(this, event);
1300         },
1301
1302         mousedown: function onMouseDown(event) {
1303             let elem = event.target;
1304             let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
1305
1306             for (; win; win = win != win.parent && win.parent)
1307                 win.document.dactylFocusAllowed = true;
1308         },
1309
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);
1315             }
1316             else if (elem.localName !== "tooltip")
1317                 if (Events.isHidden(elem)) {
1318                     if (elem.hidePopup && Events.isHidden(elem.parentNode))
1319                         elem.hidePopup();
1320                 }
1321                 else if (modes.main != modes.MENU)
1322                     modes.push(modes.MENU);
1323         },
1324
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);
1330         },
1331
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 });
1338             }
1339         }
1340     },
1341
1342     // argument "event" is deliberately not used, as i don't seem to have
1343     // access to the real focus target
1344     // Huh? --djk
1345     onFocusChange: function onFocusChange(event) {
1346         function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument
1347         if (dactyl.ignoreFocus)
1348             return;
1349
1350         let win  = window.document.commandDispatcher.focusedWindow;
1351         let elem = window.document.commandDispatcher.focusedElement;
1352
1353         if (elem == null && Editor.getEditor(win))
1354             elem = win;
1355
1356         if (win && win.top == content && dactyl.has("tabs"))
1357             buffer.focusedFrame = win;
1358
1359         try {
1360             if (elem && elem.readOnly)
1361                 return;
1362
1363             if (isinstance(elem, [HTMLEmbedElement, HTMLEmbedElement])) {
1364                 modes.push(modes.EMBED);
1365                 return;
1366             }
1367
1368             let haveInput = modes.stack.some(function (m) m.main.input);
1369
1370             if (elem instanceof HTMLTextAreaElement
1371                || elem instanceof Element && util.computedStyle(elem).MozUserModify === "read-write"
1372                || elem == null && win && Editor.getEditor(win)) {
1373
1374                 if (modes.main == modes.VISUAL && elem.selectionEnd == elem.selectionStart)
1375                     modes.pop();
1376
1377                 if (!haveInput)
1378                     if (options["insertmode"])
1379                         modes.push(modes.INSERT);
1380                     else {
1381                         modes.push(modes.TEXT_EDIT);
1382                         if (elem.selectionEnd - elem.selectionStart > 0)
1383                             modes.push(modes.VISUAL);
1384                     }
1385
1386                 if (hasHTMLDocument(win))
1387                     buffer.lastInputField = elem;
1388                 return;
1389             }
1390
1391             if (Events.isInputElement(elem)) {
1392                 if (!haveInput)
1393                     modes.push(modes.INSERT);
1394
1395                 if (hasHTMLDocument(win))
1396                     buffer.lastInputField = elem;
1397                 return;
1398             }
1399
1400             if (config.focusChange) {
1401                 config.focusChange(win);
1402                 return;
1403             }
1404
1405             let urlbar = document.getElementById("urlbar");
1406             if (elem == null && urlbar && urlbar.inputField == this._lastFocus)
1407                 util.threadYield(true); // Why? --Kris
1408
1409             while (modes.main.ownsFocus && !modes.topOfStack.params.holdFocus)
1410                  modes.pop(null, { fromFocus: true });
1411         }
1412         finally {
1413             this._lastFocus = elem;
1414         }
1415     },
1416
1417     onSelectionChange: function onSelectionChange(event) {
1418         let controller = document.commandDispatcher.getControllerForCommand("cmd_copy");
1419         let couldCopy = controller && controller.isCommandEnabled("cmd_copy");
1420
1421         if (modes.main == modes.VISUAL) {
1422             if (!couldCopy)
1423                 modes.pop(); // Really not ideal.
1424         }
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);
1430         }
1431     },
1432
1433     shouldPass: function shouldPass(event)
1434         !event.noremap && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) &&
1435         options.get("passkeys").has(events.toString(event))
1436 }, {
1437     ABORT: {},
1438     KILL: true,
1439     PASS: false,
1440     PASS_THROUGH: {},
1441     WAIT: null,
1442
1443     isEscape: function isEscape(event)
1444         let (key = isString(event) ? event : events.toString(event))
1445             key === "<Esc>" || key === "<C-[>",
1446
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)
1451                 return true;
1452         }
1453         return false;
1454     },
1455
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);
1463     },
1464
1465     kill: function kill(event) {
1466         event.stopPropagation();
1467         event.preventDefault();
1468     }
1469 }, {
1470     commands: function () {
1471         commands.add(["delmac[ros]"],
1472             "Delete macros",
1473             function (args) {
1474                 dactyl.assert(!args.bang || !args[0], _("error.invalidArgument"));
1475
1476                 if (args.bang)
1477                     events.deleteMacros();
1478                 else if (args[0])
1479                     events.deleteMacros(args[0]);
1480                 else
1481                     dactyl.echoerr(_("error.argumentRequired"));
1482             }, {
1483                 bang: true,
1484                 completer: function (context) completion.macro(context),
1485                 literal: 0
1486             });
1487
1488         commands.add(["macros"],
1489             "List all macros",
1490             function (args) { completion.listCompleter("macro", args[0]); }, {
1491                 argCount: "?",
1492                 completer: function (context) completion.macro(context)
1493             });
1494     },
1495     completion: function () {
1496         completion.macro = function macro(context) {
1497             context.title = ["Macro", "Keys"];
1498             context.completions = [item for (item in events.getMacros())];
1499         };
1500     },
1501     mappings: function () {
1502
1503         mappings.add([modes.MAIN],
1504             ["<A-b>"], "Process the next key as a builtin mapping",
1505             function () {
1506                 events.processor = ProcessorStack(modes.getStack(0), mappings.hives.array, true);
1507                 events.processor.keyEvents = events.keyEvents;
1508             });
1509
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); });
1513
1514         mappings.add([modes.MAIN],
1515             ["<C-v>", "<pass-next-key>"], "Pass through next key",
1516             function () {
1517                 if (modes.main == modes.QUOTE)
1518                     return Events.PASS;
1519                 modes.push(modes.QUOTE);
1520             });
1521
1522         mappings.add([modes.BASE],
1523             ["<Nop>"], "Do nothing",
1524             function () {});
1525
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 });
1534                 };
1535             });
1536
1537         // macros
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;
1543             },
1544             { get arg() !modes.recording });
1545
1546         mappings.add([modes.COMMAND],
1547             ["@", "<play-macro>"], "Play a macro",
1548             function ({ arg, count }) {
1549                 count = Math.max(count, 1);
1550                 while (count--)
1551                     events.playMacro(arg);
1552             },
1553             { arg: true, count: true });
1554
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)
1561                     util.sleep(count);
1562             },
1563             { count: true });
1564
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");
1571                 }
1572             },
1573             { count: true });
1574     },
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,
1582                     keys: keys
1583                 });
1584             },
1585
1586             get active() this.stack.length,
1587
1588             get: function get(mode, key) this.stack.mappings[key],
1589
1590             getCandidates: function getCandidates(mode, key) this.stack.candidates[key]
1591         });
1592         options.add(["passkeys", "pk"],
1593             "Pass certain keys through directly for the given URLs",
1594             "sitemap", "", {
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"));
1600                 },
1601
1602                 has: function (key) set.has(this.pass, key) || set.has(this.commandHive.stack.mappings, key),
1603
1604                 get pass() (this.flush(), this.pass),
1605
1606                 keepQuotes: true,
1607
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);
1612
1613                         filter.commandKeys = vals.slice(1).map(events.closure.canonicalKeys);
1614                         filter.inputKeys = filter.commandKeys.filter(/^<[ACM]-/);
1615                     });
1616                     this.flush();
1617                     return values;
1618                 }
1619             });
1620
1621         options.add(["strictfocus", "sf"],
1622             "Prevent scripts from focusing input elements without user intervention",
1623             "boolean", true);
1624
1625         options.add(["timeout", "tmo"],
1626             "Whether to execute a shorter key command after a timeout when a longer command exists",
1627             "boolean", true);
1628
1629         options.add(["timeoutlen", "tmol"],
1630             "Maximum time (milliseconds) to wait for a longer key command when a shorter one exists",
1631             "number", 1000);
1632     },
1633     sanitizer: function () {
1634         sanitizer.addItem("macros", {
1635             description: "Saved macros",
1636             persistent: true,
1637             action: function (timespan, host) {
1638                 if (!host)
1639                     for (let [k, m] in events._macros)
1640                         if (timespan.contains(m.timeRecorded * 1000))
1641                             events._macros.remove(k);
1642             }
1643         });
1644     }
1645 });
1646
1647 // vim: set fdm=marker sw=4 ts=4 et: