]> git.donarmstrong.com Git - dactyl.git/blob - common/content/events.js
finalize changelog for 7904
[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-2014 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 /**
12  * A hive used mainly for tracking event listeners and cleaning them up when a
13  * group is destroyed.
14  */
15 var EventHive = Class("EventHive", Contexts.Hive, {
16     init: function init(group) {
17         init.supercall(this, group);
18         this.sessionListeners = [];
19     },
20
21     cleanup: function cleanup() {
22         this.unlisten(null);
23     },
24
25     _events: function _events(event, callback) {
26         if (!isObject(event))
27             var [self, events] = [null, array.toObject([[event, callback]])];
28         else
29             [self, events] = [event, event[callback || "events"]];
30
31         if (hasOwnProperty(events, "input") && !hasOwnProperty(events, "dactyl-input"))
32             events["dactyl-input"] = events.input;
33
34         return [self, events];
35     },
36
37     /**
38      * Adds an event listener for this session and removes it on
39      * dactyl shutdown.
40      *
41      * @param {Element} target The element on which to listen.
42      * @param {string} event The event to listen for.
43      * @param {function} callback The function to call when the event is received.
44      * @param {boolean} capture When true, listen during the capture
45      *      phase, otherwise during the bubbling phase.
46      * @param {boolean} allowUntrusted When true, allow capturing of
47      *      untrusted events.
48      */
49     listen: function (target, event, callback, capture, allowUntrusted) {
50         var [self, events] = this._events(event, callback);
51
52         for (let [event, callback] in Iterator(events)) {
53             let args = [util.weakReference(target),
54                         util.weakReference(self),
55                         event,
56                         this.wrapListener(callback, self),
57                         capture,
58                         allowUntrusted];
59
60             target.addEventListener.apply(target, args.slice(2));
61             this.sessionListeners.push(args);
62         }
63     },
64
65     /**
66      * Remove an event listener.
67      *
68      * @param {Element} target The element on which to listen.
69      * @param {string} event The event to listen for.
70      * @param {function} callback The function to call when the event is received.
71      * @param {boolean} capture When true, listen during the capture
72      *      phase, otherwise during the bubbling phase.
73      */
74     unlisten: function (target, event, callback, capture) {
75         if (target != null)
76             var [self, events] = this._events(event, callback);
77
78         this.sessionListeners = this.sessionListeners.filter(function (args) {
79             let elem = args[0].get();
80             if (target == null || elem == target
81                                && self == args[1].get()
82                                && hasOwnProperty(events, args[2])
83                                && args[3].wrapped == events[args[2]]
84                                && args[4] == capture) {
85
86                 elem.removeEventListener.apply(elem, args.slice(2));
87                 return false;
88             }
89             return elem;
90         });
91     },
92
93     get wrapListener() events.bound.wrapListener
94 });
95
96 /**
97  * @instance events
98  */
99 var Events = Module("events", {
100     dbg: function () {},
101
102     init: function () {
103         this.keyEvents = [];
104
105         overlay.overlayWindow(window, {
106             append: [
107                 ["window", { id: document.documentElement.id, xmlns: "xul" },
108                     // http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands
109                     ["commandset", { id: "dactyl-onfocus", commandupdater: "true", events: "focus",
110                                      commandupdate: this.bound.onFocusChange }],
111                     ["commandset", { id: "dactyl-onselect", commandupdater: "true", events: "select",
112                                      commandupdate: this.bound.onSelectionChange }]]]
113         });
114
115         this._fullscreen = window.fullScreen;
116         this._lastFocus = { get: function () null };
117         this._macroKeys = [];
118         this._lastMacro = "";
119
120         this._macros = storage.newMap("registers", { privateData: true, store: true });
121         if (storage.exists("macros")) {
122             for (let [k, m] in storage.newMap("macros", { store: true }))
123                 this._macros.set(k, { text: m.keys, timestamp: m.timeRecorded * 1000 });
124             storage.remove("macros");
125         }
126
127         this.popups = {
128             active: [],
129
130             activeMenubar: null,
131
132             update: function update(elem) {
133                 if (elem) {
134                     if (elem instanceof Ci.nsIAutoCompletePopup
135                             || elem.localName == "tooltip"
136                             || !elem.popupBoxObject)
137                         return;
138
139                     if (!~this.active.indexOf(elem))
140                         this.active.push(elem);
141                 }
142
143                 this.active = this.active.filter(e => e.popupBoxObject && e.popupBoxObject.popupState != "closed");
144
145                 if (!this.active.length && !this.activeMenubar)
146                     modes.remove(modes.MENU, true);
147                 else if (modes.main != modes.MENU)
148                     modes.push(modes.MENU);
149             },
150
151             events: {
152                 DOMMenuBarActive: function onDOMMenuBarActive(event) {
153                     this.activeMenubar = event.target;
154                     if (modes.main != modes.MENU)
155                         modes.push(modes.MENU);
156                 },
157
158                 DOMMenuBarInactive: function onDOMMenuBarInactive(event) {
159                     this.activeMenubar = null;
160                     modes.remove(modes.MENU, true);
161                 },
162
163                 popupshowing: function onPopupShowing(event) {
164                     this.update(event.originalTarget);
165                 },
166
167                 popupshown: function onPopupShown(event) {
168                     let elem = event.originalTarget;
169                     this.update(elem);
170
171                     if (elem instanceof Ci.nsIAutoCompletePopup) {
172                         if (modes.main != modes.AUTOCOMPLETE)
173                             modes.push(modes.AUTOCOMPLETE);
174                     }
175                     else if (elem.hidePopup && elem.localName !== "tooltip"
176                                 && Events.isHidden(elem)
177                                 && Events.isHidden(elem.parentNode)) {
178                         elem.hidePopup();
179                     }
180                 },
181
182                 popuphidden: function onPopupHidden(event) {
183                     this.update();
184                     modes.remove(modes.AUTOCOMPLETE);
185                 }
186             }
187         };
188
189         this.listen(window, this, "events", true);
190         this.listen(window, this.popups, "events", true);
191
192         this.grabFocus = 0;
193         this.grabFocusTimer = Timer(100, 10000, () => {
194             this.grabFocus = 0;
195         });
196     },
197
198     cleanup: function cleanup() {
199         let elem = dactyl.focusedElement;
200         if (DOM(elem).isEditable)
201             util.trapErrors("removeEditActionListener",
202                             DOM(elem).editor, editor);
203     },
204
205     signals: {
206         "browser.locationChange": function (webProgress, request, uri) {
207             options.get("passkeys").flush();
208         },
209         "modes.change": function (oldMode, newMode) {
210             delete this.processor;
211         }
212     },
213
214     get listen() this.builtin.bound.listen,
215     addSessionListener: deprecated("events.listen", { get: function addSessionListener() this.listen }),
216
217     /**
218      * Wraps an event listener to ensure that errors are reported.
219      */
220     wrapListener: function wrapListener(method, self=this) {
221         method.wrapper = wrappedListener;
222         wrappedListener.wrapped = method;
223         function wrappedListener(event) {
224             try {
225                 method.apply(self, arguments);
226             }
227             catch (e) {
228                 dactyl.reportError(e);
229                 if (e.message == "Interrupted")
230                     dactyl.echoerr(_("error.interrupted"), commandline.FORCE_SINGLELINE);
231                 else
232                     dactyl.echoerr(_("event.error", event.type, e.echoerr || e),
233                                    commandline.FORCE_SINGLELINE);
234             }
235         };
236         return wrappedListener;
237     },
238
239     /**
240      * @property {boolean} Whether synthetic key events are currently being
241      *     processed.
242      */
243     feedingKeys: false,
244
245     /**
246      * Initiates the recording of a key event macro.
247      *
248      * @param {string} macro The name for the macro.
249      */
250     _recording: null,
251     get recording() this._recording,
252
253     set recording(macro) {
254         dactyl.assert(macro == null || /[a-zA-Z0-9]/.test(macro),
255                       _("macro.invalid", macro));
256
257         modes.recording = macro;
258
259         if (/[A-Z]/.test(macro)) { // Append.
260             macro = macro.toLowerCase();
261             this._macroKeys = DOM.Event.iterKeys(editor.getRegister(macro))
262                                  .toArray();
263         }
264         else if (macro) { // Record afresh.
265             this._macroKeys = [];
266         }
267         else if (this.recording) { // Save.
268             editor.setRegister(this.recording, this._macroKeys.join(""));
269
270             dactyl.log(_("macro.recorded", this.recording, this._macroKeys.join("")), 9);
271             dactyl.echomsg(_("macro.recorded", this.recording));
272         }
273         this._recording = macro || null;
274     },
275
276     /**
277      * Replays a macro.
278      *
279      * @param {string} The name of the macro to replay.
280      * @returns {boolean}
281      */
282     playMacro: function (macro) {
283         dactyl.assert(/^[a-zA-Z0-9@]$/.test(macro),
284                       _("macro.invalid", macro));
285
286         if (macro == "@")
287             dactyl.assert(this._lastMacro, _("macro.noPrevious"));
288         else
289             this._lastMacro = macro.toLowerCase(); // XXX: sets last played macro, even if it does not yet exist
290
291         let keys = editor.getRegister(this._lastMacro);
292         if (keys)
293             return modes.withSavedValues(["replaying"], function () {
294                 this.replaying = true;
295                 return events.feedkeys(keys, { noremap: true });
296             });
297
298         // TODO: ignore this like Vim?
299         dactyl.echoerr(_("macro.noSuch", this._lastMacro));
300         return false;
301     },
302
303     /**
304      * Returns all macros matching *filter*.
305      *
306      * @param {string} filter A regular expression filter string. A null
307      *     filter selects all macros.
308      */
309     getMacros: function (filter) {
310         let re = RegExp(filter || "");
311         return ([k, m.text] for ([k, m] in editor.registers) if (re.test(k)));
312     },
313
314     /**
315      * Deletes all macros matching *filter*.
316      *
317      * @param {string} filter A regular expression filter string. A null
318      *     filter deletes all macros.
319      */
320     deleteMacros: function (filter) {
321         let re = RegExp(filter || "");
322         for (let [item, ] in editor.registers) {
323             if (!filter || re.test(item))
324                 editor.registers.remove(item);
325         }
326     },
327
328     /**
329      * Feeds a list of events to *target* or the originalTarget member
330      * of each event if *target* is null.
331      *
332      * @param {EventTarget} target The destination node for the events.
333      *      @optional
334      * @param {[Event]} list The events to dispatch.
335      * @param {object} extra Extra properties for processing by dactyl.
336      *      @optional
337      */
338     feedevents: function feedevents(target, list, extra) {
339         list.forEach(function _feedevent(event, i) {
340             let elem = target || event.originalTarget;
341             if (elem) {
342                 let doc = elem.ownerDocument || elem.document || elem;
343                 let evt = DOM.Event(doc, event.type, event);
344                 DOM.Event.dispatch(elem, evt, extra);
345             }
346             else if (i > 0 && event.type === "keypress")
347                 events.events.keypress.call(events, event);
348         });
349     },
350
351     /**
352      * Pushes keys onto the event queue from dactyl. It is similar to
353      * Vim's feedkeys() method, but cannot cope with 2 partially-fed
354      * strings, you have to feed one parseable string.
355      *
356      * @param {string} keys A string like "2<C-f>" to push onto the event
357      *     queue. If you want "<" to be taken literally, prepend it with a
358      *     "\\".
359      * @param {boolean} noremap Whether recursive mappings should be
360      *     disallowed.
361      * @param {boolean} silent Whether the command should be echoed to the
362      *     command line.
363      * @returns {boolean}
364      */
365     feedkeys: function (keys, noremap, quiet, mode) {
366         try {
367             var savedEvents = this._processor && this._processor.keyEvents;
368
369             var wasFeeding = this.feedingKeys;
370             this.feedingKeys = true;
371
372             var wasQuiet = commandline.quiet;
373             if (quiet)
374                 commandline.quiet = quiet;
375
376             for (let [, evt_obj] in Iterator(DOM.Event.parse(keys))) {
377                 let now = Date.now();
378                 let key = DOM.Event.stringify(evt_obj);
379                 for (let type in values(["keydown", "keypress", "keyup"])) {
380                     let evt = update({}, evt_obj, { type: type });
381                     if (type !== "keypress" && !evt.keyCode)
382                         evt.keyCode = evt._keyCode || 0;
383
384                     if (isObject(noremap))
385                         update(evt, noremap);
386                     else
387                         evt.noremap = !!noremap;
388                     evt.isMacro = true;
389                     evt.dactylMode = mode;
390                     evt.dactylSavedEvents = savedEvents;
391                     DOM.Event.feedingEvent = evt;
392
393                     let doc = document.commandDispatcher.focusedWindow.document;
394
395                     let target = dactyl.focusedElement
396                               || ["complete", "interactive"].indexOf(doc.readyState) >= 0 && doc.documentElement
397                               || doc.defaultView;
398
399                     if (target instanceof Element && !Events.isInputElement(target) &&
400                         ["<Return>", "<Space>"].indexOf(key) == -1)
401                         target = target.ownerDocument.documentElement;
402
403                     let event = DOM.Event(doc, type, evt);
404                     if (!evt_obj.dactylString && !mode)
405                         DOM.Event.dispatch(target, event, evt);
406                     else if (type === "keypress")
407                         events.events.keypress.call(events, event);
408                 }
409
410                 if (!this.feedingKeys)
411                     return false;
412             }
413         }
414         catch (e) {
415             util.reportError(e);
416         }
417         finally {
418             DOM.Event.feedingEvent = null;
419             this.feedingKeys = wasFeeding;
420             if (quiet)
421                 commandline.quiet = wasQuiet;
422             dactyl.triggerObserver("events.doneFeeding");
423         }
424         return true;
425     },
426
427     canonicalKeys: deprecated("DOM.Event.canonicalKeys", { get: function canonicalKeys() DOM.Event.bound.canonicalKeys }),
428     create:        deprecated("DOM.Event", function create() DOM.Event.apply(null, arguments)),
429     dispatch:      deprecated("DOM.Event.dispatch", function dispatch() DOM.Event.dispatch.apply(DOM.Event, arguments)),
430     fromString:    deprecated("DOM.Event.parse", { get: function fromString() DOM.Event.bound.parse }),
431     iterKeys:      deprecated("DOM.Event.iterKeys", { get: function iterKeys() DOM.Event.bound.iterKeys }),
432
433     toString: function toString() {
434         if (!arguments.length)
435             return toString.supercall(this);
436
437         deprecated.warn(toString, "toString", "DOM.Event.stringify");
438         return DOM.Event.stringify.apply(DOM.Event, arguments);
439     },
440
441     get defaultTarget() dactyl.focusedElement || content.document.body || document.documentElement,
442
443     /**
444      * Returns true if there's a known native key handler for the given
445      * event in the given mode.
446      *
447      * @param {Event} event A keypress event.
448      * @param {Modes.Mode} mode The main mode.
449      * @param {boolean} passUnknown Whether unknown keys should be passed.
450      */
451     hasNativeKey: function hasNativeKey(event, mode, passUnknown) {
452         if (mode.input && event.charCode && !(event.ctrlKey || event.metaKey))
453             return true;
454
455         if (!passUnknown)
456             return false;
457
458         var elements = document.getElementsByTagNameNS(XUL, "key");
459         var filters = [];
460
461         if (event.keyCode)
462             filters.push(["keycode", this._code_nativeKey[event.keyCode]]);
463         if (event.charCode) {
464             let key = String.fromCharCode(event.charCode);
465             filters.push(["key", key.toUpperCase()],
466                          ["key", key.toLowerCase()]);
467         }
468
469         let accel = config.OS.isMacOSX ? "metaKey" : "ctrlKey";
470
471         let access = iter({ 1: "shiftKey", 2: "ctrlKey", 4: "altKey", 8: "metaKey" })
472                         .filter(function ([k, v]) this & k,
473                                 prefs.get("ui.key.chromeAccess"))
474                         .map(([k, v]) => [v, true])
475                         .toObject();
476
477     outer:
478         for (let [, key] in iter(elements))
479             if (filters.some(([k, v]) => key.getAttribute(k) == v)) {
480                 let keys = { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false };
481                 let needed = { ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey };
482
483                 let modifiers = (key.getAttribute("modifiers") || "").trim().split(/[\s,]+/);
484                 for (let modifier in values(modifiers))
485                     switch (modifier) {
486                     case "access": update(keys, access); break;
487                     case "accel":  keys[accel] = true; break;
488                     default:       keys[modifier + "Key"] = true; break;
489                     case "any":
490                         if (!iter.some(keys, ([k, v]) => v && needed[k]))
491                             continue outer;
492                         for (let [k, v] in iter(keys)) {
493                             if (v)
494                                 needed[k] = false;
495                             keys[k] = false;
496                         }
497                         break;
498                     }
499
500                 if (iter(needed).every(([k, v]) => (v == keys[k])))
501                     return key;
502             }
503
504         return false;
505     },
506
507     /**
508      * Returns true if *key* is a key code defined to accept/execute input on
509      * the command line.
510      *
511      * @param {string} key The key code to test.
512      * @returns {boolean}
513      */
514     isAcceptKey: function (key) key == "<Return>" || key == "<C-j>" || key == "<C-m>",
515
516     /**
517      * Returns true if *key* is a key code defined to reject/cancel input on
518      * the command line.
519      *
520      * @param {string} key The key code to test.
521      * @returns {boolean}
522      */
523     isCancelKey: function (key) key == "<Esc>" || key == "<C-[>" || key == "<C-c>",
524
525     /**
526      * Returns true if *node* belongs to the current content document or any
527      * sub-frame thereof.
528      *
529      * @param {Node|Document|Window} node The node to test.
530      * @returns {boolean}
531      */
532     isContentNode: function isContentNode(node) {
533         let win = (node.ownerDocument || node).defaultView || node;
534         return XPCNativeWrapper(win).top == content;
535     },
536
537     /**
538      * Waits for the current buffer to successfully finish loading. Returns
539      * true for a successful page load otherwise false.
540      *
541      * @returns {boolean}
542      */
543     waitForPageLoad: function (time) {
544         if (buffer.loaded)
545             return true;
546
547         dactyl.echo(_("macro.loadWaiting"), commandline.FORCE_SINGLELINE);
548
549         const maxWaitTime = (time || 25);
550         util.waitFor(() => buffer.loaded, this,
551                      maxWaitTime * 1000, true);
552
553         dactyl.echo("", commandline.FORCE_SINGLELINE);
554         if (!buffer.loaded)
555             dactyl.echoerr(_("macro.loadFailed", maxWaitTime));
556
557         return buffer.loaded;
558     },
559
560     /**
561      * Ensures that the currently focused element is visible and blurs
562      * it if it's not.
563      */
564     checkFocus: function () {
565         if (dactyl.focusedElement) {
566             let rect = dactyl.focusedElement.getBoundingClientRect();
567             if (!rect.width || !rect.height) {
568                 services.focus.clearFocus(window);
569                 document.commandDispatcher.focusedWindow = content;
570                 // onFocusChange needs to die.
571                 this.onFocusChange();
572             }
573         }
574     },
575
576     events: {
577         blur: function onBlur(event) {
578             let elem = event.originalTarget;
579             if (DOM(elem).editor)
580                 util.trapErrors("removeEditActionListener",
581                                 DOM(elem).editor, editor);
582
583             if (elem instanceof Window && services.focus.activeWindow == null
584                 && document.commandDispatcher.focusedWindow !== window) {
585                 // Deals with circumstances where, after the main window
586                 // blurs while a collapsed frame has focus, re-activating
587                 // the main window does not restore focus and we lose key
588                 // input.
589                 services.focus.clearFocus(window);
590                 document.commandDispatcher.focusedWindow = Editor.getEditor(content) ? window : content;
591             }
592
593             let hold = modes.topOfStack.params.holdFocus;
594             if (elem == hold) {
595                 dactyl.focus(hold);
596                 this.timeout(function () { dactyl.focus(hold); });
597             }
598         },
599
600         // TODO: Merge with onFocusChange
601         focus: function onFocus(event) {
602             let elem = event.originalTarget;
603             if (DOM(elem).editor)
604                 util.trapErrors("addEditActionListener",
605                                 DOM(elem).editor, editor);
606
607             if (elem == window)
608                 overlay.activeWindow = window;
609
610             overlay.setData(elem, "had-focus", true);
611             if (event.target instanceof Ci.nsIDOMXULTextBoxElement)
612                 if (Events.isHidden(elem, true))
613                     elem.blur();
614
615             let win = (elem.ownerDocument || elem).defaultView || elem;
616
617             if (!(services.focus.getLastFocusMethod(win) & 0x3000)
618                 && events.isContentNode(elem)
619                 && !buffer.focusAllowed(elem)
620                 && isinstance(elem, [Ci.nsIDOMHTMLInputElement,
621                                      Ci.nsIDOMHTMLSelectElement,
622                                      Ci.nsIDOMHTMLTextAreaElement,
623                                      Ci.nsIDOMWindow])) {
624                 if (this.grabFocus++ > 5)
625                     ; // Something is fighting us. Give up.
626                 else {
627                     this.grabFocusTimer.tell();
628                     if (elem.frameElement)
629                         dactyl.focusContent(true);
630                     else if (!(elem instanceof Window) || Editor.getEditor(elem))
631                         dactyl.focus(window);
632                 }
633             }
634
635             if (elem instanceof Element)
636                 delete overlay.getData(elem)["focus-allowed"];
637         },
638
639         /*
640         onFocus: function onFocus(event) {
641             let elem = event.originalTarget;
642             if (!(elem instanceof Element))
643                 return;
644             let win = elem.ownerDocument.defaultView;
645
646             try {
647                 util.dump(elem, services.focus.getLastFocusMethod(win) & (0x7000));
648                 if (buffer.focusAllowed(win))
649                     win.dactylLastFocus = elem;
650                 else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement])) {
651                     if (win.dactylLastFocus)
652                         dactyl.focus(win.dactylLastFocus);
653                     else
654                         elem.blur();
655                 }
656             }
657             catch (e) {
658                 util.dump(win, String(elem.ownerDocument), String(elem.ownerDocument && elem.ownerDocument.defaultView));
659                 util.reportError(e);
660             }
661         },
662         */
663
664         input: function onInput(event) {
665             event.originalTarget.dactylKeyPress = undefined;
666         },
667
668         // this keypress handler gets always called first, even if e.g.
669         // the command-line has focus
670         // TODO: ...help me...please...
671         keypress: function onKeyPress(event) {
672             event.dactylDefaultPrevented = event.defaultPrevented;
673
674             let duringFeed = this.duringFeed || [];
675             this.duringFeed = [];
676             try {
677                 let ourEvent = DOM.Event.feedingEvent;
678                 DOM.Event.feedingEvent = null;
679                 if (ourEvent)
680                     for (let [k, v] in Iterator(ourEvent))
681                         if (!(k in event))
682                             event[k] = v;
683
684                 let key = DOM.Event.stringify(ourEvent || event);
685                 event.dactylString = key;
686
687                 // Hack to deal with <BS> and so forth not dispatching input
688                 // events
689                 if (key && event.originalTarget instanceof Ci.nsIDOMHTMLInputElement && !modes.main.passthrough) {
690                     let elem = event.originalTarget;
691                     elem.dactylKeyPress = elem.value;
692                     util.timeout(function () {
693                         if (elem.dactylKeyPress !== undefined && elem.value !== elem.dactylKeyPress)
694                             DOM(elem).dactylInput();
695                         elem.dactylKeyPress = undefined;
696                     });
697                 }
698
699                 if (!key)
700                      return null;
701
702                 if (modes.recording && !event.isReplay)
703                     events._macroKeys.push(key);
704
705                 // feedingKeys needs to be separate from interrupted so
706                 // we can differentiate between a recorded <C-c>
707                 // interrupting whatever it's started and a real <C-c>
708                 // interrupting our playback.
709                 if (events.feedingKeys && !event.isMacro) {
710                     if (key == "<C-c>") {
711                         events.feedingKeys = false;
712                         if (modes.replaying) {
713                             modes.replaying = false;
714                             this.timeout(function () { dactyl.echomsg(_("macro.canceled", this._lastMacro)); }, 100);
715                         }
716                     }
717                     else
718                         duringFeed.push(event);
719
720                     return Events.kill(event);
721                 }
722
723                 if (this.processor)
724                     events.dbg("ON KEYPRESS " + key + " processor: " + this.processor,
725                                event.originalTarget instanceof Element ? event.originalTarget : String(event.originalTarget));
726                 else {
727                     let mode = modes.getStack(0);
728                     if (event.dactylMode)
729                         mode = Modes.StackElement(event.dactylMode);
730
731                     let ignore = false;
732
733                     if (mode.main == modes.PASS_THROUGH)
734                         ignore = !Events.isEscape(key) && key != "<C-v>";
735                     else if (mode.main == modes.QUOTE) {
736                         if (modes.getStack(1).main == modes.PASS_THROUGH) {
737                             mode = Modes.StackElement(modes.getStack(2).main);
738                             ignore = Events.isEscape(key);
739                         }
740                         else if (events.shouldPass(event))
741                             mode = Modes.StackElement(modes.getStack(1).main);
742                         else
743                             ignore = true;
744
745                         modes.pop();
746                     }
747                     else if (!event.isMacro && !event.noremap && events.shouldPass(event))
748                         ignore = true;
749
750                     events.dbg("\n\n");
751                     events.dbg("ON KEYPRESS " + key + " ignore: " + ignore,
752                                event.originalTarget instanceof Element ? event.originalTarget : String(event.originalTarget));
753
754                     if (ignore)
755                         return null;
756
757                     // FIXME: Why is this hard coded? --Kris
758                     if (key == "<C-c>")
759                         util.interrupted = true;
760
761                     this.processor = ProcessorStack(mode, mappings.hives.array, event.noremap);
762                     this.processor.keyEvents = this.keyEvents;
763                 }
764
765                 let { keyEvents, processor } = this;
766                 this._processor = processor;
767                 this.processor = null;
768                 this.keyEvents = [];
769
770                 if (!processor.process(event)) {
771                     this.keyEvents = keyEvents;
772                     this.processor = processor;
773                 }
774
775             }
776             catch (e) {
777                 dactyl.reportError(e);
778             }
779             finally {
780                 [duringFeed, this.duringFeed] = [this.duringFeed, duringFeed];
781                 if (this.feedingKeys)
782                     this.duringFeed = this.duringFeed.concat(duringFeed);
783                 else
784                     for (let event in values(duringFeed))
785                         try {
786                             DOM.Event.dispatch(event.originalTarget, event, event);
787                         }
788                         catch (e) {
789                             util.reportError(e);
790                         }
791             }
792         },
793
794         keyup: function onKeyUp(event) {
795             if (event.type == "keydown")
796                 this.keyEvents.push(event);
797             else if (!this.processor)
798                 this.keyEvents = [];
799
800             let pass = this.passing && !event.isMacro ||
801                     DOM.Event.feedingEvent && DOM.Event.feedingEvent.isReplay ||
802                     event.isReplay ||
803                     modes.main == modes.PASS_THROUGH ||
804                     modes.main == modes.QUOTE
805                         && modes.getStack(1).main !== modes.PASS_THROUGH
806                         && !this.shouldPass(event) ||
807                     !modes.passThrough && this.shouldPass(event) ||
808                     !this.processor && event.type === "keydown"
809                         && options.get("passunknown").getKey(modes.main.allBases)
810                         && let (key = DOM.Event.stringify(event))
811                             !(modes.main.count && /^\d$/.test(key) ||
812                               modes.main.allBases.some(
813                                 mode => mappings.hives
814                                                 .some(hive => hive.get(mode, key)
815                                                            || hive.getCandidates(mode, key))));
816
817             events.dbg("ON " + event.type.toUpperCase() + " " + DOM.Event.stringify(event) +
818                        " passing: " + this.passing + " " +
819                        " pass: " + pass +
820                        " replay: " + event.isReplay +
821                        " macro: " + event.isMacro);
822
823             if (event.type === "keydown")
824                 this.passing = pass;
825
826             // Prevents certain sites from transferring focus to an input box
827             // before we get a chance to process our key bindings on the
828             // "keypress" event.
829             if (!pass)
830                 event.stopPropagation();
831         },
832         keydown: function onKeyDown(event) {
833             if (!event.isMacro)
834                 this.passing = false;
835             this.events.keyup.call(this, event);
836         },
837
838         mousedown: function onMouseDown(event) {
839             let elem = event.target;
840             let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
841
842             for (; win; win = win != win.parent && win.parent) {
843                 for (; elem instanceof Element; elem = elem.parentNode)
844                     overlay.setData(elem, "focus-allowed", true);
845                 overlay.setData(win.document, "focus-allowed", true);
846             }
847         },
848
849         resize: function onResize(event) {
850             if (window.fullScreen != this._fullscreen) {
851                 statusline.statusBar.removeAttribute("moz-collapsed");
852                 this._fullscreen = window.fullScreen;
853                 dactyl.triggerObserver("fullscreen", this._fullscreen);
854                 autocommands.trigger("Fullscreen", { url: this._fullscreen ? "on" : "off", state: this._fullscreen });
855             }
856             statusline.updateZoomLevel();
857         }
858     },
859
860     // argument "event" is deliberately not used, as i don't seem to have
861     // access to the real focus target
862     // Huh? --djk
863     onFocusChange: util.wrapCallback(function onFocusChange(event) {
864         function hasHTMLDocument(win) win && win.document && win.document instanceof Ci.nsIDOMHTMLDocument
865         if (dactyl.ignoreFocus)
866             return;
867
868         let win  = window.document.commandDispatcher.focusedWindow;
869         let elem = window.document.commandDispatcher.focusedElement;
870
871         if (elem == null && Editor.getEditor(win))
872             elem = win;
873
874         if (win && win.top == content && dactyl.has("tabs"))
875             buffer.focusedFrame = win;
876
877         try {
878             if (elem && elem.readOnly)
879                 return;
880
881             if (isinstance(elem, [Ci.nsIDOMHTMLEmbedElement, Ci.nsIDOMHTMLEmbedElement])) {
882                 if (!modes.main.passthrough && modes.main != modes.EMBED)
883                     modes.push(modes.EMBED);
884                 return;
885             }
886
887             let haveInput = modes.stack.some(m => m.main.input);
888
889             if (DOM(elem || win).isEditable) {
890                 let e = elem || win;
891                 if (!(e instanceof Ci.nsIDOMWindow &&
892                         DOM(e.document.activeElement).style.MozUserModify != "read-write")) {
893                     if (!haveInput)
894                         if (!isinstance(modes.main, [modes.INPUT, modes.TEXT_EDIT, modes.VISUAL]))
895                             if (options["insertmode"])
896                                 modes.push(modes.INSERT);
897                             else {
898                                 modes.push(modes.TEXT_EDIT);
899                                 if (elem.selectionEnd - elem.selectionStart > 0)
900                                     modes.push(modes.VISUAL);
901                             }
902
903                     if (hasHTMLDocument(win))
904                         buffer.lastInputField = elem || win;
905                     return;
906                 }
907             }
908
909             if (elem && Events.isInputElement(elem)) {
910                 if (!haveInput)
911                     modes.push(modes.INSERT);
912
913                 if (hasHTMLDocument(win))
914                     buffer.lastInputField = elem;
915                 return;
916             }
917
918             if (config.focusChange) {
919                 config.focusChange(win);
920                 return;
921             }
922
923             let urlbar = document.getElementById("urlbar");
924             if (elem == null && urlbar && urlbar.inputField == this._lastFocus.get())
925                 util.threadYield(true); // Why? --Kris
926
927             while (modes.main.ownsFocus
928                     && let ({ ownsFocus } = modes.topOfStack.params)
929                          (!ownsFocus ||
930                              ownsFocus.get() != elem &&
931                              ownsFocus.get() != win)
932                     && !modes.topOfStack.params.holdFocus)
933                  modes.pop(null, { fromFocus: true });
934         }
935         finally {
936             this._lastFocus = util.weakReference(elem);
937
938             if (modes.main.ownsFocus)
939                 modes.topOfStack.params.ownsFocus = util.weakReference(elem);
940         }
941     }),
942
943     onSelectionChange: function onSelectionChange(event) {
944         // Ignore selection events caused by editor commands.
945         if (editor.inEditMap || modes.main == modes.OPERATOR)
946             return;
947
948         let controller = document.commandDispatcher.getControllerForCommand("cmd_copy");
949         let couldCopy = controller && controller.isCommandEnabled("cmd_copy");
950
951         if (couldCopy) {
952             if (modes.main == modes.TEXT_EDIT)
953                 modes.push(modes.VISUAL);
954             else if (modes.main == modes.CARET)
955                 modes.push(modes.VISUAL);
956         }
957     },
958
959     shouldPass: function shouldPass(event)
960         !event.noremap && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) &&
961         options.get("passkeys").has(DOM.Event.stringify(event))
962 }, {
963     ABORT: {},
964     KILL: true,
965     PASS: false,
966     PASS_THROUGH: {},
967     WAIT: null,
968
969     isEscape: function isEscape(event)
970         let (key = isString(event) ? event : DOM.Event.stringify(event))
971             key === "<Esc>" || key === "<C-[>",
972
973     isHidden: function isHidden(elem, aggressive) {
974         if (DOM(elem).style.visibility !== "visible")
975             return true;
976
977         if (aggressive)
978             for (let e = elem; e instanceof Element; e = e.parentNode) {
979                 if (!/set$/.test(e.localName) && e.boxObject && e.boxObject.height === 0)
980                     return true;
981                 else if (e.namespaceURI == XUL && e.localName === "panel")
982                     break;
983             }
984         return false;
985     },
986
987     isInputElement: function isInputElement(elem) {
988         return elem instanceof Ci.nsIDOMElement && DOM(elem).isEditable ||
989                isinstance(elem, [Ci.nsIDOMHTMLEmbedElement,
990                                  Ci.nsIDOMHTMLObjectElement,
991                                  Ci.nsIDOMHTMLSelectElement]);
992     },
993
994     kill: function kill(event) {
995         event.stopPropagation();
996         event.preventDefault();
997     }
998 }, {
999     contexts: function initContexts(dactyl, modules, window) {
1000         update(Events.prototype, {
1001             hives: contexts.Hives("events", EventHive),
1002             user: contexts.hives.events.user,
1003             builtin: contexts.hives.events.builtin
1004         });
1005     },
1006
1007     commands: function initCommands() {
1008         commands.add(["delmac[ros]"],
1009             "Delete macros",
1010             function (args) {
1011                 dactyl.assert(!args.bang || !args[0], _("error.invalidArgument"));
1012
1013                 if (args.bang)
1014                     events.deleteMacros();
1015                 else if (args[0])
1016                     events.deleteMacros(args[0]);
1017                 else
1018                     dactyl.echoerr(_("error.argumentRequired"));
1019             }, {
1020                 argCount: "?",
1021                 bang: true,
1022                 completer: function (context) completion.macro(context),
1023                 literal: 0
1024             });
1025
1026         commands.add(["mac[ros]"],
1027             "List all macros",
1028             function (args) { completion.listCompleter("macro", args[0]); }, {
1029                 argCount: "?",
1030                 completer: function (context) completion.macro(context)
1031             });
1032     },
1033     completion: function initCompletion() {
1034         completion.macro = function macro(context) {
1035             context.title = ["Macro", "Keys"];
1036             context.completions = [item for (item in events.getMacros())];
1037         };
1038     },
1039     mappings: function initMappings() {
1040
1041         mappings.add([modes.MAIN],
1042             ["<A-b>", "<pass-next-key-builtin>"], "Process the next key as a builtin mapping",
1043             function () {
1044                 events.processor = ProcessorStack(modes.getStack(0), mappings.hives.array, true);
1045                 events.processor.keyEvents = events.keyEvents;
1046             });
1047
1048         mappings.add([modes.MAIN],
1049             ["<C-z>", "<pass-all-keys>"], "Temporarily ignore all " + config.appName + " key bindings",
1050             function () {
1051                 if (modes.main != modes.PASS_THROUGH)
1052                     modes.push(modes.PASS_THROUGH);
1053             });
1054
1055         mappings.add([modes.MAIN, modes.PASS_THROUGH, modes.QUOTE],
1056             ["<C-v>", "<pass-next-key>"], "Pass through next key",
1057             function () {
1058                 if (modes.main == modes.QUOTE)
1059                     return Events.PASS;
1060                 modes.push(modes.QUOTE);
1061             });
1062
1063         mappings.add([modes.BASE],
1064             ["<CapsLock>"], "Do Nothing",
1065             function () {});
1066
1067         mappings.add([modes.BASE],
1068             ["<Nop>"], "Do nothing",
1069             function () {});
1070
1071         mappings.add([modes.BASE],
1072             ["<Pass>"], "Pass the events consumed by the last executed mapping",
1073             function ({ keypressEvents: [event] }) {
1074                 dactyl.assert(event.dactylSavedEvents,
1075                               _("event.nothingToPass"));
1076                 return function () {
1077                     events.feedevents(null, event.dactylSavedEvents,
1078                                       { skipmap: true, isMacro: true, isReplay: true });
1079                 };
1080             });
1081
1082         // macros
1083         mappings.add([modes.COMMAND],
1084             ["q", "<record-macro>"], "Record a key sequence into a macro",
1085             function ({ arg }) {
1086                 util.assert(arg == null || /^[a-z]$/i.test(arg));
1087                 events._macroKeys.pop();
1088                 events.recording = arg;
1089             },
1090             { get arg() !modes.recording });
1091
1092         mappings.add([modes.COMMAND],
1093             ["@", "<play-macro>"], "Play a macro",
1094             function ({ arg, count }) {
1095                 count = Math.max(count, 1);
1096                 while (count--)
1097                     events.playMacro(arg);
1098             },
1099             { arg: true, count: true });
1100
1101         mappings.add([modes.COMMAND],
1102             ["<A-m>s", "<sleep>"], "Sleep for {count} milliseconds before continuing macro playback",
1103             function ({ command, count }) {
1104                 let now = Date.now();
1105                 dactyl.assert(count, _("error.countRequired", command));
1106                 if (events.feedingKeys)
1107                     util.sleep(count);
1108             },
1109             { count: true });
1110
1111         mappings.add([modes.COMMAND],
1112             ["<A-m>l", "<wait-for-page-load>"], "Wait for the current page to finish loading before continuing macro playback",
1113             function ({ count }) {
1114                 if (events.feedingKeys && !events.waitForPageLoad(count)) {
1115                     util.interrupted = true;
1116                     throw Error("Interrupted");
1117                 }
1118             },
1119             { count: true });
1120     },
1121     options: function initOptions() {
1122         const Hive = Class("Hive", {
1123             init: function init(values, map) {
1124                 this.name = "passkeys:" + map;
1125                 this.stack = MapHive.Stack(values.map(v => Map(v[map + "Keys"])));
1126                 function Map(keys) ({
1127                     execute: function () Events.PASS_THROUGH,
1128                     keys: keys
1129                 });
1130             },
1131
1132             get active() this.stack.length,
1133
1134             get: function get(mode, key) this.stack.mappings[key],
1135
1136             getCandidates: function getCandidates(mode, key) this.stack.candidates[key]
1137         });
1138         options.add(["passkeys", "pk"],
1139             "Pass certain keys through directly for the given URLs",
1140             "sitemap", "", {
1141                 flush: function flush() {
1142                     memoize(this, "filters", function () this.value.filter(function (f) f(buffer.documentURI)));
1143                     memoize(this, "pass", function () RealSet(array.flatten(this.filters.map(function (f) f.keys))));
1144                     memoize(this, "commandHive", function hive() Hive(this.filters, "command"));
1145                     memoize(this, "inputHive", function hive() Hive(this.filters, "input"));
1146                 },
1147
1148                 has: function (key) this.pass.has(key) || hasOwnProperty(this.commandHive.stack.mappings, key),
1149
1150                 get pass() (this.flush(), this.pass),
1151
1152                 parse: function parse() {
1153                     let value = parse.superapply(this, arguments);
1154                     value.forEach(function (filter) {
1155                         let vals = Option.splitList(filter.result);
1156                         filter.keys = DOM.Event.parse(vals[0]).map(DOM.Event.bound.stringify);
1157
1158                         filter.commandKeys = vals.slice(1).map(DOM.Event.bound.canonicalKeys);
1159                         filter.inputKeys = filter.commandKeys.filter(bind("test", /^<[ACM]-/));
1160                     });
1161                     return value;
1162                 },
1163
1164                 keepQuotes: true,
1165
1166                 setter: function (value) {
1167                     this.flush();
1168                     return value;
1169                 }
1170             });
1171
1172         options.add(["strictfocus", "sf"],
1173             "Prevent scripts from focusing input elements without user intervention",
1174             "sitemap", "'chrome:*':laissez-faire,*:moderate",
1175             {
1176                 values: {
1177                     despotic: "Only allow focus changes when explicitly requested by the user",
1178                     moderate: "Allow focus changes after user-initiated focus change",
1179                     "laissez-faire": "Always allow focus changes"
1180                 }
1181             });
1182
1183         options.add(["timeout", "tmo"],
1184             "Whether to execute a shorter key command after a timeout when a longer command exists",
1185             "boolean", true);
1186
1187         options.add(["timeoutlen", "tmol"],
1188             "Maximum time (milliseconds) to wait for a longer key command when a shorter one exists",
1189             "number", 1000);
1190     }
1191 });
1192
1193 // vim: set fdm=marker sw=4 sts=4 ts=8 et: