X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=common%2Fcontent%2Fevents.js;fp=common%2Fcontent%2Fevents.js;h=4b1841d916036233bf06ffe07537fbcfc4bf8573;hb=9044153cb63835e39b9de8ec4ade237c03e3888a;hp=117d64b5891fcd9daf7969bd8d7f6bf238b2c92a;hpb=70740024f9c028c1fd63e1a1850ab062ff956054;p=dactyl.git diff --git a/common/content/events.js b/common/content/events.js index 117d64b..4b1841d 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -4,325 +4,10 @@ // // 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: @@ -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 == "") { ... } - 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", { ["", ""].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. - * - * maps to , maps to - * maps to , maps to A - * << maps to - * - * 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 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 is passed as - * dactylString, and dactylShift is set when a user specifies - * 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 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("" + 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; // - if (evt_obj.keyCode == 60 || evt_obj.charCode == 60) - evt_obj.charCode = evt_obj.keyCode = 60; // - - 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 , , , , 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 (and - // if your fancy keyboard permits such things), but - // these mappings are probably pathological ( - // 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 bug - key = "Esc"; - modifier = modifier.replace("C-", ""); - } - else // [Ctrl-Bug 2,3,4,5/5] the , , , 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 ( and ) space can be shifted, 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 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 === "" || key === "", 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], ["", ""], "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], ["", ""], "Pass through next key", @@ -1710,6 +1061,7 @@ var Events = Module("events", { mappings.add([modes.COMMAND], ["q", ""], "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); - } - }); } });