1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
11 var ProcessorStack = Class("ProcessorStack", {
12 init: function (mode, hives, builtin) {
13 this.main = mode.main;
19 events.dbg("STACK " + mode);
21 let main = { __proto__: mode.main, params: mode.params };
22 this.modes = array([mode.params.keyModes, main, mode.main.allBases.slice(1)]).flatten().compact();
25 hives = hives.filter(function (h) h.name === "builtin");
27 this.processors = this.modes.map(function (m) hives.map(function (h) KeyProcessor(m, h)))
29 this.ownsBuffer = !this.processors.some(function (p) p.main.ownsBuffer);
31 for (let [i, input] in Iterator(this.processors)) {
32 let params = input.main.params;
34 if (params.preExecute)
35 input.preExecute = params.preExecute;
37 if (params.postExecute)
38 input.postExecute = params.postExecute;
40 if (params.onKeyPress && input.hive === mappings.builtin)
41 input.fallthrough = function fallthrough(events) {
42 return params.onKeyPress(events) === false ? Events.KILL : Events.PASS;
46 let hive = options.get("passkeys")[this.main.input ? "inputHive" : "commandHive"];
47 if (!builtin && hive.active && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)))
48 this.processors.unshift(KeyProcessor(modes.BASE, hive));
51 passUnknown: Class.memoize(function () options.get("passunknown").getKey(this.modes)),
54 events.dbg("NOTIFY()");
55 events.keyEvents = [];
56 events.processor = null;
57 if (!this.execute(undefined, true)) {
58 events.processor = this;
59 events.keyEvents = this.keyEvents;
63 _result: function (result) (result === Events.KILL ? "KILL" :
64 result === Events.PASS ? "PASS" :
65 result === Events.PASS_THROUGH ? "PASS_THROUGH" :
66 result === Events.ABORT ? "ABORT" :
67 callable(result) ? result.toSource().substr(0, 50) : result),
69 execute: function execute(result, force) {
70 events.dbg("EXECUTE(" + this._result(result) + ", " + force + ") events:" + this.events.length
71 + " processors:" + this.processors.length + " actions:" + this.actions.length);
73 let processors = this.processors;
80 statusline.inputBuffer = this.processors.length ? this.buffer : "";
82 if (!this.processors.some(function (p) !p.extended) && this.actions.length) {
83 // We have matching actions and no processors other than
84 // those waiting on further arguments. Execute actions as
85 // long as they continue to return PASS.
87 for (var action in values(this.actions)) {
88 while (callable(action)) {
89 length = action.eventLength;
90 action = dactyl.trapErrors(action);
91 events.dbg("ACTION RES: " + length + " " + this._result(action));
93 if (action !== Events.PASS)
97 // Result is the result of the last action. Unless it's
98 // PASS, kill any remaining argument processors.
99 result = action !== undefined ? action : Events.KILL;
100 if (action !== Events.PASS)
101 this.processors.length = 0;
103 else if (this.processors.length) {
104 // We're still waiting on the longest matching processor.
105 // Kill the event, set a timeout to give up waiting if applicable.
107 result = Events.KILL;
108 if (options["timeout"] && (this.actions.length || events.hasNativeKey(this.events[0], this.main, this.passUnknown)))
109 this.timer = services.Timer(this, options["timeoutlen"], services.Timer.TYPE_ONE_SHOT);
111 else if (result !== Events.KILL && !this.actions.length &&
112 !(this.events[0].isReplay || this.passUnknown
113 || this.modes.some(function (m) m.passEvent(this), this.events[0]))) {
114 // No patching processors, this isn't a fake, pass-through
115 // event, we're not in pass-through mode, and we're not
116 // choosing to pass unknown keys. Kill the event and beep.
118 result = Events.ABORT;
119 if (!Events.isEscape(this.events.slice(-1)[0]))
121 events.feedingKeys = false;
123 else if (result === undefined)
124 // No matching processors, we're willing to pass this event,
125 // and we don't have a default action from a processor. Just
127 result = Events.PASS;
129 events.dbg("RESULT: " + length + " " + this._result(result) + "\n\n");
131 if (result !== Events.PASS || this.events.length > 1)
132 if (result !== Events.ABORT || !this.events[0].isReplay)
133 Events.kill(this.events[this.events.length - 1]);
135 if (result === Events.PASS_THROUGH || result === Events.PASS && this.passUnknown)
136 events.passing = true;
138 if (result === Events.PASS_THROUGH && this.keyEvents.length)
139 events.dbg("PASS_THROUGH:\n\t" + this.keyEvents.map(function (e) [e.type, events.toString(e)]).join("\n\t"));
141 if (result === Events.PASS_THROUGH)
142 events.feedevents(null, this.keyEvents, { skipmap: true, isMacro: true, isReplay: true });
144 let list = this.events.filter(function (e) e.getPreventDefault() && !e.dactylDefaultPrevented);
146 if (result === Events.PASS)
147 events.dbg("PASS THROUGH: " + list.slice(0, length).filter(function (e) e.type === "keypress").map(events.closure.toString));
148 if (list.length > length)
149 events.dbg("REFEED: " + list.slice(length).filter(function (e) e.type === "keypress").map(events.closure.toString));
151 if (result === Events.PASS)
152 events.feedevents(null, list.slice(0, length), { skipmap: true, isMacro: true, isReplay: true });
153 if (list.length > length && this.processors.length === 0)
154 events.feedevents(null, list.slice(length));
157 return this.processors.length === 0;
160 process: function process(event) {
164 let key = events.toString(event);
165 this.events.push(event);
167 this.keyEvents.push(event);
174 events.dbg("PROCESS(" + key + ") skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay);
176 for (let [i, input] in Iterator(this.processors)) {
177 let res = input.process(event);
178 if (res !== Events.ABORT)
181 events.dbg("RES: " + input + " " + this._result(res));
183 if (res === Events.KILL)
189 if (res === Events.WAIT || input.waiting)
190 processors.push(input);
191 if (isinstance(res, KeyProcessor))
192 processors.push(res);
195 events.dbg("RESULT: " + event.getPreventDefault() + " " + this._result(result));
196 events.dbg("ACTIONS: " + actions.length + " " + this.actions.length);
197 events.dbg("PROCESSORS:", processors, "\n");
199 this._actions = actions;
200 this.actions = actions.concat(this.actions);
202 for (let action in values(actions))
203 if (!("eventLength" in action))
204 action.eventLength = this.events.length;
206 if (result === Events.KILL)
208 else if (!this.actions.length && !processors.length)
209 for (let input in values(this.processors))
210 if (input.fallthrough) {
211 if (result === Events.KILL)
213 result = dactyl.trapErrors(input.fallthrough, input, this.events);
216 this.processors = processors;
218 return this.execute(result, options["timeout"] && options["timeoutlen"] === 0);
222 var KeyProcessor = Class("KeyProcessor", {
223 init: function init(main, hive) {
227 this.wantCount = this.main.count;
230 get toStringParams() [this.main.name, this.hive.name],
234 get count() this.countStr ? Number(this.countStr) : null,
236 append: function append(event) {
237 this.events.push(event);
238 let key = events.toString(event);
240 if (this.wantCount && !this.command &&
241 (this.countStr ? /^[0-9]$/ : /^[1-9]$/).test(key))
242 this.countStr += key;
248 process: function process(event) {
250 this.waiting = false;
251 return this.onKeyPress(event);
254 execute: function execute(map, args)
258 self.preExecute.apply(self, args);
260 args.self = self.main.params.mappingSelf || self.main.mappingSelf || map;
261 let res = map.execute.call(map, args);
263 if (self.postExecute)
264 self.postExecute.apply(self, args);
268 onKeyPress: function onKeyPress(event) {
275 var map = this.hive.get(this.main, this.command);
276 this.waiting = this.hive.getCandidates(this.main, this.command);
279 return KeyArgProcessor(this, map, false, "arg");
281 return KeyArgProcessor(this, map, true, "motion");
283 return this.execute(map, {
284 keyEvents: this.keyEvents,
285 command: this.command,
287 keypressEvents: this.events
292 return this.main.insert ? Events.PASS : Events.ABORT;
298 var KeyArgProcessor = Class("KeyArgProcessor", KeyProcessor, {
299 init: function init(input, map, wantCount, argName) {
300 init.supercall(this, input.main, input.hive);
303 this.argName = argName;
304 this.wantCount = wantCount;
309 onKeyPress: function onKeyPress(event) {
310 if (Events.isEscape(event))
316 command: this.parent.command,
317 count: this.count || this.parent.count,
318 events: this.parent.events.concat(this.events)
320 args[this.argName] = this.command;
322 return this.execute(this.map, args);
327 * A hive used mainly for tracking event listeners and cleaning them up when a
328 * group is destroyed.
330 var EventHive = Class("EventHive", Contexts.Hive, {
331 init: function init(group) {
332 init.supercall(this, group);
333 this.sessionListeners = [];
336 cleanup: function cleanup() {
341 * Adds an event listener for this session and removes it on
344 * @param {Element} target The element on which to listen.
345 * @param {string} event The event to listen for.
346 * @param {function} callback The function to call when the event is received.
347 * @param {boolean} capture When true, listen during the capture
348 * phase, otherwise during the bubbling phase.
349 * @param {boolean} allowUntrusted When true, allow capturing of
352 listen: function (target, event, callback, capture, allowUntrusted) {
353 if (!isObject(event))
354 var [self, events] = [null, array.toObject([[event, callback]])];
356 [self, events] = [event, event[callback || "events"]];
357 [, , capture, allowUntrusted] = arguments;
360 if (Set.has(events, "input") && !Set.has(events, "dactyl-input"))
361 events["dactyl-input"] = events.input;
363 for (let [event, callback] in Iterator(events)) {
364 let args = [Cu.getWeakReference(target),
366 this.wrapListener(callback, self),
370 target.addEventListener.apply(target, args.slice(1));
371 this.sessionListeners.push(args);
376 * Remove an event listener.
378 * @param {Element} target The element on which to listen.
379 * @param {string} event The event to listen for.
380 * @param {function} callback The function to call when the event is received.
381 * @param {boolean} capture When true, listen during the capture
382 * phase, otherwise during the bubbling phase.
384 unlisten: function (target, event, callback, capture) {
385 this.sessionListeners = this.sessionListeners.filter(function (args) {
386 if (target == null || args[0].get() == target && args[1] == event && args[2] == callback && args[3] == capture) {
387 args[0].get().removeEventListener.apply(args[0].get(), args.slice(1));
390 return !args[0].get();
398 var Events = Module("events", {
405 hives: contexts.Hives("events", EventHive),
406 user: contexts.hives.events.user,
407 builtin: contexts.hives.events.builtin
410 EventHive.prototype.wrapListener = this.closure.wrapListener;
412 XML.ignoreWhitespace = true;
413 util.overlayWindow(window, {
414 append: <e4x xmlns={XUL}>
415 <window id={document.documentElement.id}>
416 <!-- http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands -->
417 <commandset id="dactyl-onfocus" commandupdater="true" events="focus"
418 oncommandupdate="dactyl.modules.events.onFocusChange(event);"/>
419 <commandset id="dactyl-onselect" commandupdater="true" events="select"
420 oncommandupdate="dactyl.modules.events.onSelectionChange(event);"/>
425 this._fullscreen = window.fullScreen;
426 this._lastFocus = null;
427 this._macroKeys = [];
428 this._lastMacro = "";
430 this._macros = storage.newMap("macros", { privateData: true, store: true });
431 for (let [k, m] in this._macros)
433 m = { keys: m, timeRecorded: Date.now() };
435 // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
436 // matters, so use that string as the first item, that you
437 // want to refer to within dactyl's source code for
438 // comparisons like if (key == "<Esc>") { ... }
440 add: ["Plus", "Add"],
444 escape: ["Esc", "Escape"],
445 insert: ["Insert", "Ins"],
447 left_shift: ["LT", "<"],
450 return: ["Return", "CR", "Enter"],
452 space: ["Space", " "],
453 subtract: ["Minus", "Subtract"]
456 this._pseudoKeys = Set(["count", "leader", "nop", "pass"]);
461 this._code_nativeKey = {};
463 for (let list in values(this._keyTable))
464 for (let v in values(list)) {
467 this._key_key[v.toLowerCase()] = v;
470 for (let [k, v] in Iterator(KeyEvent)) {
471 this._code_nativeKey[v] = k.substr(4);
473 k = k.substr(7).toLowerCase();
474 let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
475 .replace(/^NUMPAD/, "k")];
477 if (names[0].length == 1)
478 names[0] = names[0].toLowerCase();
480 if (k in this._keyTable)
481 names = this._keyTable[k];
482 this._code_key[v] = names[0];
483 for (let [, name] in Iterator(names)) {
484 this._key_key[name.toLowerCase()] = name;
485 this._key_code[name.toLowerCase()] = v;
489 // HACK: as Gecko does not include an event for <, we must add this in manually.
490 if (!("<" in this._key_code)) {
491 this._key_code["<"] = 60;
492 this._key_code["lt"] = 60;
493 this._code_key[60] = "lt";
496 this._activeMenubar = false;
497 this.listen(window, this, "events");
501 "browser.locationChange": function (webProgress, request, uri) {
502 options.get("passkeys").flush();
504 "modes.change": function (oldMode, newMode) {
505 delete this.processor;
509 get listen() this.builtin.closure.listen,
510 addSessionListener: deprecated("events.listen", { get: function addSessionListener() this.listen }),
513 * Wraps an event listener to ensure that errors are reported.
515 wrapListener: function wrapListener(method, self) {
517 method.wrapped = wrappedListener;
518 function wrappedListener(event) {
520 method.apply(self, arguments);
523 dactyl.reportError(e);
524 if (e.message == "Interrupted")
525 dactyl.echoerr(_("error.interrupted"), commandline.FORCE_SINGLELINE);
527 dactyl.echoerr(_("event.error", event.type, e.echoerr || e),
528 commandline.FORCE_SINGLELINE);
531 return wrappedListener;
535 * @property {boolean} Whether synthetic key events are currently being
541 * Initiates the recording of a key event macro.
543 * @param {string} macro The name for the macro.
546 get recording() this._recording,
548 set recording(macro) {
549 dactyl.assert(macro == null || /[a-zA-Z0-9]/.test(macro),
550 _("macro.invalid", macro));
552 modes.recording = !!macro;
554 if (/[A-Z]/.test(macro)) { // uppercase (append)
555 macro = macro.toLowerCase();
556 this._macroKeys = events.fromString((this._macros.get(macro) || { keys: "" }).keys, true)
557 .map(events.closure.toString);
560 this._macroKeys = [];
563 this._macros.set(this.recording, {
564 keys: this._macroKeys.join(""),
565 timeRecorded: Date.now()
568 dactyl.log(_("macro.recorded", this.recording, this._macroKeys.join("")), 9);
569 dactyl.echomsg(_("macro.recorded", this.recording));
571 this._recording = macro || null;
577 * @param {string} The name of the macro to replay.
580 playMacro: function (macro) {
582 dactyl.assert(/^[a-zA-Z0-9@]$/.test(macro), _("macro.invalid", macro));
585 dactyl.assert(this._lastMacro, _("macro.noPrevious"));
587 this._lastMacro = macro.toLowerCase(); // XXX: sets last played macro, even if it does not yet exist
589 if (this._macros.get(this._lastMacro)) {
591 modes.replaying = true;
592 res = events.feedkeys(this._macros.get(this._lastMacro).keys, { noremap: true });
595 modes.replaying = false;
599 // TODO: ignore this like Vim?
600 dactyl.echoerr(_("macro.noSuch", this._lastMacro));
605 * Returns all macros matching *filter*.
607 * @param {string} filter A regular expression filter string. A null
608 * filter selects all macros.
610 getMacros: function (filter) {
611 let re = RegExp(filter || "");
612 return ([k, m.keys] for ([k, m] in events._macros) if (re.test(k)));
616 * Deletes all macros matching *filter*.
618 * @param {string} filter A regular expression filter string. A null
619 * filter deletes all macros.
621 deleteMacros: function (filter) {
622 let re = RegExp(filter || "");
623 for (let [item, ] in this._macros) {
624 if (!filter || re.test(item))
625 this._macros.remove(item);
630 * Feeds a list of events to *target* or the originalTarget member
631 * of each event if *target* is null.
633 * @param {EventTarget} target The destination node for the events.
635 * @param {[Event]} list The events to dispatch.
636 * @param {object} extra Extra properties for processing by dactyl.
639 feedevents: function feedevents(target, list, extra) {
640 list.forEach(function _feedevent(event, i) {
641 let elem = target || event.originalTarget;
643 let doc = elem.ownerDocument || elem.document || elem;
644 let evt = events.create(doc, event.type, event);
645 events.dispatch(elem, evt, extra);
647 else if (i > 0 && event.type === "keypress")
648 events.events.keypress.call(events, event);
653 * Pushes keys onto the event queue from dactyl. It is similar to
654 * Vim's feedkeys() method, but cannot cope with 2 partially-fed
655 * strings, you have to feed one parseable string.
657 * @param {string} keys A string like "2<C-f>" to push onto the event
658 * queue. If you want "<" to be taken literally, prepend it with a
660 * @param {boolean} noremap Whether recursive mappings should be
662 * @param {boolean} silent Whether the command should be echoed to the
666 feedkeys: function (keys, noremap, quiet, mode) {
668 var savedEvents = this._processor && this._processor.keyEvents;
670 var wasFeeding = this.feedingKeys;
671 this.feedingKeys = true;
673 var wasQuiet = commandline.quiet;
675 commandline.quiet = quiet;
677 keys = mappings.expandLeader(keys);
679 for (let [, evt_obj] in Iterator(events.fromString(keys))) {
680 let now = Date.now();
681 let key = events.toString(evt_obj);
682 for (let type in values(["keydown", "keypress", "keyup"])) {
683 let evt = update({}, evt_obj, { type: type });
684 if (type !== "keypress" && !evt.keyCode)
685 evt.keyCode = evt._keyCode || 0;
687 if (isObject(noremap))
688 update(evt, noremap);
690 evt.noremap = !!noremap;
692 evt.dactylMode = mode;
693 evt.dactylSavedEvents = savedEvents;
694 this.feedingEvent = evt;
696 let doc = document.commandDispatcher.focusedWindow.document;
697 let event = events.create(doc, type, evt);
698 let target = dactyl.focusedElement
699 || ["complete", "interactive"].indexOf(doc.readyState) >= 0 && doc.documentElement
702 if (target instanceof Element && !Events.isInputElement(target) &&
703 ["<Return>", "<Space>"].indexOf(key) == -1)
704 target = target.ownerDocument.documentElement;
706 if (!evt_obj.dactylString && !mode)
707 events.dispatch(target, event, evt);
708 else if (type === "keypress")
709 events.events.keypress.call(events, event);
712 if (!this.feedingKeys)
720 this.feedingEvent = null;
721 this.feedingKeys = wasFeeding;
723 commandline.quiet = wasQuiet;
724 dactyl.triggerObserver("events.doneFeeding");
730 * Creates an actual event from a pseudo-event object.
732 * The pseudo-event object (such as may be retrieved from events.fromString)
733 * should have any properties you want the event to have.
735 * @param {Document} doc The DOM document to associate this event with
736 * @param {Type} type The type of event (keypress, click, etc.)
737 * @param {Object} opts The pseudo-event. @optional
739 create: function (doc, type, opts) {
742 type: type, bubbles: true, cancelable: false
746 bubbles: true, cancelable: true,
747 view: doc.defaultView,
748 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
749 keyCode: 0, charCode: 0
753 bubbles: true, cancelable: true,
754 view: doc.defaultView,
756 screenX: 0, screenY: 0,
757 clientX: 0, clientY: 0,
758 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
766 var t = this._create_types[type];
767 var evt = doc.createEvent((t || "HTML") + "Events");
769 let defaults = DEFAULTS[t || "HTML"];
771 let args = Object.keys(defaults)
772 .map(function (k) k in opts ? opts[k] : defaults[k]);
774 evt["init" + t + "Event"].apply(evt, args);
778 _create_types: Class.memoize(function () iter(
780 Mouse: "click mousedown mouseout mouseover mouseup",
781 Key: "keydown keypress keyup",
782 "": "change dactyl-input input submit"
784 ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
789 * Converts a user-input string of keys into a canonical
792 * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
793 * <C- > maps to <C-Space>, <S-a> maps to A
794 * << maps to <lt><lt>
796 * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
799 * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
802 * @param {string} keys Messy form.
803 * @param {boolean} unknownOk Whether unknown keys are passed
804 * through rather than being converted to <lt>keyname>.
806 * @returns {string} Canonical form.
808 canonicalKeys: function (keys, unknownOk) {
809 if (arguments.length === 1)
811 return events.fromString(keys, unknownOk).map(events.closure.toString).join("");
814 iterKeys: function (keys) iter(function () {
815 let match, re = /<.*?>?>|[^<]/g;
816 while (match = re.exec(keys))
821 * Dispatches an event to an element as if it were a native event.
823 * @param {Node} target The DOM node to which to dispatch the event.
824 * @param {Event} event The event to dispatch.
826 dispatch: Class.memoize(function ()
828 ? function dispatch(target, event, extra) {
830 this.feedingEvent = extra;
831 if (target instanceof Element)
832 // This causes a crash on Gecko<2.0, it seems.
833 return (target.ownerDocument || target.document || target).defaultView
834 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
835 .dispatchDOMEventViaPresShell(target, event, true);
837 target.dispatchEvent(event);
838 return !event.getPreventDefault();
845 this.feedingEvent = null;
848 : function dispatch(target, event, extra) {
850 this.feedingEvent = extra;
851 target.dispatchEvent(update(event, extra));
854 this.feedingEvent = null;
858 get defaultTarget() dactyl.focusedElement || content.document.body || document.documentElement,
861 * Converts an event string into an array of pseudo-event objects.
863 * These objects can be used as arguments to events.toString or
864 * events.create, though they are unlikely to be much use for other
865 * purposes. They have many of the properties you'd expect to find on a
866 * real event, but none of the methods.
868 * Also may contain two "special" parameters, .dactylString and
869 * .dactylShift these are set for characters that can never by
870 * typed, but may appear in mappings, for example <Nop> is passed as
871 * dactylString, and dactylShift is set when a user specifies
872 * <S-@> where @ is a non-case-changeable, non-space character.
874 * @param {string} keys The string to parse.
875 * @param {boolean} unknownOk Whether unknown keys are passed
876 * through rather than being converted to <lt>keyname>.
878 * @returns {Array[Object]}
880 fromString: function (input, unknownOk) {
882 if (arguments.length === 1)
886 for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
887 let evt_str = match[0];
889 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
890 keyCode: 0, charCode: 0, type: "keypress" };
892 if (evt_str.length == 1) {
893 evt_obj.charCode = evt_str.charCodeAt(0);
894 evt_obj._keyCode = this._key_code[evt_str[0].toLowerCase()];
895 evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
898 let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
899 modifier = Set(modifier.toUpperCase());
900 keyname = keyname.toLowerCase();
901 evt_obj.dactylKeyname = keyname;
902 if (/^u[0-9a-f]+$/.test(keyname))
903 keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
905 if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
906 this._key_code[keyname] || Set.has(this._pseudoKeys, keyname))) {
907 evt_obj.globKey ="*" in modifier;
908 evt_obj.ctrlKey ="C" in modifier;
909 evt_obj.altKey ="A" in modifier;
910 evt_obj.shiftKey ="S" in modifier;
911 evt_obj.metaKey ="M" in modifier || "⌘" in modifier;
912 evt_obj.dactylShift = evt_obj.shiftKey;
914 if (keyname.length == 1) { // normal characters
915 if (evt_obj.shiftKey)
916 keyname = keyname.toUpperCase();
918 evt_obj.charCode = keyname.charCodeAt(0);
919 evt_obj._keyCode = this._key_code[keyname.toLowerCase()];
921 else if (Set.has(this._pseudoKeys, keyname)) {
922 evt_obj.dactylString = "<" + this._key_key[keyname] + ">";
924 else if (/mouse$/.test(keyname)) { // mouse events
925 evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
926 evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
927 delete evt_obj.keyCode;
928 delete evt_obj.charCode;
930 else { // spaces, control characters, and <
931 evt_obj.keyCode = this._key_code[keyname];
932 evt_obj.charCode = 0;
935 else { // an invalid sequence starting with <, treat as a literal
936 out = out.concat(events.fromString("<lt>" + evt_str.substr(1)));
941 // TODO: make a list of characters that need keyCode and charCode somewhere
942 if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
943 evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
944 if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
945 evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
947 evt_obj.modifiers = (evt_obj.ctrlKey && Ci.nsIDOMNSEvent.CONTROL_MASK)
948 | (evt_obj.altKey && Ci.nsIDOMNSEvent.ALT_MASK)
949 | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
950 | (evt_obj.metaKey && Ci.nsIDOMNSEvent.META_MASK);
958 * Converts the specified event to a string in dactyl key-code
959 * notation. Returns null for an unknown event.
961 * @param {Event} event
964 toString: function toString(event) {
966 return toString.supercall(this);
968 if (event.dactylString)
969 return event.dactylString;
983 if (/^key/.test(event.type)) {
984 let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
986 if (event.keyCode in this._code_key) {
987 key = this._code_key[event.keyCode];
989 if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
991 else if (!modifier && key.length === 1)
993 key = key.toUpperCase();
995 key = key.toLowerCase();
997 if (!modifier && /^[a-z0-9]$/i.test(key))
1001 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1002 // (i.e., cntrl codes 27--31)
1004 // For more information, see:
1005 // [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
1006 // [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
1007 // https://bugzilla.mozilla.org/show_bug.cgi?id=416227
1008 // [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
1009 // https://bugzilla.mozilla.org/show_bug.cgi?id=432951
1012 // The following fixes are only activated if util.OS.isMacOSX.
1013 // Technically, they prevent mappings from <C-Esc> (and
1014 // <C-C-]> if your fancy keyboard permits such things<?>), but
1015 // these <C-control> mappings are probably pathological (<C-Esc>
1016 // certainly is on Windows), and so it is probably
1017 // harmless to remove the util.OS.isMacOSX if desired.
1019 else if (util.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1020 if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1022 modifier = modifier.replace("C-", "");
1024 else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1025 key = String.fromCharCode(charCode + 64);
1027 // a normal key like a, b, c, 0, etc.
1028 else if (charCode > 0) {
1029 key = String.fromCharCode(charCode);
1031 if (!/^[a-z0-9]$/i.test(key) && key in this._key_code) {
1032 // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
1033 if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
1036 key = this._code_key[this._key_code[key]];
1039 // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
1040 // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
1041 if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
1043 if (/^\s$/.test(key))
1044 key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
1045 else if (modifier.length == 0)
1052 key = this._key_key[event.dactylKeyname] || event.dactylKeyname;
1057 else if (event.type == "click" || event.type == "dblclick") {
1060 if (event.type == "dblclick")
1062 // TODO: triple and quadruple click
1064 switch (event.button) {
1069 key = "MiddleMouse";
1080 return "<" + modifier + key + ">";
1084 * Returns true if there's a known native key handler for the given
1085 * event in the given mode.
1087 * @param {Event} event A keypress event.
1088 * @param {Modes.Mode} mode The main mode.
1089 * @param {boolean} passUnknown Whether unknown keys should be passed.
1091 hasNativeKey: function hasNativeKey(event, mode, passUnknown) {
1092 if (mode.input && event.charCode && !(event.ctrlKey || event.metaKey))
1098 var elements = document.getElementsByTagNameNS(XUL, "key");
1102 filters.push(["keycode", this._code_nativeKey[event.keyCode]]);
1103 if (event.charCode) {
1104 let key = String.fromCharCode(event.charCode);
1105 filters.push(["key", key.toUpperCase()],
1106 ["key", key.toLowerCase()]);
1109 let accel = util.OS.isMacOSX ? "metaKey" : "ctrlKey";
1111 let access = iter({ 1: "shiftKey", 2: "ctrlKey", 4: "altKey", 8: "metaKey" })
1112 .filter(function ([k, v]) this & k, prefs.get("ui.key.chromeAccess"))
1113 .map(function ([k, v]) [v, true])
1117 for (let [, key] in iter(elements))
1118 if (filters.some(function ([k, v]) key.getAttribute(k) == v)) {
1119 let keys = { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false };
1120 let needed = { ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey };
1122 let modifiers = (key.getAttribute("modifiers") || "").trim().split(/[\s,]+/);
1123 for (let modifier in values(modifiers))
1125 case "access": update(keys, access); break;
1126 case "accel": keys[accel] = true; break;
1127 default: keys[modifier + "Key"] = true; break;
1129 if (!iter.some(keys, function ([k, v]) v && needed[k]))
1131 for (let [k, v] in iter(keys)) {
1139 if (iter(needed).every(function ([k, v]) v == keys[k]))
1147 * Returns true if *key* is a key code defined to accept/execute input on
1150 * @param {string} key The key code to test.
1151 * @returns {boolean}
1153 isAcceptKey: function (key) key == "<Return>" || key == "<C-j>" || key == "<C-m>",
1156 * Returns true if *key* is a key code defined to reject/cancel input on
1159 * @param {string} key The key code to test.
1160 * @returns {boolean}
1162 isCancelKey: function (key) key == "<Esc>" || key == "<C-[>" || key == "<C-c>",
1165 * Returns true if *node* belongs to the current content document or any
1166 * sub-frame thereof.
1168 * @param {Node|Document|Window} node The node to test.
1169 * @returns {boolean}
1171 isContentNode: function isContentNode(node) {
1172 let win = (node.ownerDocument || node).defaultView || node;
1173 return XPCNativeWrapper(win).top == content;
1177 * Waits for the current buffer to successfully finish loading. Returns
1178 * true for a successful page load otherwise false.
1180 * @returns {boolean}
1182 waitForPageLoad: function (time) {
1186 dactyl.echo(_("macro.loadWaiting"), commandline.FORCE_SINGLELINE);
1188 const maxWaitTime = (time || 25);
1189 util.waitFor(function () buffer.loaded, this, maxWaitTime * 1000, true);
1191 dactyl.echo("", commandline.FORCE_SINGLELINE);
1193 dactyl.echoerr(_("macro.loadFailed", maxWaitTime));
1195 return buffer.loaded;
1199 * Ensures that the currently focused element is visible and blurs
1202 checkFocus: function () {
1203 if (dactyl.focusedElement) {
1204 let rect = dactyl.focusedElement.getBoundingClientRect();
1205 if (!rect.width || !rect.height) {
1206 services.focus.clearFocus(window);
1207 document.commandDispatcher.focusedWindow = content;
1208 // onFocusChange needs to die.
1209 this.onFocusChange();
1215 DOMMenuBarActive: function () {
1216 this._activeMenubar = true;
1217 if (modes.main != modes.MENU)
1218 modes.push(modes.MENU);
1221 DOMMenuBarInactive: function () {
1222 this._activeMenubar = false;
1223 modes.remove(modes.MENU, true);
1226 blur: function onBlur(event) {
1227 let elem = event.originalTarget;
1228 if (elem instanceof Window && services.focus.activeWindow == null
1229 && document.commandDispatcher.focusedWindow !== window) {
1230 // Deals with circumstances where, after the main window
1231 // blurs while a collapsed frame has focus, re-activating
1232 // the main window does not restore focus and we lose key
1234 services.focus.clearFocus(window);
1235 document.commandDispatcher.focusedWindow = Editor.getEditor(content) ? window : content;
1238 let hold = modes.topOfStack.params.holdFocus;
1241 this.timeout(function () { dactyl.focus(hold); });
1245 // TODO: Merge with onFocusChange
1246 focus: function onFocus(event) {
1247 let elem = event.originalTarget;
1249 if (event.target instanceof Ci.nsIDOMXULTextBoxElement)
1250 if (Events.isHidden(elem, true))
1253 let win = (elem.ownerDocument || elem).defaultView || elem;
1255 if (!(services.focus.getLastFocusMethod(win) & 0x7000)
1256 && events.isContentNode(elem)
1257 && !buffer.focusAllowed(elem)
1258 && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window])) {
1260 if (elem.frameElement)
1261 dactyl.focusContent(true);
1262 else if (!(elem instanceof Window) || Editor.getEditor(elem))
1263 dactyl.focus(window);
1266 if (elem instanceof Element)
1267 elem.dactylFocusAllowed = undefined;
1271 onFocus: function onFocus(event) {
1272 let elem = event.originalTarget;
1273 if (!(elem instanceof Element))
1275 let win = elem.ownerDocument.defaultView;
1278 util.dump(elem, services.focus.getLastFocusMethod(win) & (0x7000));
1279 if (buffer.focusAllowed(win))
1280 win.dactylLastFocus = elem;
1281 else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement])) {
1282 if (win.dactylLastFocus)
1283 dactyl.focus(win.dactylLastFocus);
1289 util.dump(win, String(elem.ownerDocument), String(elem.ownerDocument && elem.ownerDocument.defaultView));
1290 util.reportError(e);
1295 input: function onInput(event) {
1296 event.originalTarget.dactylKeyPress = undefined;
1299 // this keypress handler gets always called first, even if e.g.
1300 // the command-line has focus
1301 // TODO: ...help me...please...
1302 keypress: function onKeyPress(event) {
1303 event.dactylDefaultPrevented = event.getPreventDefault();
1305 let duringFeed = this.duringFeed || [];
1306 this.duringFeed = [];
1308 if (this.feedingEvent)
1309 for (let [k, v] in Iterator(this.feedingEvent))
1312 this.feedingEvent = null;
1314 let key = events.toString(event);
1316 // Hack to deal with <BS> and so forth not dispatching input
1318 if (key && event.originalTarget instanceof HTMLInputElement && !modes.main.passthrough) {
1319 let elem = event.originalTarget;
1320 elem.dactylKeyPress = elem.value;
1321 util.timeout(function () {
1322 if (elem.dactylKeyPress !== undefined && elem.value !== elem.dactylKeyPress)
1323 events.dispatch(elem, events.create(elem.ownerDocument, "dactyl-input"));
1324 elem.dactylKeyPress = undefined;
1331 if (modes.recording && !event.isReplay)
1332 events._macroKeys.push(key);
1334 // feedingKeys needs to be separate from interrupted so
1335 // we can differentiate between a recorded <C-c>
1336 // interrupting whatever it's started and a real <C-c>
1337 // interrupting our playback.
1338 if (events.feedingKeys && !event.isMacro) {
1339 if (key == "<C-c>") {
1340 events.feedingKeys = false;
1341 if (modes.replaying) {
1342 modes.replaying = false;
1343 this.timeout(function () { dactyl.echomsg(_("macro.canceled", this._lastMacro)); }, 100);
1347 duringFeed.push(event);
1349 return Events.kill(event);
1352 if (!this.processor) {
1353 let mode = modes.getStack(0);
1354 if (event.dactylMode)
1355 mode = Modes.StackElement(event.dactylMode);
1359 if (mode.main == modes.PASS_THROUGH)
1360 ignore = !Events.isEscape(key) && key != "<C-v>";
1361 else if (mode.main == modes.QUOTE) {
1362 if (modes.getStack(1).main == modes.PASS_THROUGH) {
1363 mode = Modes.StackElement(modes.getStack(2).main);
1364 ignore = Events.isEscape(key);
1366 else if (events.shouldPass(event))
1367 mode = Modes.StackElement(modes.getStack(1).main);
1373 else if (!event.isMacro && !event.noremap && events.shouldPass(event))
1377 events.dbg("ON KEYPRESS " + key + " ignore: " + ignore,
1378 event.originalTarget instanceof Element ? event.originalTarget : String(event.originalTarget));
1383 // FIXME: Why is this hard coded? --Kris
1385 util.interrupted = true;
1387 this.processor = ProcessorStack(mode, mappings.hives.array, event.noremap);
1388 this.processor.keyEvents = this.keyEvents;
1391 let { keyEvents, processor } = this;
1392 this._processor = processor;
1393 this.processor = null;
1394 this.keyEvents = [];
1396 if (!processor.process(event)) {
1397 this.keyEvents = keyEvents;
1398 this.processor = processor;
1403 dactyl.reportError(e);
1406 [duringFeed, this.duringFeed] = [this.duringFeed, duringFeed];
1407 if (this.feedingKeys)
1408 this.duringFeed = this.duringFeed.concat(duringFeed);
1410 for (let event in values(duringFeed))
1412 this.dispatch(event.originalTarget, event, event);
1415 util.reportError(e);
1420 keyup: function onKeyUp(event) {
1421 if (event.type == "keydown")
1422 this.keyEvents.push(event);
1423 else if (!this.processor)
1424 this.keyEvents = [];
1426 let pass = this.passing && !event.isMacro ||
1427 this.feedingEvent && this.feedingEvent.isReplay ||
1429 modes.main == modes.PASS_THROUGH ||
1430 modes.main == modes.QUOTE
1431 && modes.getStack(1).main !== modes.PASS_THROUGH
1432 && !this.shouldPass(event) ||
1433 !modes.passThrough && this.shouldPass(event) ||
1434 !this.processor && event.type === "keydown"
1435 && options.get("passunknown").getKey(modes.main.allBases)
1436 && let (key = events.toString(event))
1437 !modes.main.allBases.some(
1438 function (mode) mappings.hives.some(
1439 function (hive) hive.get(mode, key) || hive.getCandidates(mode, key)));
1441 if (event.type === "keydown")
1442 this.passing = pass;
1444 events.dbg("ON " + event.type.toUpperCase() + " " + this.toString(event) + " pass: " + pass + " replay: " + event.isReplay + " macro: " + event.isMacro);
1446 // Prevents certain sites from transferring focus to an input box
1447 // before we get a chance to process our key bindings on the
1448 // "keypress" event.
1450 event.stopPropagation();
1452 keydown: function onKeyDown(event) {
1454 this.passing = false;
1455 this.events.keyup.call(this, event);
1458 mousedown: function onMouseDown(event) {
1459 let elem = event.target;
1460 let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
1462 for (; win; win = win != win.parent && win.parent) {
1463 for (; elem instanceof Element; elem = elem.parentNode)
1464 elem.dactylFocusAllowed = true;
1465 win.document.dactylFocusAllowed = true;
1469 popupshown: function onPopupShown(event) {
1470 let elem = event.originalTarget;
1471 if (elem instanceof Ci.nsIAutoCompletePopup) {
1472 if (modes.main != modes.AUTOCOMPLETE)
1473 modes.push(modes.AUTOCOMPLETE);
1475 else if (elem.localName !== "tooltip")
1476 if (Events.isHidden(elem)) {
1477 if (elem.hidePopup && Events.isHidden(elem.parentNode))
1480 else if (modes.main != modes.MENU)
1481 modes.push(modes.MENU);
1484 popuphidden: function onPopupHidden(event) {
1485 if (window.gContextMenu == null && !this._activeMenubar)
1486 modes.remove(modes.MENU, true);
1487 modes.remove(modes.AUTOCOMPLETE);
1490 resize: function onResize(event) {
1491 if (window.fullScreen != this._fullscreen) {
1492 statusline.statusBar.removeAttribute("moz-collapsed");
1493 this._fullscreen = window.fullScreen;
1494 dactyl.triggerObserver("fullscreen", this._fullscreen);
1495 autocommands.trigger("Fullscreen", { url: this._fullscreen ? "on" : "off", state: this._fullscreen });
1500 // argument "event" is deliberately not used, as i don't seem to have
1501 // access to the real focus target
1503 onFocusChange: function onFocusChange(event) {
1504 function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument
1505 if (dactyl.ignoreFocus)
1508 let win = window.document.commandDispatcher.focusedWindow;
1509 let elem = window.document.commandDispatcher.focusedElement;
1511 if (elem == null && Editor.getEditor(win))
1514 if (win && win.top == content && dactyl.has("tabs"))
1515 buffer.focusedFrame = win;
1518 if (elem && elem.readOnly)
1521 if (isinstance(elem, [HTMLEmbedElement, HTMLEmbedElement])) {
1522 modes.push(modes.EMBED);
1526 let haveInput = modes.stack.some(function (m) m.main.input);
1528 if (elem instanceof HTMLTextAreaElement
1529 || elem instanceof Element && util.computedStyle(elem).MozUserModify === "read-write"
1530 || elem == null && win && Editor.getEditor(win)) {
1532 if (modes.main == modes.VISUAL && elem.selectionEnd == elem.selectionStart)
1536 if (options["insertmode"])
1537 modes.push(modes.INSERT);
1539 modes.push(modes.TEXT_EDIT);
1540 if (elem.selectionEnd - elem.selectionStart > 0)
1541 modes.push(modes.VISUAL);
1544 if (hasHTMLDocument(win))
1545 buffer.lastInputField = elem;
1549 if (Events.isInputElement(elem)) {
1551 modes.push(modes.INSERT);
1553 if (hasHTMLDocument(win))
1554 buffer.lastInputField = elem;
1558 if (config.focusChange) {
1559 config.focusChange(win);
1563 let urlbar = document.getElementById("urlbar");
1564 if (elem == null && urlbar && urlbar.inputField == this._lastFocus)
1565 util.threadYield(true); // Why? --Kris
1567 while (modes.main.ownsFocus && modes.topOfStack.params.ownsFocus != elem
1568 && !modes.topOfStack.params.holdFocus)
1569 modes.pop(null, { fromFocus: true });
1572 this._lastFocus = elem;
1574 if (modes.main.ownsFocus)
1575 modes.topOfStack.params.ownsFocus = elem;
1579 onSelectionChange: function onSelectionChange(event) {
1580 let controller = document.commandDispatcher.getControllerForCommand("cmd_copy");
1581 let couldCopy = controller && controller.isCommandEnabled("cmd_copy");
1583 if (modes.main == modes.VISUAL) {
1585 modes.pop(); // Really not ideal.
1587 else if (couldCopy) {
1588 if (modes.main == modes.TEXT_EDIT && !options["insertmode"])
1589 modes.push(modes.VISUAL);
1590 else if (modes.main == modes.CARET)
1591 modes.push(modes.VISUAL);
1595 shouldPass: function shouldPass(event)
1596 !event.noremap && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) &&
1597 options.get("passkeys").has(events.toString(event))
1605 isEscape: function isEscape(event)
1606 let (key = isString(event) ? event : events.toString(event))
1607 key === "<Esc>" || key === "<C-[>",
1609 isHidden: function isHidden(elem, aggressive) {
1610 if (util.computedStyle(elem).visibility !== "visible")
1614 for (let e = elem; e instanceof Element; e = e.parentNode) {
1615 if (!/set$/.test(e.localName) && e.boxObject && e.boxObject.height === 0)
1617 else if (e.namespaceURI == XUL && e.localName === "panel")
1623 isInputElement: function isInputElement(elem) {
1624 return elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type) ||
1625 isinstance(elem, [HTMLEmbedElement,
1626 HTMLObjectElement, HTMLSelectElement,
1627 HTMLTextAreaElement,
1628 Ci.nsIDOMXULTextBoxElement]) ||
1629 elem instanceof Window && Editor.getEditor(elem);
1632 kill: function kill(event) {
1633 event.stopPropagation();
1634 event.preventDefault();
1637 commands: function () {
1638 commands.add(["delmac[ros]"],
1641 dactyl.assert(!args.bang || !args[0], _("error.invalidArgument"));
1644 events.deleteMacros();
1646 events.deleteMacros(args[0]);
1648 dactyl.echoerr(_("error.argumentRequired"));
1652 completer: function (context) completion.macro(context),
1656 commands.add(["mac[ros]"],
1658 function (args) { completion.listCompleter("macro", args[0]); }, {
1660 completer: function (context) completion.macro(context)
1663 completion: function () {
1664 completion.macro = function macro(context) {
1665 context.title = ["Macro", "Keys"];
1666 context.completions = [item for (item in events.getMacros())];
1669 mappings: function () {
1671 mappings.add([modes.MAIN],
1672 ["<A-b>", "<pass-next-key-builtin>"], "Process the next key as a builtin mapping",
1674 events.processor = ProcessorStack(modes.getStack(0), mappings.hives.array, true);
1675 events.processor.keyEvents = events.keyEvents;
1678 mappings.add([modes.MAIN],
1679 ["<C-z>", "<pass-all-keys>"], "Temporarily ignore all " + config.appName + " key bindings",
1680 function () { modes.push(modes.PASS_THROUGH); });
1682 mappings.add([modes.MAIN, modes.PASS_THROUGH, modes.QUOTE],
1683 ["<C-v>", "<pass-next-key>"], "Pass through next key",
1685 if (modes.main == modes.QUOTE)
1687 modes.push(modes.QUOTE);
1690 mappings.add([modes.BASE],
1691 ["<CapsLock>"], "Do Nothing",
1694 mappings.add([modes.BASE],
1695 ["<Nop>"], "Do nothing",
1698 mappings.add([modes.BASE],
1699 ["<Pass>"], "Pass the events consumed by the last executed mapping",
1700 function ({ keypressEvents: [event] }) {
1701 dactyl.assert(event.dactylSavedEvents,
1702 _("event.nothingToPass"));
1703 return function () {
1704 events.feedevents(null, event.dactylSavedEvents,
1705 { skipmap: true, isMacro: true, isReplay: true });
1710 mappings.add([modes.COMMAND],
1711 ["q", "<record-macro>"], "Record a key sequence into a macro",
1712 function ({ arg }) {
1713 events._macroKeys.pop();
1714 events.recording = arg;
1716 { get arg() !modes.recording });
1718 mappings.add([modes.COMMAND],
1719 ["@", "<play-macro>"], "Play a macro",
1720 function ({ arg, count }) {
1721 count = Math.max(count, 1);
1723 events.playMacro(arg);
1725 { arg: true, count: true });
1727 mappings.add([modes.COMMAND],
1728 ["<A-m>s", "<sleep>"], "Sleep for {count} milliseconds before continuing macro playback",
1729 function ({ command, count }) {
1730 let now = Date.now();
1731 dactyl.assert(count, _("error.countRequired", command));
1732 if (events.feedingKeys)
1737 mappings.add([modes.COMMAND],
1738 ["<A-m>l", "<wait-for-page-load>"], "Wait for the current page to finish loading before continuing macro playback",
1739 function ({ count }) {
1740 if (events.feedingKeys && !events.waitForPageLoad(count)) {
1741 util.interrupted = true;
1742 throw Error("Interrupted");
1747 options: function () {
1748 const Hive = Class("Hive", {
1749 init: function init(values, map) {
1750 this.name = "passkeys:" + map;
1751 this.stack = MapHive.Stack(values.map(function (v) Map(v[map + "Keys"])));
1752 function Map(keys) ({
1753 execute: function () Events.PASS_THROUGH,
1758 get active() this.stack.length,
1760 get: function get(mode, key) this.stack.mappings[key],
1762 getCandidates: function getCandidates(mode, key) this.stack.candidates[key]
1764 options.add(["passkeys", "pk"],
1765 "Pass certain keys through directly for the given URLs",
1767 flush: function flush() {
1768 memoize(this, "filters", function () this.value.filter(function (f) f(buffer.documentURI)));
1769 memoize(this, "pass", function () Set(array.flatten(this.filters.map(function (f) f.keys))));
1770 memoize(this, "commandHive", function hive() Hive(this.filters, "command"));
1771 memoize(this, "inputHive", function hive() Hive(this.filters, "input"));
1774 has: function (key) Set.has(this.pass, key) || Set.has(this.commandHive.stack.mappings, key),
1776 get pass() (this.flush(), this.pass),
1780 setter: function (values) {
1781 values.forEach(function (filter) {
1782 let vals = Option.splitList(filter.result);
1783 filter.keys = events.fromString(vals[0]).map(events.closure.toString);
1785 filter.commandKeys = vals.slice(1).map(events.closure.canonicalKeys);
1786 filter.inputKeys = filter.commandKeys.filter(bind("test", /^<[ACM]-/));
1793 options.add(["strictfocus", "sf"],
1794 "Prevent scripts from focusing input elements without user intervention",
1795 "sitemap", "'chrome:*':laissez-faire,*:moderate",
1798 despotic: "Only allow focus changes when explicitly requested by the user",
1799 moderate: "Allow focus changes after user-initiated focus change",
1800 "laissez-faire": "Always allow focus changes"
1804 options.add(["timeout", "tmo"],
1805 "Whether to execute a shorter key command after a timeout when a longer command exists",
1808 options.add(["timeoutlen", "tmol"],
1809 "Maximum time (milliseconds) to wait for a longer key command when a shorter one exists",
1812 sanitizer: function () {
1813 sanitizer.addItem("macros", {
1814 description: "Saved macros",
1816 action: function (timespan, host) {
1818 for (let [k, m] in events._macros)
1819 if (timespan.contains(m.timeRecorded * 1000))
1820 events._macros.remove(k);
1826 // vim: set fdm=marker sw=4 ts=4 et: