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