// Copyright (c) 2006-2008 by Martin Stubenschrott // Copyright (c) 2007-2011 by Doug Kearns // Copyright (c) 2008-2011 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. "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 = []; let main = { __proto__: mode.main, params: mode.params }; this.modes = array([mode.params.keyModes, main, mode.main.allBases]).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.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 + " " + this.events.length + " " + this.processors.length); let processors = this.processors; let length = 1; if (force) this.processors = []; if (this.ownsBuffer) statusline.inputBuffer = this.processors.length ? this.buffer : ""; if (!processors.some(function (p) !p.extended) && this.actions.length) { 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 = action !== undefined ? action : Events.KILL; if (action !== Events.PASS) this.processors.length = 0; } else if (this.processors.length) { result = Events.KILL; if (options["timeout"]) 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]))) { result = Events.ABORT; if (!Events.isEscape(this.events.slice(-1)[0])) dactyl.beep(); events.feedingKeys = false; } else if (result === undefined) result = Events.PASS; events.dbg("RESULT: " + length + " " + this._result(result)); 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) { events.passing = true; 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("KEY: " + 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; buffer = buffer || input.inputBuffer; 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); 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); let res = map.execute.call(map, update({ self: self.main.params.mappingSelf || self.main.mappingSelf || 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); } }); var EventHive = Class("EventHive", Contexts.Hive, { init: function init(group) { init.supercall(this, group); this.sessionListeners = []; }, cleanup: function cleanup() { this.unlisten(null); }, /** * Adds an event listener for this session and removes it on * dactyl shutdown. * * @param {Element} target The element on which to listen. * @param {string} event The event to listen for. * @param {function} callback The function to call when the event is received. * @param {boolean} capture When true, listen during the capture * phase, otherwise during the bubbling phase. * @param {boolean} allowUntrusted When true, allow capturing of * 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; for (let [event, callback] in Iterator(events)) { let args = [Cu.getWeakReference(target), event, this.wrapListener(callback, self), capture, allowUntrusted]; target.addEventListener.apply(target, args.slice(1)); this.sessionListeners.push(args); } }, /** * Remove an event listener. * * @param {Element} target The element on which to listen. * @param {string} event The event to listen for. * @param {function} callback The function to call when the event is received. * @param {boolean} capture When true, listen during the capture * phase, otherwise during the bubbling phase. */ unlisten: function (target, event, callback, capture) { 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)); return false; } return !args[0].get(); }); } }); /** * @instance events */ var Events = Module("events", { dbg: function () {}, 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, { append: .elements() }); this._fullscreen = window.fullScreen; this._lastFocus = null; 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._pseudoKeys = set(["count", "leader", "nop", "pass"]); this._key_key = {}; this._code_key = {}; this._key_code = {}; 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; } for (let [k, v] in Iterator(KeyEvent)) { k = k.substr(7).toLowerCase(); let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase()) .replace(/^NUMPAD/, "k")]; if (names[0].length == 1) names[0] = names[0].toLowerCase(); 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; } } // 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._activeMenubar = false; this.listen(window, this, "events", true); }, signals: { "browser.locationChange": function (webProgress, request, uri) { options.get("passkeys").flush(); }, "modes.change": function (oldMode, newMode) { delete this.processor; } }, /** * Adds an event listener for this session and removes it on * dactyl shutdown. * * @param {Element} target The element on which to listen. * @param {string} event The event to listen for. * @param {function} callback The function to call when the event is received. * @param {boolean} capture When true, listen during the capture * phase, otherwise during the bubbling phase. */ get listen() this.builtin.closure.listen, addSessionListener: deprecated("events.listen", { get: function addSessionListener() this.listen }), /** * Wraps an event listener to ensure that errors are reported. */ wrapListener: function wrapListener(method, self) { self = self || this; method.wrapped = wrappedListener; function wrappedListener(event) { try { method.apply(self, arguments); } catch (e) { dactyl.reportError(e); if (e.message == "Interrupted") dactyl.echoerr(_("error.interrupted"), commandline.FORCE_SINGLELINE); else dactyl.echoerr(_("event.error", event.type, e.echoerr || e), commandline.FORCE_SINGLELINE); } }; return wrappedListener; }, /** * @property {boolean} Whether synthetic key events are currently being * processed. */ feedingKeys: false, /** * Initiates the recording of a key event macro. * * @param {string} macro The name for the macro. */ _recording: null, get recording() this._recording, set recording(macro) { dactyl.assert(macro == null || /[a-zA-Z0-9]/.test(macro), _("macro.invalid", macro)); modes.recording = !!macro; if (/[A-Z]/.test(macro)) { // uppercase (append) macro = macro.toLowerCase(); this._macroKeys = events.fromString((this._macros.get(macro) || { keys: "" }).keys, true) .map(events.closure.toString); } else if (macro) { this._macroKeys = []; } else { this._macros.set(this.recording, { keys: this._macroKeys.join(""), timeRecorded: Date.now() }); dactyl.log(_("macro.recorded", this.recording, this._macroKeys.join("")), 9); dactyl.echomsg(_("macro.recorded", this.recording)); } this._recording = macro || null; }, /** * Replays a macro. * * @param {string} The name of the macro to replay. * @returns {boolean} */ playMacro: function (macro) { let res = false; 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; }, /** * Returns all macros matching *filter*. * * @param {string} filter A regular expression filter string. A null * filter selects all macros. */ getMacros: function (filter) { let re = RegExp(filter || ""); return ([k, m.keys] for ([k, m] in events._macros) if (re.test(k))); }, /** * Deletes all macros matching *filter*. * * @param {string} filter A regular expression filter string. A null * filter deletes all macros. */ deleteMacros: function (filter) { let re = RegExp(filter || ""); for (let [item, ] in this._macros) { if (!filter || re.test(item)) this._macros.remove(item); } }, /** * Feeds a list of events to *target* or the originalTarget member * of each event if *target* is null. * * @param {EventTarget} target The destination node for the events. * @optional * @param {[Event]} list The events to dispatch. * @param {object} extra Extra properties for processing by dactyl. * @optional */ feedevents: function feedevents(target, list, extra) { list.forEach(function _feedevent(event, i) { 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); } else if (i > 0 && event.type === "keypress") events.events.keypress.call(events, event); }); }, /** * Pushes keys onto the event queue from dactyl. It is similar to * Vim's feedkeys() method, but cannot cope with 2 partially-fed * strings, you have to feed one parseable string. * * @param {string} keys A string like "2" to push onto the event * queue. If you want "<" to be taken literally, prepend it with a * "\\". * @param {boolean} noremap Whether recursive mappings should be * disallowed. * @param {boolean} silent Whether the command should be echoed to the * command line. * @returns {boolean} */ feedkeys: function (keys, noremap, quiet, mode) { try { var savedEvents = this._processor && this._processor.keyEvents; var wasFeeding = this.feedingKeys; this.feedingKeys = true; var wasQuiet = commandline.quiet; if (quiet) commandline.quiet = quiet; for (let [, evt_obj] in Iterator(events.fromString(keys))) { let now = Date.now(); for (let type in values(["keydown", "keypress", "keyup"])) { let evt = update({}, evt_obj, { type: type }); if (type !== "keypress" && !evt.keyCode) evt.keyCode = evt._keyCode || 0; if (isObject(noremap)) update(evt, noremap); else evt.noremap = !!noremap; evt.isMacro = true; evt.dactylMode = mode; evt.dactylSavedEvents = savedEvents; this.feedingEvent = evt; let doc = document.commandDispatcher.focusedWindow.document; let event = events.create(doc, type, evt); if (!evt_obj.dactylString && !mode) events.dispatch(dactyl.focusedElement || ["complete", "interactive"].indexOf(doc.readyState) >= 0 && doc.documentElement || doc.defaultView, event, evt); else if (type === "keypress") events.events.keypress.call(events, event); } if (!this.feedingKeys) return false; } } catch (e) { util.reportError(e); } finally { this.feedingEvent = null; this.feedingKeys = wasFeeding; if (quiet) commandline.quiet = wasQuiet; dactyl.triggerObserver("events.doneFeeding"); } 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) { opts = opts || {}; var 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 } }; const TYPES = { change: "", "dactyl-input": "", input: "", submit: "", click: "Mouse", mousedown: "Mouse", mouseup: "Mouse", mouseover: "Mouse", mouseout: "Mouse", keypress: "Key", keyup: "Key", keydown: "Key" }; var t = TYPES[type]; var evt = doc.createEvent((t || "HTML") + "Events"); let defaults = DEFAULTS[t || "HTML"]; evt["init" + t + "Event"].apply(evt, Object.keys(defaults) .map(function (k) k in opts ? opts[k] : defaults[k])); return evt; }, /** * 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(""); }, 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; 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 + ">"; }, /** * Whether *key* is a key code defined to accept/execute input on the * command line. * * @param {string} key The key code to test. * @returns {boolean} */ isAcceptKey: function (key) key == "" || key == "" || key == "", /** * Whether *key* is a key code defined to reject/cancel input on the * command line. * * @param {string} key The key code to test. * @returns {boolean} */ isCancelKey: function (key) key == "" || key == "" || key == "", isContentNode: function isContentNode(node) { let win = (node.ownerDocument || node).defaultView || node; return XPCNativeWrapper(win).top == content; }, /** * Waits for the current buffer to successfully finish loading. Returns * true for a successful page load otherwise false. * * @returns {boolean} */ waitForPageLoad: function (time) { if (buffer.loaded) return true; dactyl.echo(_("macro.loadWaiting"), commandline.FORCE_SINGLELINE); const maxWaitTime = (time || 25); util.waitFor(function () buffer.loaded, this, maxWaitTime * 1000, true); dactyl.echo("", commandline.FORCE_SINGLELINE); if (!buffer.loaded) dactyl.echoerr(_("macro.loadFailed", maxWaitTime)); return buffer.loaded; }, /** * Ensures that the currently focused element is visible and blurs * it if it's not. */ checkFocus: function () { if (dactyl.focusedElement) { let rect = dactyl.focusedElement.getBoundingClientRect(); if (!rect.width || !rect.height) { services.focus.clearFocus(window); document.commandDispatcher.focusedWindow = content; // onFocusChange needs to die. this.onFocusChange(); } } }, 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 (elem instanceof Window && services.focus.activeWindow == null && document.commandDispatcher.focusedWindow !== window) { // Deals with circumstances where, after the main window // blurs while a collapsed frame has focus, re-activating // the main window does not restore focus and we lose key // input. services.focus.clearFocus(window); document.commandDispatcher.focusedWindow = Editor.getEditor(content) ? window : content; } let hold = modes.topOfStack.params.holdFocus; if (elem == hold) { dactyl.focus(hold); this.timeout(function () { dactyl.focus(hold); }); } }, // TODO: Merge with onFocusChange focus: function onFocus(event) { let elem = event.originalTarget; 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) && events.isContentNode(elem) && !buffer.focusAllowed(elem) && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window])) { if (elem.frameElement) dactyl.focusContent(true); else if (!(elem instanceof Window) || Editor.getEditor(elem)) dactyl.focus(window); } if (elem instanceof Element) elem.dactylFocusAllowed = undefined; }, /* onFocus: function onFocus(event) { let elem = event.originalTarget; if (!(elem instanceof Element)) return; let win = elem.ownerDocument.defaultView; try { util.dump(elem, services.focus.getLastFocusMethod(win) & (0x7000)); if (buffer.focusAllowed(win)) win.dactylLastFocus = elem; else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement])) { if (win.dactylLastFocus) dactyl.focus(win.dactylLastFocus); else elem.blur(); } } catch (e) { util.dump(win, String(elem.ownerDocument), String(elem.ownerDocument && elem.ownerDocument.defaultView)); util.reportError(e); } }, */ input: function onInput(event) { event.originalTarget.dactylKeyPress = undefined; }, // this keypress handler gets always called first, even if e.g. // the command-line has focus // TODO: ...help me...please... keypress: function onKeyPress(event) { event.dactylDefaultPrevented = event.getPreventDefault(); let duringFeed = this.duringFeed || []; this.duringFeed = []; try { if (this.feedingEvent) for (let [k, v] in Iterator(this.feedingEvent)) if (!(k in event)) event[k] = v; this.feedingEvent = null; let key = events.toString(event); // Hack to deal with and so forth not dispatching input // events if (key && event.originalTarget instanceof HTMLInputElement && !modes.main.passthrough) { let elem = event.originalTarget; elem.dactylKeyPress = elem.value; util.timeout(function () { if (elem.dactylKeyPress !== undefined && elem.value !== elem.dactylKeyPress) events.dispatch(elem, events.create(elem.ownerDocument, "dactyl-input")); elem.dactylKeyPress = undefined; }); } if (!key) return null; if (modes.recording && !event.isReplay) events._macroKeys.push(key); // feedingKeys needs to be separate from interrupted so // we can differentiate between a recorded // interrupting whatever it's started and a real // interrupting our playback. if (events.feedingKeys && !event.isMacro) { if (key == "") { events.feedingKeys = false; if (modes.replaying) { modes.replaying = false; this.timeout(function () { dactyl.echomsg(_("macro.canceled", this._lastMacro)); }, 100); } } else duringFeed.push(event); return Events.kill(event); } if (!this.processor) { let mode = modes.getStack(0); if (event.dactylMode) mode = Modes.StackElement(event.dactylMode); let ignore = false; if (modes.main == modes.PASS_THROUGH) ignore = !Events.isEscape(key) && key != ""; else if (modes.main == modes.QUOTE) { if (modes.getStack(1).main == modes.PASS_THROUGH) { mode.params.mainMode = modes.getStack(2).main; ignore = Events.isEscape(key); } else if (events.shouldPass(event)) mode.params.mainMode = modes.getStack(1).main; else ignore = true; if (ignore && !Events.isEscape(key)) modes.pop(); } else if (!event.isMacro && !event.noremap && events.shouldPass(event)) ignore = true; events.dbg("\n\n"); events.dbg("ON KEYPRESS " + key + " ignore: " + ignore, event.originalTarget instanceof Element ? event.originalTarget : String(event.originalTarget)); if (ignore) return null; // FIXME: Why is this hard coded? --Kris if (key == "") util.interrupted = true; this.processor = ProcessorStack(mode, mappings.hives.array, event.noremap); this.processor.keyEvents = this.keyEvents; } let { keyEvents, processor } = this; this._processor = processor; this.processor = null; this.keyEvents = []; if (!processor.process(event)) { this.keyEvents = keyEvents; this.processor = processor; } } catch (e) { dactyl.reportError(e); } finally { [duringFeed, this.duringFeed] = [this.duringFeed, duringFeed]; if (this.feedingKeys) this.duringFeed = this.duringFeed.concat(duringFeed); else for (let event in values(duringFeed)) try { this.dispatch(event.originalTarget, event, event); } catch (e) { util.reportError(e); } } }, keyup: function onKeyUp(event) { this.keyEvents.push(event); let pass = this.passing && !event.isMacro || this.feedingEvent && this.feedingEvent.isReplay || event.isReplay || modes.main == modes.PASS_THROUGH || modes.main == modes.QUOTE && modes.getStack(1).main !== modes.PASS_THROUGH && !this.shouldPass(event) || !modes.passThrough && this.shouldPass(event); 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. if (!pass) event.stopPropagation(); }, keydown: function onKeyDown(event) { if (!event.isMacro) this.passing = false; this.events.keyup.call(this, event); }, mousedown: function onMouseDown(event) { let elem = event.target; let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem; 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); } 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() { // gContextMenu is set to NULL, when a context menu is closed if (window.gContextMenu == null && !this._activeMenubar) modes.remove(modes.MENU, true); modes.remove(modes.AUTOCOMPLETE); }, resize: function onResize(event) { if (window.fullScreen != this._fullscreen) { statusline.statusBar.removeAttribute("moz-collapsed"); this._fullscreen = window.fullScreen; dactyl.triggerObserver("fullscreen", this._fullscreen); autocommands.trigger("Fullscreen", { url: this._fullscreen ? "on" : "off", state: this._fullscreen }); } } }, // 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) { function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument if (dactyl.ignoreFocus) return; let win = window.document.commandDispatcher.focusedWindow; let elem = window.document.commandDispatcher.focusedElement; if (elem == null && Editor.getEditor(win)) elem = win; if (win && win.top == content && dactyl.has("tabs")) buffer.focusedFrame = win; try { if (elem && elem.readOnly) return; if (isinstance(elem, [HTMLEmbedElement, HTMLEmbedElement])) { 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 (!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 (hasHTMLDocument(win)) buffer.lastInputField = elem; return; } if (Events.isInputElement(elem)) { if (!haveInput) modes.push(modes.INSERT); if (hasHTMLDocument(win)) buffer.lastInputField = elem; return; } if (config.focusChange) { config.focusChange(win); return; } let urlbar = document.getElementById("urlbar"); if (elem == null && urlbar && urlbar.inputField == this._lastFocus) util.threadYield(true); // Why? --Kris while (modes.main.ownsFocus && !modes.topOfStack.params.holdFocus) modes.pop(null, { fromFocus: true }); } finally { this._lastFocus = elem; } }, onSelectionChange: function onSelectionChange(event) { 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"]) modes.push(modes.VISUAL); else if (modes.main == modes.CARET) modes.push(modes.VISUAL); } }, shouldPass: function shouldPass(event) !event.noremap && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) && options.get("passkeys").has(events.toString(event)) }, { ABORT: {}, KILL: true, PASS: false, PASS_THROUGH: {}, WAIT: null, isEscape: function isEscape(event) let (key = isString(event) ? event : events.toString(event)) key === "" || key === "", isHidden: function isHidden(elem, aggressive) { for (let e = elem; e instanceof Element; e = e.parentNode) { if (util.computedStyle(e).visibility !== "visible" || aggressive && !/set$/.test(e.localName) && e.boxObject && e.boxObject.height === 0) return true; else if (e.namespaceURI == XUL && e.localName === "panel") break; } return false; }, isInputElement: function isInputElement(elem) { return elem instanceof HTMLInputElement && set.has(util.editableInputs, elem.type) || isinstance(elem, [HTMLIsIndexElement, HTMLEmbedElement, HTMLObjectElement, HTMLSelectElement, HTMLTextAreaElement, Ci.nsIDOMXULTreeElement, Ci.nsIDOMXULTextBoxElement]) || elem instanceof Window && Editor.getEditor(elem); }, kill: function kill(event) { event.stopPropagation(); event.preventDefault(); } }, { commands: function () { commands.add(["delmac[ros]"], "Delete macros", function (args) { dactyl.assert(!args.bang || !args[0], _("error.invalidArgument")); if (args.bang) events.deleteMacros(); else if (args[0]) events.deleteMacros(args[0]); else dactyl.echoerr(_("error.argumentRequired")); }, { bang: true, completer: function (context) completion.macro(context), literal: 0 }); commands.add(["macros"], "List all macros", function (args) { completion.listCompleter("macro", args[0]); }, { argCount: "?", completer: function (context) completion.macro(context) }); }, completion: function () { completion.macro = function macro(context) { context.title = ["Macro", "Keys"]; context.completions = [item for (item in events.getMacros())]; }; }, mappings: function () { mappings.add([modes.MAIN], [""], "Process the next key as a builtin mapping", function () { events.processor = ProcessorStack(modes.getStack(0), mappings.hives.array, true); events.processor.keyEvents = events.keyEvents; }); mappings.add([modes.MAIN], ["", ""], "Temporarily ignore all " + config.appName + " key bindings", function () { modes.push(modes.PASS_THROUGH); }); mappings.add([modes.MAIN], ["", ""], "Pass through next key", function () { if (modes.main == modes.QUOTE) return Events.PASS; modes.push(modes.QUOTE); }); mappings.add([modes.BASE], [""], "Do Nothing", function () {}); mappings.add([modes.BASE], [""], "Do nothing", function () {}); mappings.add([modes.BASE], [""], "Pass the events consumed by the last executed mapping", function ({ keypressEvents: [event] }) { dactyl.assert(event.dactylSavedEvents, _("event.nothingToPass")); return function () { events.feedevents(null, event.dactylSavedEvents, { skipmap: true, isMacro: true, isReplay: true }); }; }); // macros mappings.add([modes.COMMAND], ["q", ""], "Record a key sequence into a macro", function ({ arg }) { events._macroKeys.pop(); events.recording = arg; }, { get arg() !modes.recording }); mappings.add([modes.COMMAND], ["@", ""], "Play a macro", function ({ arg, count }) { count = Math.max(count, 1); while (count--) events.playMacro(arg); }, { arg: true, count: true }); mappings.add([modes.COMMAND], ["s", ""], "Sleep for {count} milliseconds before continuing macro playback", function ({ command, count }) { let now = Date.now(); dactyl.assert(count, _("error.countRequired", command)); if (events.feedingKeys) util.sleep(count); }, { count: true }); mappings.add([modes.COMMAND], ["l", ""], "Wait for the current page to finish loading before continuing macro playback", function ({ count }) { if (events.feedingKeys && !events.waitForPageLoad(count)) { util.interrupted = true; throw Error("Interrupted"); } }, { count: true }); }, options: function () { const Hive = Class("Hive", { init: function init(values, map) { this.name = "passkeys:" + map; this.stack = MapHive.Stack(values.map(function (v) Map(v[map + "Keys"]))); function Map(keys) ({ execute: function () Events.PASS_THROUGH, keys: keys }); }, get active() this.stack.length, get: function get(mode, key) this.stack.mappings[key], getCandidates: function getCandidates(mode, key) this.stack.candidates[key] }); options.add(["passkeys", "pk"], "Pass certain keys through directly for the given URLs", "sitemap", "", { flush: function flush() { memoize(this, "filters", function () this.value.filter(function (f) f(buffer.documentURI))); memoize(this, "pass", function () set(array.flatten(this.filters.map(function (f) f.keys)))); memoize(this, "commandHive", function hive() Hive(this.filters, "command")); memoize(this, "inputHive", function hive() Hive(this.filters, "input")); }, has: function (key) set.has(this.pass, key) || set.has(this.commandHive.stack.mappings, key), get pass() (this.flush(), this.pass), keepQuotes: true, setter: function (values) { values.forEach(function (filter) { let vals = Option.splitList(filter.result); filter.keys = events.fromString(vals[0]).map(events.closure.toString); filter.commandKeys = vals.slice(1).map(events.closure.canonicalKeys); filter.inputKeys = filter.commandKeys.filter(/^<[ACM]-/); }); this.flush(); return values; } }); options.add(["strictfocus", "sf"], "Prevent scripts from focusing input elements without user intervention", "sitemap", "*:moderate", { values: { despotic: "Only allow focus changes when explicitly requested by the user", moderate: "Allow focus changes after user-initiated focus change", "laissez-faire": "Always allow focus changes" } }); options.add(["timeout", "tmo"], "Whether to execute a shorter key command after a timeout when a longer command exists", "boolean", true); 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); } }); } }); // vim: set fdm=marker sw=4 ts=4 et: