]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/content/events.js
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / content / events.js
index 0b279fd275757fc435ee6643507ea4ba4080d672..117d64b5891fcd9daf7969bd8d7f6bf238b2c92a 100644 (file)
@@ -16,22 +16,27 @@ var ProcessorStack = Class("ProcessorStack", {
         this.buffer = "";
         this.events = [];
 
+        events.dbg("STACK " + mode);
+
         let main = { __proto__: mode.main, params: mode.params };
-        let keyModes = array([mode.params.keyModes, main, mode.main.allBases]).flatten().compact();
+        this.modes = array([mode.params.keyModes, main, mode.main.allBases.slice(1)]).flatten().compact();
 
         if (builtin)
             hives = hives.filter(function (h) h.name === "builtin");
 
-        this.processors = keyModes.map(function (m) hives.map(function (h) KeyProcessor(m, h)))
-                                  .flatten().array;
+        this.processors = this.modes.map(function (m) hives.map(function (h) KeyProcessor(m, h)))
+                                    .flatten().array;
         this.ownsBuffer = !this.processors.some(function (p) p.main.ownsBuffer);
 
         for (let [i, input] in Iterator(this.processors)) {
             let params = input.main.params;
+
             if (params.preExecute)
                 input.preExecute = params.preExecute;
+
             if (params.postExecute)
                 input.postExecute = params.postExecute;
+
             if (params.onKeyPress && input.hive === mappings.builtin)
                 input.fallthrough = function fallthrough(events) {
                     return params.onKeyPress(events) === false ? Events.KILL : Events.PASS;
@@ -39,15 +44,17 @@ var ProcessorStack = Class("ProcessorStack", {
             }
 
         let hive = options.get("passkeys")[this.main.input ? "inputHive" : "commandHive"];
-        if (!builtin && hive.active
-                && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)))
+        if (!builtin && hive.active && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)))
             this.processors.unshift(KeyProcessor(modes.BASE, hive));
     },
 
+    passUnknown: Class.memoize(function () options.get("passunknown").getKey(this.modes)),
+
     notify: function () {
+        events.dbg("NOTIFY()");
         events.keyEvents = [];
         events.processor = null;
-        if (!this.execute(Events.KILL, true)) {
+        if (!this.execute(undefined, true)) {
             events.processor = this;
             events.keyEvents = this.keyEvents;
         }
@@ -60,64 +67,91 @@ var ProcessorStack = Class("ProcessorStack", {
                                 callable(result) ? result.toSource().substr(0, 50) : result),
 
     execute: function execute(result, force) {
+        events.dbg("EXECUTE(" + this._result(result) + ", " + force + ") events:" + this.events.length
+                   + " processors:" + this.processors.length + " actions:" + this.actions.length);
+
+        let processors = this.processors;
+        let length = 1;
 
-        if (force && this.actions.length)
-            this.processors.length = 0;
+        if (force)
+            this.processors = [];
 
         if (this.ownsBuffer)
             statusline.inputBuffer = this.processors.length ? this.buffer : "";
 
         if (!this.processors.some(function (p) !p.extended) && this.actions.length) {
-            if (this._actions.length == 0) {
-                dactyl.beep();
-                events.feedingKeys = false;
-            }
+            // We have matching actions and no processors other than
+            // those waiting on further arguments. Execute actions as
+            // long as they continue to return PASS.
 
             for (var action in values(this.actions)) {
                 while (callable(action)) {
+                    length = action.eventLength;
                     action = dactyl.trapErrors(action);
-                    events.dbg("ACTION RES: " + this._result(action));
+                    events.dbg("ACTION RES: " + length + " " + this._result(action));
                 }
                 if (action !== Events.PASS)
                     break;
             }
 
+            // Result is the result of the last action. Unless it's
+            // PASS, kill any remaining argument processors.
             result = action !== undefined ? action : Events.KILL;
             if (action !== Events.PASS)
                 this.processors.length = 0;
         }
         else if (this.processors.length) {
+            // We're still waiting on the longest matching processor.
+            // Kill the event, set a timeout to give up waiting if applicable.
+
             result = Events.KILL;
-            if (this.actions.length && options["timeout"])
+            if (options["timeout"] && (this.actions.length || events.hasNativeKey(this.events[0], this.main, this.passUnknown)))
                 this.timer = services.Timer(this, options["timeoutlen"], services.Timer.TYPE_ONE_SHOT);
         }
         else if (result !== Events.KILL && !this.actions.length &&
-                 (this.events.length > 1 ||
-                     this.processors.some(function (p) !p.main.passUnknown))) {
-            result = Events.KILL;
+                 !(this.events[0].isReplay || this.passUnknown
+                   || this.modes.some(function (m) m.passEvent(this), this.events[0]))) {
+            // No patching processors, this isn't a fake, pass-through
+            // event, we're not in pass-through mode, and we're not
+            // choosing to pass unknown keys. Kill the event and beep.
+
+            result = Events.ABORT;
             if (!Events.isEscape(this.events.slice(-1)[0]))
                 dactyl.beep();
             events.feedingKeys = false;
         }
         else if (result === undefined)
+            // No matching processors, we're willing to pass this event,
+            // and we don't have a default action from a processor. Just
+            // pass the event.
             result = Events.PASS;
 
-        events.dbg("RESULT: " + this._result(result));
-
-        if (result === Events.PASS || result === Events.PASS_THROUGH)
-            if (this.events[0].originalTarget)
-                this.events[0].originalTarget.dactylKeyPress = undefined;
+        events.dbg("RESULT: " + length + " " + this._result(result) + "\n\n");
 
         if (result !== Events.PASS || this.events.length > 1)
-            Events.kill(this.events[this.events.length - 1]);
+            if (result !== Events.ABORT || !this.events[0].isReplay)
+                Events.kill(this.events[this.events.length - 1]);
+
+        if (result === Events.PASS_THROUGH || result === Events.PASS && this.passUnknown)
+            events.passing = true;
+
+        if (result === Events.PASS_THROUGH && this.keyEvents.length)
+            events.dbg("PASS_THROUGH:\n\t" + this.keyEvents.map(function (e) [e.type, events.toString(e)]).join("\n\t"));
 
         if (result === Events.PASS_THROUGH)
             events.feedevents(null, this.keyEvents, { skipmap: true, isMacro: true, isReplay: true });
-        else if (result === Events.PASS || result === Events.ABORT) {
+        else {
             let list = this.events.filter(function (e) e.getPreventDefault() && !e.dactylDefaultPrevented);
-            if (list.length)
-                events.dbg("REFEED: " + list.map(events.closure.toString).join(""));
-            events.feedevents(null, list, { skipmap: true, isMacro: true, isReplay: true });
+
+            if (result === Events.PASS)
+                events.dbg("PASS THROUGH: " + list.slice(0, length).filter(function (e) e.type === "keypress").map(events.closure.toString));
+            if (list.length > length)
+                events.dbg("REFEED: " + list.slice(length).filter(function (e) e.type === "keypress").map(events.closure.toString));
+
+            if (result === Events.PASS)
+                events.feedevents(null, list.slice(0, length), { skipmap: true, isMacro: true, isReplay: true });
+            if (list.length > length && this.processors.length === 0)
+                events.feedevents(null, list.slice(length));
         }
 
         return this.processors.length === 0;
@@ -137,7 +171,7 @@ var ProcessorStack = Class("ProcessorStack", {
         let actions = [];
         let processors = [];
 
-        events.dbg("KEY: " + key + " skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay);
+        events.dbg("PROCESS(" + key + ") skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay);
 
         for (let [i, input] in Iterator(this.processors)) {
             let res = input.process(event);
@@ -149,8 +183,6 @@ var ProcessorStack = Class("ProcessorStack", {
             if (res === Events.KILL)
                 break;
 
-            buffer = buffer || input.inputBuffer;
-
             if (callable(res))
                 actions.push(res);
 
@@ -162,11 +194,15 @@ var ProcessorStack = Class("ProcessorStack", {
 
         events.dbg("RESULT: " + event.getPreventDefault() + " " + this._result(result));
         events.dbg("ACTIONS: " + actions.length + " " + this.actions.length);
-        events.dbg("PROCESSORS:", processors);
+        events.dbg("PROCESSORS:", processors, "\n");
 
         this._actions = actions;
         this.actions = actions.concat(this.actions);
 
+        for (let action in values(actions))
+            if (!("eventLength" in action))
+                action.eventLength = this.events.length;
+
         if (result === Events.KILL)
             this.actions = [];
         else if (!this.actions.length && !processors.length)
@@ -220,8 +256,10 @@ var KeyProcessor = Class("KeyProcessor", {
             function execute() {
                 if (self.preExecute)
                     self.preExecute.apply(self, args);
-                let res = map.execute.call(map, update({ self: self.main.params.mappingSelf || self.main.mappingSelf || map },
-                                                       args));
+
+                args.self = self.main.params.mappingSelf || self.main.mappingSelf || map;
+                let res = map.execute.call(map, args);
+
                 if (self.postExecute)
                     self.postExecute.apply(self, args);
                 return res;
@@ -285,6 +323,10 @@ var KeyArgProcessor = Class("KeyArgProcessor", KeyProcessor, {
     }
 });
 
+/**
+ * A hive used mainly for tracking event listeners and cleaning them up when a
+ * group is destroyed.
+ */
 var EventHive = Class("EventHive", Contexts.Hive, {
     init: function init(group) {
         init.supercall(this, group);
@@ -312,9 +354,12 @@ var EventHive = Class("EventHive", Contexts.Hive, {
             var [self, events] = [null, array.toObject([[event, callback]])];
         else {
             [self, events] = [event, event[callback || "events"]];
-            [,, capture, allowUntrusted] = arguments;
+            [, , capture, allowUntrusted] = arguments;
         }
 
+        if (Set.has(events, "input") && !Set.has(events, "dactyl-input"))
+            events["dactyl-input"] = events.input;
+
         for (let [event, callback] in Iterator(events)) {
             let args = [Cu.getWeakReference(target),
                         event,
@@ -354,7 +399,6 @@ var Events = Module("events", {
     dbg: function () {},
 
     init: function () {
-        const self = this;
         this.keyEvents = [];
 
         update(this, {
@@ -369,9 +413,7 @@ var Events = Module("events", {
         util.overlayWindow(window, {
             append: <e4x xmlns={XUL}>
                 <window id={document.documentElement.id}>
-                    <!--this notifies us also of focus events in the XUL
-                        from: http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands !-->
-                    <!-- I don't think we really need this. ––Kris -->
+                    <!-- http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands -->
                     <commandset id="dactyl-onfocus" commandupdater="true" events="focus"
                                 oncommandupdate="dactyl.modules.events.onFocusChange(event);"/>
                     <commandset id="dactyl-onselect" commandupdater="true" events="select"
@@ -411,11 +453,12 @@ var Events = Module("events", {
             subtract: ["Minus", "Subtract"]
         };
 
-        this._pseudoKeys = set(["count", "leader", "nop", "pass"]);
+        this._pseudoKeys = Set(["count", "leader", "nop", "pass"]);
 
         this._key_key = {};
         this._code_key = {};
         this._key_code = {};
+        this._code_nativeKey = {};
 
         for (let list in values(this._keyTable))
             for (let v in values(list)) {
@@ -425,6 +468,8 @@ var Events = Module("events", {
             }
 
         for (let [k, v] in Iterator(KeyEvent)) {
+            this._code_nativeKey[v] = k.substr(4);
+
             k = k.substr(7).toLowerCase();
             let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
                           .replace(/^NUMPAD/, "k")];
@@ -449,29 +494,18 @@ var Events = Module("events", {
         }
 
         this._activeMenubar = false;
-        this.listen(window, this, "events", true);
-
-        dactyl.registerObserver("modeChange", function () {
-            delete self.processor;
-        });
+        this.listen(window, this, "events");
     },
 
     signals: {
         "browser.locationChange": function (webProgress, request, uri) {
             options.get("passkeys").flush();
+        },
+        "modes.change": function (oldMode, newMode) {
+            delete this.processor;
         }
     },
 
-    /**
-     * Adds an event listener for this session and removes it on
-     * dactyl shutdown.
-     *
-     * @param {Element} target The element on which to listen.
-     * @param {string} event The event to listen for.
-     * @param {function} callback The function to call when the event is received.
-     * @param {boolean} capture When true, listen during the capture
-     *      phase, otherwise during the bubbling phase.
-     */
     get listen() this.builtin.closure.listen,
     addSessionListener: deprecated("events.listen", { get: function addSessionListener() this.listen }),
 
@@ -531,7 +565,7 @@ var Events = Module("events", {
                 timeRecorded: Date.now()
             });
 
-            dactyl.log("Recorded " + this.recording + ": " + this._macroKeys.join(""), 9);
+            dactyl.log(_("macro.recorded", this.recording, this._macroKeys.join("")), 9);
             dactyl.echomsg(_("macro.recorded", this.recording));
         }
         this._recording = macro || null;
@@ -640,10 +674,15 @@ var Events = Module("events", {
             if (quiet)
                 commandline.quiet = quiet;
 
+            keys = mappings.expandLeader(keys);
+
             for (let [, evt_obj] in Iterator(events.fromString(keys))) {
                 let now = Date.now();
-                for (let type in values(["keydown", "keyup", "keypress"])) {
+                let key = events.toString(evt_obj);
+                for (let type in values(["keydown", "keypress", "keyup"])) {
                     let evt = update({}, evt_obj, { type: type });
+                    if (type !== "keypress" && !evt.keyCode)
+                        evt.keyCode = evt._keyCode || 0;
 
                     if (isObject(noremap))
                         update(evt, noremap);
@@ -654,9 +693,18 @@ var Events = Module("events", {
                     evt.dactylSavedEvents = savedEvents;
                     this.feedingEvent = evt;
 
-                    let event = events.create(document.commandDispatcher.focusedWindow.document, type, evt);
-                    if (!evt_obj.dactylString && !evt_obj.dactylShift && !mode)
-                        events.dispatch(dactyl.focusedElement || buffer.focusedFrame, event, evt);
+                    let doc = document.commandDispatcher.focusedWindow.document;
+                    let event = events.create(doc, type, evt);
+                    let target = dactyl.focusedElement
+                              || ["complete", "interactive"].indexOf(doc.readyState) >= 0 && doc.documentElement
+                              || doc.defaultView;
+
+                    if (target instanceof Element && !Events.isInputElement(target) &&
+                        ["<Return>", "<Space>"].indexOf(key) == -1)
+                        target = target.ownerDocument.documentElement;
+
+                    if (!evt_obj.dactylString && !mode)
+                        events.dispatch(target, event, evt);
                     else if (type === "keypress")
                         events.events.keypress.call(events, event);
                 }
@@ -689,8 +737,7 @@ var Events = Module("events", {
      * @param {Object} opts The pseudo-event. @optional
      */
     create: function (doc, type, opts) {
-        opts = opts || {};
-        var DEFAULTS = {
+        const DEFAULTS = {
             HTML: {
                 type: type, bubbles: true, cancelable: false
             },
@@ -713,22 +760,31 @@ var Events = Module("events", {
                 relatedTarget: null
             }
         };
-        const TYPES = {
-            change: "", input: "", submit: "",
-            click: "Mouse", mousedown: "Mouse", mouseup: "Mouse",
-            mouseover: "Mouse", mouseout: "Mouse",
-            keypress: "Key", keyup: "Key", keydown: "Key"
-        };
-        var t = TYPES[type];
+
+        opts = opts || {};
+
+        var t = this._create_types[type];
         var evt = doc.createEvent((t || "HTML") + "Events");
 
         let defaults = DEFAULTS[t || "HTML"];
-        evt["init" + t + "Event"].apply(evt, Object.keys(defaults)
-                                                   .map(function (k) k in opts ? opts[k]
-                                                                               : defaults[k]));
+
+        let args = Object.keys(defaults)
+                         .map(function (k) k in opts ? opts[k] : defaults[k]);
+
+        evt["init" + t + "Event"].apply(evt, args);
         return evt;
     },
 
+    _create_types: Class.memoize(function () iter(
+        {
+            Mouse: "click mousedown mouseout mouseover mouseup",
+            Key:   "keydown keypress keyup",
+            "":    "change dactyl-input input submit"
+        }
+    ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
+     .flatten()
+     .toObject()),
+
     /**
      * Converts a user-input string of keys into a canonical
      * representation.
@@ -755,11 +811,11 @@ var Events = Module("events", {
         return events.fromString(keys, unknownOk).map(events.closure.toString).join("");
     },
 
-    iterKeys: function (keys) {
+    iterKeys: function (keys) iter(function () {
         let match, re = /<.*?>?>|[^<]/g;
         while (match = re.exec(keys))
             yield match[0];
-    },
+    }()),
 
     /**
      * Dispatches an event to an element as if it were a native event.
@@ -829,34 +885,40 @@ var Events = Module("events", {
         let out = [];
         for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
             let evt_str = match[0];
+
             let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
                             keyCode: 0, charCode: 0, type: "keypress" };
 
-            if (evt_str.length > 1) { // <.*?>
-                let [match, modifier, keyname] = evt_str.match(/^<((?:[CSMA]-)*)(.+?)>$/i) || [false, '', ''];
-                modifier = modifier.toUpperCase();
+            if (evt_str.length == 1) {
+                evt_obj.charCode = evt_str.charCodeAt(0);
+                evt_obj._keyCode = this._key_code[evt_str[0].toLowerCase()];
+                evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
+            }
+            else {
+                let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
+                modifier = Set(modifier.toUpperCase());
                 keyname = keyname.toLowerCase();
                 evt_obj.dactylKeyname = keyname;
                 if (/^u[0-9a-f]+$/.test(keyname))
                     keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
 
                 if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
-                                this._key_code[keyname] || set.has(this._pseudoKeys, keyname))) {
-                    evt_obj.ctrlKey  = /C-/.test(modifier);
-                    evt_obj.altKey   = /A-/.test(modifier);
-                    evt_obj.shiftKey = /S-/.test(modifier);
-                    evt_obj.metaKey  = /M-/.test(modifier);
+                                this._key_code[keyname] || Set.has(this._pseudoKeys, keyname))) {
+                    evt_obj.globKey  ="*" in modifier;
+                    evt_obj.ctrlKey  ="C" in modifier;
+                    evt_obj.altKey   ="A" in modifier;
+                    evt_obj.shiftKey ="S" in modifier;
+                    evt_obj.metaKey  ="M" in modifier || "⌘" in modifier;
+                    evt_obj.dactylShift = evt_obj.shiftKey;
 
                     if (keyname.length == 1) { // normal characters
-                        if (evt_obj.shiftKey) {
+                        if (evt_obj.shiftKey)
                             keyname = keyname.toUpperCase();
-                            if (keyname == keyname.toLowerCase())
-                                evt_obj.dactylShift = true;
-                        }
 
                         evt_obj.charCode = keyname.charCodeAt(0);
+                        evt_obj._keyCode = this._key_code[keyname.toLowerCase()];
                     }
-                    else if (set.has(this._pseudoKeys, keyname)) {
+                    else if (Set.has(this._pseudoKeys, keyname)) {
                         evt_obj.dactylString = "<" + this._key_key[keyname] + ">";
                     }
                     else if (/mouse$/.test(keyname)) { // mouse events
@@ -875,8 +937,6 @@ var Events = Module("events", {
                     continue;
                 }
             }
-            else // a simple key (no <...>)
-                evt_obj.charCode = evt_str.charCodeAt(0);
 
             // TODO: make a list of characters that need keyCode and charCode somewhere
             if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
@@ -884,10 +944,10 @@ var Events = Module("events", {
             if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
                 evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
 
-            evt_obj.modifiers = (evt_obj.ctrlKey && Ci.nsIDOMNSEvent.CONTROL_MASK)
-                              | (evt_obj.altKey && Ci.nsIDOMNSEvent.ALT_MASK)
+            evt_obj.modifiers = (evt_obj.ctrlKey  && Ci.nsIDOMNSEvent.CONTROL_MASK)
+                              | (evt_obj.altKey   && Ci.nsIDOMNSEvent.ALT_MASK)
                               | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
-                              | (evt_obj.metaKey && Ci.nsIDOMNSEvent.META_MASK);
+                              | (evt_obj.metaKey  && Ci.nsIDOMNSEvent.META_MASK);
 
             out.push(evt_obj);
         }
@@ -911,6 +971,8 @@ var Events = Module("events", {
         let key = null;
         let modifier = "";
 
+        if (event.globKey)
+            modifier += "*-";
         if (event.ctrlKey)
             modifier += "C-";
         if (event.altKey)
@@ -931,6 +993,7 @@ var Events = Module("events", {
                             key = key.toUpperCase();
                         else
                             key = key.toLowerCase();
+
                     if (!modifier && /^[a-z0-9]$/i.test(key))
                         return key;
                 }
@@ -975,7 +1038,7 @@ var Events = Module("events", {
                 else {
                     // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
                     // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
-                    if (key != key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
+                    if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
                         modifier += "S-";
                     if (/^\s$/.test(key))
                         key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
@@ -983,8 +1046,11 @@ var Events = Module("events", {
                         return key;
                 }
             }
-            if (key == null)
+            if (key == null) {
+                if (event.shiftKey)
+                    modifier += "S-";
                 key = this._key_key[event.dactylKeyname] || event.dactylKeyname;
+            }
             if (key == null)
                 return null;
         }
@@ -1015,8 +1081,71 @@ var Events = Module("events", {
     },
 
     /**
-     * Whether *key* is a key code defined to accept/execute input on the
-     * command line.
+     * Returns true if there's a known native key handler for the given
+     * event in the given mode.
+     *
+     * @param {Event} event A keypress event.
+     * @param {Modes.Mode} mode The main mode.
+     * @param {boolean} passUnknown Whether unknown keys should be passed.
+     */
+    hasNativeKey: function hasNativeKey(event, mode, passUnknown) {
+        if (mode.input && event.charCode && !(event.ctrlKey || event.metaKey))
+            return true;
+
+        if (!passUnknown)
+            return false;
+
+        var elements = document.getElementsByTagNameNS(XUL, "key");
+        var filters = [];
+
+        if (event.keyCode)
+            filters.push(["keycode", this._code_nativeKey[event.keyCode]]);
+        if (event.charCode) {
+            let key = String.fromCharCode(event.charCode);
+            filters.push(["key", key.toUpperCase()],
+                         ["key", key.toLowerCase()]);
+        }
+
+        let accel = util.OS.isMacOSX ? "metaKey" : "ctrlKey";
+
+        let access = iter({ 1: "shiftKey", 2: "ctrlKey", 4: "altKey", 8: "metaKey" })
+                        .filter(function ([k, v]) this & k, prefs.get("ui.key.chromeAccess"))
+                        .map(function ([k, v]) [v, true])
+                        .toObject();
+
+    outer:
+        for (let [, key] in iter(elements))
+            if (filters.some(function ([k, v]) key.getAttribute(k) == v)) {
+                let keys = { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false };
+                let needed = { ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey };
+
+                let modifiers = (key.getAttribute("modifiers") || "").trim().split(/[\s,]+/);
+                for (let modifier in values(modifiers))
+                    switch (modifier) {
+                        case "access": update(keys, access); break;
+                        case "accel":  keys[accel] = true; break;
+                        default:       keys[modifier + "Key"] = true; break;
+                        case "any":
+                            if (!iter.some(keys, function ([k, v]) v && needed[k]))
+                                continue outer;
+                            for (let [k, v] in iter(keys)) {
+                                if (v)
+                                    needed[k] = false;
+                                keys[k] = false;
+                            }
+                            break;
+                    }
+
+                if (iter(needed).every(function ([k, v]) v == keys[k]))
+                    return key;
+            }
+
+        return false;
+    },
+
+    /**
+     * Returns true if *key* is a key code defined to accept/execute input on
+     * the command line.
      *
      * @param {string} key The key code to test.
      * @returns {boolean}
@@ -1024,14 +1153,21 @@ var Events = Module("events", {
     isAcceptKey: function (key) key == "<Return>" || key == "<C-j>" || key == "<C-m>",
 
     /**
-     * Whether *key* is a key code defined to reject/cancel input on the
-     * command line.
+     * Returns true if *key* is a key code defined to reject/cancel input on
+     * the command line.
      *
      * @param {string} key The key code to test.
      * @returns {boolean}
      */
     isCancelKey: function (key) key == "<Esc>" || key == "<C-[>" || key == "<C-c>",
 
+    /**
+     * Returns true if *node* belongs to the current content document or any
+     * sub-frame thereof.
+     *
+     * @param {Node|Document|Window} node The node to test.
+     * @returns {boolean}
+     */
     isContentNode: function isContentNode(node) {
         let win = (node.ownerDocument || node).defaultView || node;
         return XPCNativeWrapper(win).top == content;
@@ -1047,11 +1183,12 @@ var Events = Module("events", {
         if (buffer.loaded)
             return true;
 
-        dactyl.echo(_("macro.loadWaiting"), commandline.DISALLOW_MULTILINE);
+        dactyl.echo(_("macro.loadWaiting"), commandline.FORCE_SINGLELINE);
 
         const maxWaitTime = (time || 25);
-        util.waitFor(function () !events.feedingKeys || buffer.loaded, this, maxWaitTime * 1000, true);
+        util.waitFor(function () buffer.loaded, this, maxWaitTime * 1000, true);
 
+        dactyl.echo("", commandline.FORCE_SINGLELINE);
         if (!buffer.loaded)
             dactyl.echoerr(_("macro.loadFailed", maxWaitTime));
 
@@ -1115,14 +1252,19 @@ var Events = Module("events", {
 
             let win = (elem.ownerDocument || elem).defaultView || elem;
 
-            if (events.isContentNode(elem) && !buffer.focusAllowed(elem)
-                && !(services.focus.getLastFocusMethod(win) & 0x7000)
+            if (!(services.focus.getLastFocusMethod(win) & 0x7000)
+                && events.isContentNode(elem)
+                && !buffer.focusAllowed(elem)
                 && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window])) {
+
                 if (elem.frameElement)
                     dactyl.focusContent(true);
                 else if (!(elem instanceof Window) || Editor.getEditor(elem))
                     dactyl.focus(window);
             }
+
+            if (elem instanceof Element)
+                elem.dactylFocusAllowed = undefined;
         },
 
         /*
@@ -1178,7 +1320,7 @@ var Events = Module("events", {
                     elem.dactylKeyPress = elem.value;
                     util.timeout(function () {
                         if (elem.dactylKeyPress !== undefined && elem.value !== elem.dactylKeyPress)
-                            events.dispatch(elem, events.create(elem.ownerDocument, "input"));
+                            events.dispatch(elem, events.create(elem.ownerDocument, "dactyl-input"));
                         elem.dactylKeyPress = undefined;
                     });
                 }
@@ -1214,20 +1356,19 @@ var Events = Module("events", {
 
                     let ignore = false;
 
-                    if (modes.main == modes.PASS_THROUGH)
+                    if (mode.main == modes.PASS_THROUGH)
                         ignore = !Events.isEscape(key) && key != "<C-v>";
-                    else if (modes.main == modes.QUOTE) {
+                    else if (mode.main == modes.QUOTE) {
                         if (modes.getStack(1).main == modes.PASS_THROUGH) {
-                            mode.params.mainMode = modes.getStack(2).main;
+                            mode = Modes.StackElement(modes.getStack(2).main);
                             ignore = Events.isEscape(key);
                         }
                         else if (events.shouldPass(event))
-                            mode.params.mainMode = modes.getStack(1).main;
+                            mode = Modes.StackElement(modes.getStack(1).main);
                         else
                             ignore = true;
 
-                        if (ignore && !Events.isEscape(key))
-                            modes.pop();
+                        modes.pop();
                     }
                     else if (!event.isMacro && !event.noremap && events.shouldPass(event))
                         ignore = true;
@@ -1277,25 +1418,40 @@ var Events = Module("events", {
         },
 
         keyup: function onKeyUp(event) {
-            this.keyEvents.push(event);
+            if (event.type == "keydown")
+                this.keyEvents.push(event);
+            else if (!this.processor)
+                this.keyEvents = [];
 
-            let pass = this.feedingEvent && this.feedingEvent.isReplay ||
+            let pass = this.passing && !event.isMacro ||
+                    this.feedingEvent && this.feedingEvent.isReplay ||
                     event.isReplay ||
                     modes.main == modes.PASS_THROUGH ||
                     modes.main == modes.QUOTE
                         && modes.getStack(1).main !== modes.PASS_THROUGH
                         && !this.shouldPass(event) ||
-                    !modes.passThrough && this.shouldPass(event);
+                    !modes.passThrough && this.shouldPass(event) ||
+                    !this.processor && event.type === "keydown"
+                        && options.get("passunknown").getKey(modes.main.allBases)
+                        && let (key = events.toString(event))
+                            !modes.main.allBases.some(
+                                function (mode) mappings.hives.some(
+                                    function (hive) hive.get(mode, key) || hive.getCandidates(mode, key)));
+
+            if (event.type === "keydown")
+                this.passing = pass;
 
-            events.dbg("ON " + event.type.toUpperCase() + " " + this.toString(event) + " pass: " + pass);
+            events.dbg("ON " + event.type.toUpperCase() + " " + this.toString(event) + " pass: " + pass + " replay: " + event.isReplay + " macro: " + event.isMacro);
 
             // Prevents certain sites from transferring focus to an input box
             // before we get a chance to process our key bindings on the
             // "keypress" event.
-            if (!pass && !Events.isInputElement(dactyl.focusedElement))
+            if (!pass)
                 event.stopPropagation();
         },
         keydown: function onKeyDown(event) {
+            if (!event.isMacro)
+                this.passing = false;
             this.events.keyup.call(this, event);
         },
 
@@ -1303,8 +1459,11 @@ var Events = Module("events", {
             let elem = event.target;
             let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
 
-            for (; win; win = win != win.parent && win.parent)
+            for (; win; win = win != win.parent && win.parent) {
+                for (; elem instanceof Element; elem = elem.parentNode)
+                    elem.dactylFocusAllowed = true;
                 win.document.dactylFocusAllowed = true;
+            }
         },
 
         popupshown: function onPopupShown(event) {
@@ -1322,8 +1481,7 @@ var Events = Module("events", {
                     modes.push(modes.MENU);
         },
 
-        popuphidden: function onPopupHidden() {
-            // gContextMenu is set to NULL, when a context menu is closed
+        popuphidden: function onPopupHidden(event) {
             if (window.gContextMenu == null && !this._activeMenubar)
                 modes.remove(modes.MENU, true);
             modes.remove(modes.AUTOCOMPLETE);
@@ -1406,11 +1564,15 @@ var Events = Module("events", {
             if (elem == null && urlbar && urlbar.inputField == this._lastFocus)
                 util.threadYield(true); // Why? --Kris
 
-            while (modes.main.ownsFocus && !modes.topOfStack.params.holdFocus)
+            while (modes.main.ownsFocus && modes.topOfStack.params.ownsFocus != elem
+                    && !modes.topOfStack.params.holdFocus)
                  modes.pop(null, { fromFocus: true });
         }
         finally {
             this._lastFocus = elem;
+
+            if (modes.main.ownsFocus)
+                modes.topOfStack.params.ownsFocus = elem;
         }
     },
 
@@ -1445,20 +1607,25 @@ var Events = Module("events", {
             key === "<Esc>" || key === "<C-[>",
 
     isHidden: function isHidden(elem, aggressive) {
-        for (let e = elem; e instanceof Element; e = e.parentNode) {
-            if (util.computedStyle(e).visibility !== "visible" ||
-                    aggressive && e.boxObject && e.boxObject.height === 0)
-                return true;
-        }
+        if (util.computedStyle(elem).visibility !== "visible")
+            return true;
+
+        if (aggressive)
+            for (let e = elem; e instanceof Element; e = e.parentNode) {
+                if (!/set$/.test(e.localName) && e.boxObject && e.boxObject.height === 0)
+                    return true;
+                else if (e.namespaceURI == XUL && e.localName === "panel")
+                    break;
+            }
         return false;
     },
 
     isInputElement: function isInputElement(elem) {
-        return elem instanceof HTMLInputElement && set.has(util.editableInputs, elem.type) ||
-               isinstance(elem, [HTMLIsIndexElement, HTMLEmbedElement,
+        return elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type) ||
+               isinstance(elem, [HTMLEmbedElement,
                                  HTMLObjectElement, HTMLSelectElement,
                                  HTMLTextAreaElement,
-                                 Ci.nsIDOMXULTreeElement, Ci.nsIDOMXULTextBoxElement]) ||
+                                 Ci.nsIDOMXULTextBoxElement]) ||
                elem instanceof Window && Editor.getEditor(elem);
     },
 
@@ -1480,12 +1647,13 @@ var Events = Module("events", {
                 else
                     dactyl.echoerr(_("error.argumentRequired"));
             }, {
+                argCount: "?",
                 bang: true,
                 completer: function (context) completion.macro(context),
                 literal: 0
             });
 
-        commands.add(["macros"],
+        commands.add(["mac[ros]"],
             "List all macros",
             function (args) { completion.listCompleter("macro", args[0]); }, {
                 argCount: "?",
@@ -1501,7 +1669,7 @@ var Events = Module("events", {
     mappings: function () {
 
         mappings.add([modes.MAIN],
-            ["<A-b>"], "Process the next key as a builtin mapping",
+            ["<A-b>", "<pass-next-key-builtin>"], "Process the next key as a builtin mapping",
             function () {
                 events.processor = ProcessorStack(modes.getStack(0), mappings.hives.array, true);
                 events.processor.keyEvents = events.keyEvents;
@@ -1511,7 +1679,7 @@ var Events = Module("events", {
             ["<C-z>", "<pass-all-keys>"], "Temporarily ignore all " + config.appName + " key bindings",
             function () { modes.push(modes.PASS_THROUGH); });
 
-        mappings.add([modes.MAIN],
+        mappings.add([modes.MAIN, modes.PASS_THROUGH, modes.QUOTE],
             ["<C-v>", "<pass-next-key>"], "Pass through next key",
             function () {
                 if (modes.main == modes.QUOTE)
@@ -1519,6 +1687,10 @@ var Events = Module("events", {
                 modes.push(modes.QUOTE);
             });
 
+        mappings.add([modes.BASE],
+            ["<CapsLock>"], "Do Nothing",
+            function () {});
+
         mappings.add([modes.BASE],
             ["<Nop>"], "Do nothing",
             function () {});
@@ -1594,12 +1766,12 @@ var Events = Module("events", {
             "sitemap", "", {
                 flush: function flush() {
                     memoize(this, "filters", function () this.value.filter(function (f) f(buffer.documentURI)));
-                    memoize(this, "pass", function () set(array.flatten(this.filters.map(function (f) f.keys))));
+                    memoize(this, "pass", function () Set(array.flatten(this.filters.map(function (f) f.keys))));
                     memoize(this, "commandHive", function hive() Hive(this.filters, "command"));
                     memoize(this, "inputHive", function hive() Hive(this.filters, "input"));
                 },
 
-                has: function (key) set.has(this.pass, key) || set.has(this.commandHive.stack.mappings, key),
+                has: function (key) Set.has(this.pass, key) || Set.has(this.commandHive.stack.mappings, key),
 
                 get pass() (this.flush(), this.pass),
 
@@ -1611,7 +1783,7 @@ var Events = Module("events", {
                         filter.keys = events.fromString(vals[0]).map(events.closure.toString);
 
                         filter.commandKeys = vals.slice(1).map(events.closure.canonicalKeys);
-                        filter.inputKeys = filter.commandKeys.filter(/^<[ACM]-/);
+                        filter.inputKeys = filter.commandKeys.filter(bind("test", /^<[ACM]-/));
                     });
                     this.flush();
                     return values;
@@ -1620,7 +1792,14 @@ var Events = Module("events", {
 
         options.add(["strictfocus", "sf"],
             "Prevent scripts from focusing input elements without user intervention",
-            "boolean", true);
+            "sitemap", "'chrome:*':laissez-faire,*:moderate",
+            {
+                values: {
+                    despotic: "Only allow focus changes when explicitly requested by the user",
+                    moderate: "Allow focus changes after user-initiated focus change",
+                    "laissez-faire": "Always allow focus changes"
+                }
+            });
 
         options.add(["timeout", "tmo"],
             "Whether to execute a shorter key command after a timeout when a longer command exists",