]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/content/events.js
Import 1.0rc1 supporting Firefox up to 11.*
[dactyl.git] / common / content / events.js
index 117d64b5891fcd9daf7969bd8d7f6bf238b2c92a..4b1841d916036233bf06ffe07537fbcfc4bf8573 100644 (file)
 //
 // This work is licensed for reuse under an MIT license. Details are
 // given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
 
 /** @scope modules */
 
-var ProcessorStack = Class("ProcessorStack", {
-    init: function (mode, hives, builtin) {
-        this.main = mode.main;
-        this._actions = [];
-        this.actions = [];
-        this.buffer = "";
-        this.events = [];
-
-        events.dbg("STACK " + mode);
-
-        let main = { __proto__: mode.main, params: mode.params };
-        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 = 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;
-                };
-            }
-
-        let hive = options.get("passkeys")[this.main.input ? "inputHive" : "commandHive"];
-        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(undefined, true)) {
-            events.processor = this;
-            events.keyEvents = this.keyEvents;
-        }
-    },
-
-    _result: function (result) (result === Events.KILL         ? "KILL"  :
-                                result === Events.PASS         ? "PASS"  :
-                                result === Events.PASS_THROUGH ? "PASS_THROUGH"  :
-                                result === Events.ABORT        ? "ABORT" :
-                                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.processors = [];
-
-        if (this.ownsBuffer)
-            statusline.inputBuffer = this.processors.length ? this.buffer : "";
-
-        if (!this.processors.some(function (p) !p.extended) && this.actions.length) {
-            // 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: " + 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 (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[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: " + length + " " + this._result(result) + "\n\n");
-
-        if (result !== Events.PASS || 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 {
-            let list = this.events.filter(function (e) e.getPreventDefault() && !e.dactylDefaultPrevented);
-
-            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;
-    },
-
-    process: function process(event) {
-        if (this.timer)
-            this.timer.cancel();
-
-        let key = events.toString(event);
-        this.events.push(event);
-        if (this.keyEvents)
-            this.keyEvents.push(event);
-
-        this.buffer += key;
-
-        let actions = [];
-        let processors = [];
-
-        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);
-            if (res !== Events.ABORT)
-                var result = res;
-
-            events.dbg("RES: " + input + " " + this._result(res));
-
-            if (res === Events.KILL)
-                break;
-
-            if (callable(res))
-                actions.push(res);
-
-            if (res === Events.WAIT || input.waiting)
-                processors.push(input);
-            if (isinstance(res, KeyProcessor))
-                processors.push(res);
-        }
-
-        events.dbg("RESULT: " + event.getPreventDefault() + " " + this._result(result));
-        events.dbg("ACTIONS: " + actions.length + " " + this.actions.length);
-        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)
-            for (let input in values(this.processors))
-                if (input.fallthrough) {
-                    if (result === Events.KILL)
-                        break;
-                    result = dactyl.trapErrors(input.fallthrough, input, this.events);
-                }
-
-        this.processors = processors;
-
-        return this.execute(result, options["timeout"] && options["timeoutlen"] === 0);
-    }
-});
-
-var KeyProcessor = Class("KeyProcessor", {
-    init: function init(main, hive) {
-        this.main = main;
-        this.events = [];
-        this.hive = hive;
-        this.wantCount = this.main.count;
-    },
-
-    get toStringParams() [this.main.name, this.hive.name],
-
-    countStr: "",
-    command: "",
-    get count() this.countStr ? Number(this.countStr) : null,
-
-    append: function append(event) {
-        this.events.push(event);
-        let key = events.toString(event);
-
-        if (this.wantCount && !this.command &&
-                (this.countStr ? /^[0-9]$/ : /^[1-9]$/).test(key))
-            this.countStr += key;
-        else
-            this.command += key;
-        return this.events;
-    },
-
-    process: function process(event) {
-        this.append(event);
-        this.waiting = false;
-        return this.onKeyPress(event);
-    },
-
-    execute: function execute(map, args)
-        let (self = this)
-            function execute() {
-                if (self.preExecute)
-                    self.preExecute.apply(self, 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;
-            },
-
-    onKeyPress: function onKeyPress(event) {
-        if (event.skipmap)
-            return Events.ABORT;
-
-        if (!this.command)
-            return Events.WAIT;
-
-        var map = this.hive.get(this.main, this.command);
-        this.waiting = this.hive.getCandidates(this.main, this.command);
-        if (map) {
-            if (map.arg)
-                return KeyArgProcessor(this, map, false, "arg");
-            else if (map.motion)
-                return KeyArgProcessor(this, map, true, "motion");
-
-            return this.execute(map, {
-                keyEvents: this.keyEvents,
-                command: this.command,
-                count: this.count,
-                keypressEvents: this.events
-            });
-        }
-
-        if (!this.waiting)
-            return this.main.insert ? Events.PASS : Events.ABORT;
-
-        return Events.WAIT;
-    }
-});
-
-var KeyArgProcessor = Class("KeyArgProcessor", KeyProcessor, {
-    init: function init(input, map, wantCount, argName) {
-        init.supercall(this, input.main, input.hive);
-        this.map = map;
-        this.parent = input;
-        this.argName = argName;
-        this.wantCount = wantCount;
-    },
-
-    extended: true,
-
-    onKeyPress: function onKeyPress(event) {
-        if (Events.isEscape(event))
-            return Events.KILL;
-        if (!this.command)
-            return Events.WAIT;
-
-        let args = {
-            command: this.parent.command,
-            count:   this.count || this.parent.count,
-            events:  this.parent.events.concat(this.events)
-        };
-        args[this.argName] = this.command;
-
-        return this.execute(this.map, args);
-    }
-});
-
 /**
  * A hive used mainly for tracking event listeners and cleaning them up when a
  * group is destroyed.
@@ -337,6 +22,18 @@ var EventHive = Class("EventHive", Contexts.Hive, {
         this.unlisten(null);
     },
 
+    _events: function _events(event, callback) {
+        if (!isObject(event))
+            var [self, events] = [null, array.toObject([[event, callback]])];
+        else
+            [self, events] = [event, event[callback || "events"]];
+
+        if (Set.has(events, "input") && !Set.has(events, "dactyl-input"))
+            events["dactyl-input"] = events.input;
+
+        return [self, events];
+    },
+
     /**
      * Adds an event listener for this session and removes it on
      * dactyl shutdown.
@@ -350,24 +47,17 @@ var EventHive = Class("EventHive", Contexts.Hive, {
      *      untrusted events.
      */
     listen: function (target, event, callback, capture, allowUntrusted) {
-        if (!isObject(event))
-            var [self, events] = [null, array.toObject([[event, callback]])];
-        else {
-            [self, events] = [event, event[callback || "events"]];
-            [, , capture, allowUntrusted] = arguments;
-        }
-
-        if (Set.has(events, "input") && !Set.has(events, "dactyl-input"))
-            events["dactyl-input"] = events.input;
+        var [self, events] = this._events(event, callback);
 
         for (let [event, callback] in Iterator(events)) {
-            let args = [Cu.getWeakReference(target),
+            let args = [util.weakReference(target),
+                        util.weakReference(self),
                         event,
                         this.wrapListener(callback, self),
                         capture,
                         allowUntrusted];
 
-            target.addEventListener.apply(target, args.slice(1));
+            target.addEventListener.apply(target, args.slice(2));
             this.sessionListeners.push(args);
         }
     },
@@ -382,14 +72,25 @@ var EventHive = Class("EventHive", Contexts.Hive, {
      *      phase, otherwise during the bubbling phase.
      */
     unlisten: function (target, event, callback, capture) {
+        if (target != null)
+            var [self, events] = this._events(event, callback);
+
         this.sessionListeners = this.sessionListeners.filter(function (args) {
-            if (target == null || args[0].get() == target && args[1] == event && args[2] == callback && args[3] == capture) {
-                args[0].get().removeEventListener.apply(args[0].get(), args.slice(1));
+            let elem = args[0].get();
+            if (target == null || elem == target
+                               && self == args[1].get()
+                               && Set.has(events, args[2])
+                               && args[3].wrapped == events[args[2]]
+                               && args[4] == capture) {
+
+                elem.removeEventListener.apply(elem, args.slice(2));
                 return false;
             }
-            return !args[0].get();
+            return elem;
         });
-    }
+    },
+
+    get wrapListener() events.closure.wrapListener
 });
 
 /**
@@ -401,16 +102,8 @@ var Events = Module("events", {
     init: function () {
         this.keyEvents = [];
 
-        update(this, {
-            hives: contexts.Hives("events", EventHive),
-            user: contexts.hives.events.user,
-            builtin: contexts.hives.events.builtin
-        });
-
-        EventHive.prototype.wrapListener = this.closure.wrapListener;
-
         XML.ignoreWhitespace = true;
-        util.overlayWindow(window, {
+        overlay.overlayWindow(window, {
             append: <e4x xmlns={XUL}>
                 <window id={document.documentElement.id}>
                     <!-- http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands -->
@@ -427,74 +120,84 @@ var Events = Module("events", {
         this._macroKeys = [];
         this._lastMacro = "";
 
-        this._macros = storage.newMap("macros", { privateData: true, store: true });
-        for (let [k, m] in this._macros)
-            if (isString(m))
-                m = { keys: m, timeRecorded: Date.now() };
-
-        // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
-        //       matters, so use that string as the first item, that you
-        //       want to refer to within dactyl's source code for
-        //       comparisons like if (key == "<Esc>") { ... }
-        this._keyTable = {
-            add: ["Plus", "Add"],
-            back_space: ["BS"],
-            count: ["count"],
-            delete: ["Del"],
-            escape: ["Esc", "Escape"],
-            insert: ["Insert", "Ins"],
-            leader: ["Leader"],
-            left_shift: ["LT", "<"],
-            nop: ["Nop"],
-            pass: ["Pass"],
-            return: ["Return", "CR", "Enter"],
-            right_shift: [">"],
-            space: ["Space", " "],
-            subtract: ["Minus", "Subtract"]
-        };
+        this._macros = storage.newMap("registers", { privateData: true, store: true });
+        if (storage.exists("macros")) {
+            for (let [k, m] in storage.newMap("macros", { store: true }))
+                this._macros.set(k, { text: m.keys, timestamp: m.timeRecorded * 1000 });
+            storage.remove("macros");
+        }
 
-        this._pseudoKeys = Set(["count", "leader", "nop", "pass"]);
+        this.popups = {
+            active: [],
 
-        this._key_key = {};
-        this._code_key = {};
-        this._key_code = {};
-        this._code_nativeKey = {};
+            activeMenubar: null,
 
-        for (let list in values(this._keyTable))
-            for (let v in values(list)) {
-                if (v.length == 1)
-                    v = v.toLowerCase();
-                this._key_key[v.toLowerCase()] = v;
-            }
+            update: function update(elem) {
+                if (elem) {
+                    if (elem instanceof Ci.nsIAutoCompletePopup
+                            || elem.localName == "tooltip"
+                            || !elem.popupBoxObject)
+                        return;
+
+                    if (!~this.active.indexOf(elem))
+                        this.active.push(elem);
+                }
+
+                this.active = this.active.filter(function (e) e.popupBoxObject.popupState != "closed");
+
+                if (!this.active.length && !this.activeMenubar)
+                    modes.remove(modes.MENU, true);
+                else if (modes.main != modes.MENU)
+                    modes.push(modes.MENU);
+            },
 
-        for (let [k, v] in Iterator(KeyEvent)) {
-            this._code_nativeKey[v] = k.substr(4);
+            events: {
+                DOMMenuBarActive: function onDOMMenuBarActive(event) {
+                    this.activeMenubar = event.target;
+                    if (modes.main != modes.MENU)
+                        modes.push(modes.MENU);
+                },
 
-            k = k.substr(7).toLowerCase();
-            let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
-                          .replace(/^NUMPAD/, "k")];
+                DOMMenuBarInactive: function onDOMMenuBarInactive(event) {
+                    this.activeMenubar = null;
+                    modes.remove(modes.MENU, true);
+                },
 
-            if (names[0].length == 1)
-                names[0] = names[0].toLowerCase();
+                popupshowing: function onPopupShowing(event) {
+                    this.update(event.originalTarget);
+                },
+
+                popupshown: function onPopupShown(event) {
+                    let elem = event.originalTarget;
+                    this.update(elem);
 
-            if (k in this._keyTable)
-                names = this._keyTable[k];
-            this._code_key[v] = names[0];
-            for (let [, name] in Iterator(names)) {
-                this._key_key[name.toLowerCase()] = name;
-                this._key_code[name.toLowerCase()] = v;
+                    if (elem instanceof Ci.nsIAutoCompletePopup) {
+                        if (modes.main != modes.AUTOCOMPLETE)
+                            modes.push(modes.AUTOCOMPLETE);
+                    }
+                    else if (elem.hidePopup && elem.localName !== "tooltip"
+                                && Events.isHidden(elem)
+                                && Events.isHidden(elem.parentNode)) {
+                        elem.hidePopup();
+                    }
+                },
+
+                popuphidden: function onPopupHidden(event) {
+                    this.update();
+                    modes.remove(modes.AUTOCOMPLETE);
+                }
             }
-        }
+        };
 
-        // HACK: as Gecko does not include an event for <, we must add this in manually.
-        if (!("<" in this._key_code)) {
-            this._key_code["<"] = 60;
-            this._key_code["lt"] = 60;
-            this._code_key[60] = "lt";
-        }
+        this.listen(window, this, "events", true);
+        this.listen(window, this.popups, "events", true);
+    },
 
-        this._activeMenubar = false;
-        this.listen(window, this, "events");
+    cleanup: function cleanup() {
+        let elem = dactyl.focusedElement;
+        if (DOM(elem).isEditable)
+            util.trapErrors("removeEditActionListener",
+                            DOM(elem).editor, editor);
     },
 
     signals: {
@@ -514,7 +217,8 @@ var Events = Module("events", {
      */
     wrapListener: function wrapListener(method, self) {
         self = self || this;
-        method.wrapped = wrappedListener;
+        method.wrapper = wrappedListener;
+        wrappedListener.wrapped = method;
         function wrappedListener(event) {
             try {
                 method.apply(self, arguments);
@@ -549,21 +253,18 @@ var Events = Module("events", {
         dactyl.assert(macro == null || /[a-zA-Z0-9]/.test(macro),
                       _("macro.invalid", macro));
 
-        modes.recording = !!macro;
+        modes.recording = macro;
 
-        if (/[A-Z]/.test(macro)) { // uppercase (append)
+        if (/[A-Z]/.test(macro)) { // Append.
             macro = macro.toLowerCase();
-            this._macroKeys = events.fromString((this._macros.get(macro) || { keys: "" }).keys, true)
-                                    .map(events.closure.toString);
+            this._macroKeys = DOM.Event.iterKeys(editor.getRegister(macro))
+                                 .toArray();
         }
-        else if (macro) {
+        else if (macro) { // Record afresh.
             this._macroKeys = [];
         }
-        else {
-            this._macros.set(this.recording, {
-                keys: this._macroKeys.join(""),
-                timeRecorded: Date.now()
-            });
+        else if (this.recording) { // Save.
+            editor.setRegister(this.recording, this._macroKeys.join(""));
 
             dactyl.log(_("macro.recorded", this.recording, this._macroKeys.join("")), 9);
             dactyl.echomsg(_("macro.recorded", this.recording));
@@ -578,27 +279,24 @@ var Events = Module("events", {
      * @returns {boolean}
      */
     playMacro: function (macro) {
-        let res = false;
-        dactyl.assert(/^[a-zA-Z0-9@]$/.test(macro), _("macro.invalid", macro));
+        dactyl.assert(/^[a-zA-Z0-9@]$/.test(macro),
+                      _("macro.invalid", macro));
 
         if (macro == "@")
             dactyl.assert(this._lastMacro, _("macro.noPrevious"));
         else
             this._lastMacro = macro.toLowerCase(); // XXX: sets last played macro, even if it does not yet exist
 
-        if (this._macros.get(this._lastMacro)) {
-            try {
-                modes.replaying = true;
-                res = events.feedkeys(this._macros.get(this._lastMacro).keys, { noremap: true });
-            }
-            finally {
-                modes.replaying = false;
-            }
-        }
-        else
-            // TODO: ignore this like Vim?
-            dactyl.echoerr(_("macro.noSuch", this._lastMacro));
-        return res;
+        let keys = editor.getRegister(this._lastMacro);
+        if (keys)
+            return modes.withSavedValues(["replaying"], function () {
+                this.replaying = true;
+                return events.feedkeys(keys, { noremap: true });
+            });
+
+        // TODO: ignore this like Vim?
+        dactyl.echoerr(_("macro.noSuch", this._lastMacro));
+        return false;
     },
 
     /**
@@ -609,7 +307,7 @@ var Events = Module("events", {
      */
     getMacros: function (filter) {
         let re = RegExp(filter || "");
-        return ([k, m.keys] for ([k, m] in events._macros) if (re.test(k)));
+        return ([k, m.text] for ([k, m] in editor.registers) if (re.test(k)));
     },
 
     /**
@@ -620,9 +318,9 @@ var Events = Module("events", {
      */
     deleteMacros: function (filter) {
         let re = RegExp(filter || "");
-        for (let [item, ] in this._macros) {
+        for (let [item, ] in editor.registers) {
             if (!filter || re.test(item))
-                this._macros.remove(item);
+                editor.registers.remove(item);
         }
     },
 
@@ -641,8 +339,8 @@ var Events = Module("events", {
             let elem = target || event.originalTarget;
             if (elem) {
                 let doc = elem.ownerDocument || elem.document || elem;
-                let evt = events.create(doc, event.type, event);
-                events.dispatch(elem, evt, extra);
+                let evt = DOM.Event(doc, event.type, event);
+                DOM.Event.dispatch(elem, evt, extra);
             }
             else if (i > 0 && event.type === "keypress")
                 events.events.keypress.call(events, event);
@@ -676,9 +374,9 @@ var Events = Module("events", {
 
             keys = mappings.expandLeader(keys);
 
-            for (let [, evt_obj] in Iterator(events.fromString(keys))) {
+            for (let [, evt_obj] in Iterator(DOM.Event.parse(keys))) {
                 let now = Date.now();
-                let key = events.toString(evt_obj);
+                let key = DOM.Event.stringify(evt_obj);
                 for (let type in values(["keydown", "keypress", "keyup"])) {
                     let evt = update({}, evt_obj, { type: type });
                     if (type !== "keypress" && !evt.keyCode)
@@ -691,10 +389,10 @@ var Events = Module("events", {
                     evt.isMacro = true;
                     evt.dactylMode = mode;
                     evt.dactylSavedEvents = savedEvents;
-                    this.feedingEvent = evt;
+                    DOM.Event.feedingEvent = 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;
@@ -703,8 +401,9 @@ var Events = Module("events", {
                         ["<Return>", "<Space>"].indexOf(key) == -1)
                         target = target.ownerDocument.documentElement;
 
+                    let event = DOM.Event(doc, type, evt);
                     if (!evt_obj.dactylString && !mode)
-                        events.dispatch(target, event, evt);
+                        DOM.Event.dispatch(target, event, evt);
                     else if (type === "keypress")
                         events.events.keypress.call(events, event);
                 }
@@ -717,7 +416,7 @@ var Events = Module("events", {
             util.reportError(e);
         }
         finally {
-            this.feedingEvent = null;
+            DOM.Event.feedingEvent = null;
             this.feedingKeys = wasFeeding;
             if (quiet)
                 commandline.quiet = wasQuiet;
@@ -726,360 +425,22 @@ var Events = Module("events", {
         return true;
     },
 
-    /**
-     * Creates an actual event from a pseudo-event object.
-     *
-     * The pseudo-event object (such as may be retrieved from events.fromString)
-     * should have any properties you want the event to have.
-     *
-     * @param {Document} doc The DOM document to associate this event with
-     * @param {Type} type The type of event (keypress, click, etc.)
-     * @param {Object} opts The pseudo-event. @optional
-     */
-    create: function (doc, type, opts) {
-        const DEFAULTS = {
-            HTML: {
-                type: type, bubbles: true, cancelable: false
-            },
-            Key: {
-                type: type,
-                bubbles: true, cancelable: true,
-                view: doc.defaultView,
-                ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
-                keyCode: 0, charCode: 0
-            },
-            Mouse: {
-                type: type,
-                bubbles: true, cancelable: true,
-                view: doc.defaultView,
-                detail: 1,
-                screenX: 0, screenY: 0,
-                clientX: 0, clientY: 0,
-                ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
-                button: 0,
-                relatedTarget: null
-            }
-        };
-
-        opts = opts || {};
-
-        var t = this._create_types[type];
-        var evt = doc.createEvent((t || "HTML") + "Events");
-
-        let defaults = DEFAULTS[t || "HTML"];
+    canonicalKeys: deprecated("DOM.Event.canonicalKeys", { get: function canonicalKeys() DOM.Event.closure.canonicalKeys }),
+    create:        deprecated("DOM.Event", function create() DOM.Event.apply(null, arguments)),
+    dispatch:      deprecated("DOM.Event.dispatch", function dispatch() DOM.Event.dispatch.apply(DOM.Event, arguments)),
+    fromString:    deprecated("DOM.Event.parse", { get: function fromString() DOM.Event.closure.parse }),
+    iterKeys:      deprecated("DOM.Event.iterKeys", { get: function iterKeys() DOM.Event.closure.iterKeys }),
 
-        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()),
+    toString: function toString() {
+        if (!arguments.length)
+            return toString.supercall(this);
 
-    /**
-     * Converts a user-input string of keys into a canonical
-     * representation.
-     *
-     * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
-     * <C- > maps to <C-Space>, <S-a> maps to A
-     * << maps to <lt><lt>
-     *
-     * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
-     * in macros.
-     *
-     * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
-     * of x.
-     *
-     * @param {string} keys Messy form.
-     * @param {boolean} unknownOk Whether unknown keys are passed
-     *     through rather than being converted to <lt>keyname>.
-     *     @default false
-     * @returns {string} Canonical form.
-     */
-    canonicalKeys: function (keys, unknownOk) {
-        if (arguments.length === 1)
-            unknownOk = true;
-        return events.fromString(keys, unknownOk).map(events.closure.toString).join("");
+        deprecated.warn(toString, "toString", "DOM.Event.stringify");
+        return DOM.Event.stringify.apply(DOM.Event, arguments);
     },
 
-    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.
-     *
-     * @param {Node} target The DOM node to which to dispatch the event.
-     * @param {Event} event The event to dispatch.
-     */
-    dispatch: Class.memoize(function ()
-        util.haveGecko("2b")
-            ? function dispatch(target, event, extra) {
-                try {
-                    this.feedingEvent = extra;
-                    if (target instanceof Element)
-                        // This causes a crash on Gecko<2.0, it seems.
-                        return (target.ownerDocument || target.document || target).defaultView
-                               .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
-                               .dispatchDOMEventViaPresShell(target, event, true);
-                    else {
-                        target.dispatchEvent(event);
-                        return !event.getPreventDefault();
-                    }
-                }
-                catch (e) {
-                    util.reportError(e);
-                }
-                finally {
-                    this.feedingEvent = null;
-                }
-            }
-            : function dispatch(target, event, extra) {
-                try {
-                    this.feedingEvent = extra;
-                    target.dispatchEvent(update(event, extra));
-                }
-                finally {
-                    this.feedingEvent = null;
-                }
-            }),
-
     get defaultTarget() dactyl.focusedElement || content.document.body || document.documentElement,
 
-    /**
-     * Converts an event string into an array of pseudo-event objects.
-     *
-     * These objects can be used as arguments to events.toString or
-     * events.create, though they are unlikely to be much use for other
-     * purposes. They have many of the properties you'd expect to find on a
-     * real event, but none of the methods.
-     *
-     * Also may contain two "special" parameters, .dactylString and
-     * .dactylShift these are set for characters that can never by
-     * typed, but may appear in mappings, for example <Nop> is passed as
-     * dactylString, and dactylShift is set when a user specifies
-     * <S-@> where @ is a non-case-changeable, non-space character.
-     *
-     * @param {string} keys The string to parse.
-     * @param {boolean} unknownOk Whether unknown keys are passed
-     *     through rather than being converted to <lt>keyname>.
-     *     @default false
-     * @returns {Array[Object]}
-     */
-    fromString: function (input, unknownOk) {
-
-        if (arguments.length === 1)
-            unknownOk = true;
-
-        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) {
-                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.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)
-                            keyname = keyname.toUpperCase();
-
-                        evt_obj.charCode = keyname.charCodeAt(0);
-                        evt_obj._keyCode = this._key_code[keyname.toLowerCase()];
-                    }
-                    else if (Set.has(this._pseudoKeys, keyname)) {
-                        evt_obj.dactylString = "<" + this._key_key[keyname] + ">";
-                    }
-                    else if (/mouse$/.test(keyname)) { // mouse events
-                        evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
-                        evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
-                        delete evt_obj.keyCode;
-                        delete evt_obj.charCode;
-                    }
-                    else { // spaces, control characters, and <
-                        evt_obj.keyCode = this._key_code[keyname];
-                        evt_obj.charCode = 0;
-                    }
-                }
-                else { // an invalid sequence starting with <, treat as a literal
-                    out = out.concat(events.fromString("<lt>" + evt_str.substr(1)));
-                    continue;
-                }
-            }
-
-            // TODO: make a list of characters that need keyCode and charCode somewhere
-            if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
-                evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
-            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.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
-                              | (evt_obj.metaKey  && Ci.nsIDOMNSEvent.META_MASK);
-
-            out.push(evt_obj);
-        }
-        return out;
-    },
-
-    /**
-     * Converts the specified event to a string in dactyl key-code
-     * notation. Returns null for an unknown event.
-     *
-     * @param {Event} event
-     * @returns {string}
-     */
-    toString: function toString(event) {
-        if (!event)
-            return toString.supercall(this);
-
-        if (event.dactylString)
-            return event.dactylString;
-
-        let key = null;
-        let modifier = "";
-
-        if (event.globKey)
-            modifier += "*-";
-        if (event.ctrlKey)
-            modifier += "C-";
-        if (event.altKey)
-            modifier += "A-";
-        if (event.metaKey)
-            modifier += "M-";
-
-        if (/^key/.test(event.type)) {
-            let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
-            if (charCode == 0) {
-                if (event.keyCode in this._code_key) {
-                    key = this._code_key[event.keyCode];
-
-                    if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
-                        modifier += "S-";
-                    else if (!modifier && key.length === 1)
-                        if (event.shiftKey)
-                            key = key.toUpperCase();
-                        else
-                            key = key.toLowerCase();
-
-                    if (!modifier && /^[a-z0-9]$/i.test(key))
-                        return key;
-                }
-            }
-            // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
-            //            (i.e., cntrl codes 27--31)
-            // ---
-            // For more information, see:
-            //     [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
-            //     [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
-            //         https://bugzilla.mozilla.org/show_bug.cgi?id=416227
-            //     [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
-            //         https://bugzilla.mozilla.org/show_bug.cgi?id=432951
-            // ---
-            //
-            // The following fixes are only activated if util.OS.isMacOSX.
-            // Technically, they prevent mappings from <C-Esc> (and
-            // <C-C-]> if your fancy keyboard permits such things<?>), but
-            // these <C-control> mappings are probably pathological (<C-Esc>
-            // certainly is on Windows), and so it is probably
-            // harmless to remove the util.OS.isMacOSX if desired.
-            //
-            else if (util.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
-                if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
-                    key = "Esc";
-                    modifier = modifier.replace("C-", "");
-                }
-                else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
-                    key = String.fromCharCode(charCode + 64);
-            }
-            // a normal key like a, b, c, 0, etc.
-            else if (charCode > 0) {
-                key = String.fromCharCode(charCode);
-
-                if (!/^[a-z0-9]$/i.test(key) && key in this._key_code) {
-                    // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
-                    if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
-                        modifier += "S-";
-
-                    key = this._code_key[this._key_code[key]];
-                }
-                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)
-                        modifier += "S-";
-                    if (/^\s$/.test(key))
-                        key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
-                    else if (modifier.length == 0)
-                        return key;
-                }
-            }
-            if (key == null) {
-                if (event.shiftKey)
-                    modifier += "S-";
-                key = this._key_key[event.dactylKeyname] || event.dactylKeyname;
-            }
-            if (key == null)
-                return null;
-        }
-        else if (event.type == "click" || event.type == "dblclick") {
-            if (event.shiftKey)
-                modifier += "S-";
-            if (event.type == "dblclick")
-                modifier += "2-";
-            // TODO: triple and quadruple click
-
-            switch (event.button) {
-            case 0:
-                key = "LeftMouse";
-                break;
-            case 1:
-                key = "MiddleMouse";
-                break;
-            case 2:
-                key = "RightMouse";
-                break;
-            }
-        }
-
-        if (key == null)
-            return null;
-
-        return "<" + modifier + key + ">";
-    },
-
     /**
      * Returns true if there's a known native key handler for the given
      * event in the given mode.
@@ -1106,7 +467,7 @@ var Events = Module("events", {
                          ["key", key.toLowerCase()]);
         }
 
-        let accel = util.OS.isMacOSX ? "metaKey" : "ctrlKey";
+        let accel = config.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"))
@@ -1212,19 +573,12 @@ var Events = Module("events", {
     },
 
     events: {
-        DOMMenuBarActive: function () {
-            this._activeMenubar = true;
-            if (modes.main != modes.MENU)
-                modes.push(modes.MENU);
-        },
-
-        DOMMenuBarInactive: function () {
-            this._activeMenubar = false;
-            modes.remove(modes.MENU, true);
-        },
-
         blur: function onBlur(event) {
             let elem = event.originalTarget;
+            if (DOM(elem).isEditable)
+                util.trapErrors("removeEditActionListener",
+                                DOM(elem).editor, editor);
+
             if (elem instanceof Window && services.focus.activeWindow == null
                 && document.commandDispatcher.focusedWindow !== window) {
                 // Deals with circumstances where, after the main window
@@ -1245,14 +599,21 @@ var Events = Module("events", {
         // TODO: Merge with onFocusChange
         focus: function onFocus(event) {
             let elem = event.originalTarget;
+            if (DOM(elem).isEditable)
+                util.trapErrors("addEditActionListener",
+                                DOM(elem).editor, editor);
+
+            if (elem == window)
+                overlay.activeWindow = window;
 
+            overlay.setData(elem, "had-focus", true);
             if (event.target instanceof Ci.nsIDOMXULTextBoxElement)
                 if (Events.isHidden(elem, true))
                     elem.blur();
 
             let win = (elem.ownerDocument || elem).defaultView || elem;
 
-            if (!(services.focus.getLastFocusMethod(win) & 0x7000)
+            if (!(services.focus.getLastFocusMethod(win) & 0x3000)
                 && events.isContentNode(elem)
                 && !buffer.focusAllowed(elem)
                 && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window])) {
@@ -1305,13 +666,13 @@ var Events = Module("events", {
             let duringFeed = this.duringFeed || [];
             this.duringFeed = [];
             try {
-                if (this.feedingEvent)
-                    for (let [k, v] in Iterator(this.feedingEvent))
+                if (DOM.Event.feedingEvent)
+                    for (let [k, v] in Iterator(DOM.Event.feedingEvent))
                         if (!(k in event))
                             event[k] = v;
-                this.feedingEvent = null;
+                DOM.Event.feedingEvent = null;
 
-                let key = events.toString(event);
+                let key = DOM.Event.stringify(event);
 
                 // Hack to deal with <BS> and so forth not dispatching input
                 // events
@@ -1320,7 +681,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, "dactyl-input"));
+                            DOM(elem).dactylInput();
                         elem.dactylKeyPress = undefined;
                     });
                 }
@@ -1409,7 +770,7 @@ var Events = Module("events", {
                 else
                     for (let event in values(duringFeed))
                         try {
-                            this.dispatch(event.originalTarget, event, event);
+                            DOM.Event.dispatch(event.originalTarget, event, event);
                         }
                         catch (e) {
                             util.reportError(e);
@@ -1424,7 +785,7 @@ var Events = Module("events", {
                 this.keyEvents = [];
 
             let pass = this.passing && !event.isMacro ||
-                    this.feedingEvent && this.feedingEvent.isReplay ||
+                    DOM.Event.feedingEvent && DOM.Event.feedingEvent.isReplay ||
                     event.isReplay ||
                     modes.main == modes.PASS_THROUGH ||
                     modes.main == modes.QUOTE
@@ -1433,16 +794,20 @@ var Events = Module("events", {
                     !modes.passThrough && this.shouldPass(event) ||
                     !this.processor && event.type === "keydown"
                         && options.get("passunknown").getKey(modes.main.allBases)
-                        && let (key = events.toString(event))
+                        && let (key = DOM.Event.stringify(event))
                             !modes.main.allBases.some(
                                 function (mode) mappings.hives.some(
                                     function (hive) hive.get(mode, key) || hive.getCandidates(mode, key)));
 
+            events.dbg("ON " + event.type.toUpperCase() + " " + DOM.Event.stringify(event) +
+                       " passing: " + this.passing + " " +
+                       " pass: " + pass +
+                       " replay: " + event.isReplay +
+                       " macro: " + event.isMacro);
+
             if (event.type === "keydown")
                 this.passing = 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.
@@ -1461,30 +826,9 @@ var Events = Module("events", {
 
             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) {
-            let elem = event.originalTarget;
-            if (elem instanceof Ci.nsIAutoCompletePopup) {
-                if (modes.main != modes.AUTOCOMPLETE)
-                    modes.push(modes.AUTOCOMPLETE);
+                    overlay.setData(elem, "focus-allowed", true);
+                overlay.setData(win.document, "focus-allowed", true);
             }
-            else if (elem.localName !== "tooltip")
-                if (Events.isHidden(elem)) {
-                    if (elem.hidePopup && Events.isHidden(elem.parentNode))
-                        elem.hidePopup();
-                }
-                else if (modes.main != modes.MENU)
-                    modes.push(modes.MENU);
-        },
-
-        popuphidden: function onPopupHidden(event) {
-            if (window.gContextMenu == null && !this._activeMenubar)
-                modes.remove(modes.MENU, true);
-            modes.remove(modes.AUTOCOMPLETE);
         },
 
         resize: function onResize(event) {
@@ -1494,13 +838,14 @@ var Events = Module("events", {
                 dactyl.triggerObserver("fullscreen", this._fullscreen);
                 autocommands.trigger("Fullscreen", { url: this._fullscreen ? "on" : "off", state: this._fullscreen });
             }
+            statusline.updateZoomLevel();
         }
     },
 
     // argument "event" is deliberately not used, as i don't seem to have
     // access to the real focus target
     // Huh? --djk
-    onFocusChange: function onFocusChange(event) {
+    onFocusChange: util.wrapCallback(function onFocusChange(event) {
         function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument
         if (dactyl.ignoreFocus)
             return;
@@ -1519,34 +864,30 @@ var Events = Module("events", {
                 return;
 
             if (isinstance(elem, [HTMLEmbedElement, HTMLEmbedElement])) {
-                modes.push(modes.EMBED);
+                if (!modes.main.passthrough && modes.main != modes.EMBED)
+                    modes.push(modes.EMBED);
                 return;
             }
 
             let haveInput = modes.stack.some(function (m) m.main.input);
 
-            if (elem instanceof HTMLTextAreaElement
-               || elem instanceof Element && util.computedStyle(elem).MozUserModify === "read-write"
-               || elem == null && win && Editor.getEditor(win)) {
-
-                if (modes.main == modes.VISUAL && elem.selectionEnd == elem.selectionStart)
-                    modes.pop();
-
+            if (DOM(elem || win).isEditable) {
                 if (!haveInput)
-                    if (options["insertmode"])
-                        modes.push(modes.INSERT);
-                    else {
-                        modes.push(modes.TEXT_EDIT);
-                        if (elem.selectionEnd - elem.selectionStart > 0)
-                            modes.push(modes.VISUAL);
-                    }
+                    if (!isinstance(modes.main, [modes.INPUT, modes.TEXT_EDIT, modes.VISUAL]))
+                        if (options["insertmode"])
+                            modes.push(modes.INSERT);
+                        else {
+                            modes.push(modes.TEXT_EDIT);
+                            if (elem.selectionEnd - elem.selectionStart > 0)
+                                modes.push(modes.VISUAL);
+                        }
 
                 if (hasHTMLDocument(win))
-                    buffer.lastInputField = elem;
+                    buffer.lastInputField = elem || win;
                 return;
             }
 
-            if (Events.isInputElement(elem)) {
+            if (elem && Events.isInputElement(elem)) {
                 if (!haveInput)
                     modes.push(modes.INSERT);
 
@@ -1564,7 +905,9 @@ var Events = Module("events", {
             if (elem == null && urlbar && urlbar.inputField == this._lastFocus)
                 util.threadYield(true); // Why? --Kris
 
-            while (modes.main.ownsFocus && modes.topOfStack.params.ownsFocus != elem
+            while (modes.main.ownsFocus
+                    && modes.topOfStack.params.ownsFocus != elem
+                    && modes.topOfStack.params.ownsFocus != win
                     && !modes.topOfStack.params.holdFocus)
                  modes.pop(null, { fromFocus: true });
         }
@@ -1574,18 +917,18 @@ var Events = Module("events", {
             if (modes.main.ownsFocus)
                 modes.topOfStack.params.ownsFocus = elem;
         }
-    },
+    }),
 
     onSelectionChange: function onSelectionChange(event) {
+        // Ignore selection events caused by editor commands.
+        if (editor.inEditMap || modes.main == modes.OPERATOR)
+            return;
+
         let controller = document.commandDispatcher.getControllerForCommand("cmd_copy");
         let couldCopy = controller && controller.isCommandEnabled("cmd_copy");
 
-        if (modes.main == modes.VISUAL) {
-            if (!couldCopy)
-                modes.pop(); // Really not ideal.
-        }
-        else if (couldCopy) {
-            if (modes.main == modes.TEXT_EDIT && !options["insertmode"])
+        if (couldCopy) {
+            if (modes.main == modes.TEXT_EDIT)
                 modes.push(modes.VISUAL);
             else if (modes.main == modes.CARET)
                 modes.push(modes.VISUAL);
@@ -1594,7 +937,7 @@ var Events = Module("events", {
 
     shouldPass: function shouldPass(event)
         !event.noremap && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) &&
-        options.get("passkeys").has(events.toString(event))
+        options.get("passkeys").has(DOM.Event.stringify(event))
 }, {
     ABORT: {},
     KILL: true,
@@ -1603,11 +946,11 @@ var Events = Module("events", {
     WAIT: null,
 
     isEscape: function isEscape(event)
-        let (key = isString(event) ? event : events.toString(event))
+        let (key = isString(event) ? event : DOM.Event.stringify(event))
             key === "<Esc>" || key === "<C-[>",
 
     isHidden: function isHidden(elem, aggressive) {
-        if (util.computedStyle(elem).visibility !== "visible")
+        if (DOM(elem).style.visibility !== "visible")
             return true;
 
         if (aggressive)
@@ -1621,12 +964,9 @@ var Events = Module("events", {
     },
 
     isInputElement: function isInputElement(elem) {
-        return elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type) ||
-               isinstance(elem, [HTMLEmbedElement,
-                                 HTMLObjectElement, HTMLSelectElement,
-                                 HTMLTextAreaElement,
-                                 Ci.nsIDOMXULTextBoxElement]) ||
-               elem instanceof Window && Editor.getEditor(elem);
+        return DOM(elem).isEditable ||
+               isinstance(elem, [HTMLEmbedElement, HTMLObjectElement,
+                                 HTMLSelectElement])
     },
 
     kill: function kill(event) {
@@ -1634,6 +974,14 @@ var Events = Module("events", {
         event.preventDefault();
     }
 }, {
+    contexts: function initContexts(dactyl, modules, window) {
+        update(Events.prototype, {
+            hives: contexts.Hives("events", EventHive),
+            user: contexts.hives.events.user,
+            builtin: contexts.hives.events.builtin
+        });
+    },
+
     commands: function () {
         commands.add(["delmac[ros]"],
             "Delete macros",
@@ -1677,7 +1025,10 @@ var Events = Module("events", {
 
         mappings.add([modes.MAIN],
             ["<C-z>", "<pass-all-keys>"], "Temporarily ignore all " + config.appName + " key bindings",
-            function () { modes.push(modes.PASS_THROUGH); });
+            function () {
+                if (modes.main != modes.PASS_THROUGH)
+                    modes.push(modes.PASS_THROUGH);
+            });
 
         mappings.add([modes.MAIN, modes.PASS_THROUGH, modes.QUOTE],
             ["<C-v>", "<pass-next-key>"], "Pass through next key",
@@ -1710,6 +1061,7 @@ var Events = Module("events", {
         mappings.add([modes.COMMAND],
             ["q", "<record-macro>"], "Record a key sequence into a macro",
             function ({ arg }) {
+                util.assert(arg == null || /^[a-z]$/i.test(arg));
                 events._macroKeys.pop();
                 events.recording = arg;
             },
@@ -1775,18 +1127,23 @@ var Events = Module("events", {
 
                 get pass() (this.flush(), this.pass),
 
-                keepQuotes: true,
-
-                setter: function (values) {
-                    values.forEach(function (filter) {
+                parse: function parse() {
+                    let value = parse.superapply(this, arguments);
+                    value.forEach(function (filter) {
                         let vals = Option.splitList(filter.result);
-                        filter.keys = events.fromString(vals[0]).map(events.closure.toString);
+                        filter.keys = DOM.Event.parse(vals[0]).map(DOM.Event.closure.stringify);
 
-                        filter.commandKeys = vals.slice(1).map(events.closure.canonicalKeys);
+                        filter.commandKeys = vals.slice(1).map(DOM.Event.closure.canonicalKeys);
                         filter.inputKeys = filter.commandKeys.filter(bind("test", /^<[ACM]-/));
                     });
+                    return value;
+                },
+
+                keepQuotes: true,
+
+                setter: function (value) {
                     this.flush();
-                    return values;
+                    return value;
                 }
             });
 
@@ -1808,18 +1165,6 @@ var Events = Module("events", {
         options.add(["timeoutlen", "tmol"],
             "Maximum time (milliseconds) to wait for a longer key command when a shorter one exists",
             "number", 1000);
-    },
-    sanitizer: function () {
-        sanitizer.addItem("macros", {
-            description: "Saved macros",
-            persistent: true,
-            action: function (timespan, host) {
-                if (!host)
-                    for (let [k, m] in events._macros)
-                        if (timespan.contains(m.timeRecorded * 1000))
-                            events._macros.remove(k);
-            }
-        });
     }
 });