// 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 Modes = Module("modes", { init: function init() { this.modeChars = {}; this._main = 1; // NORMAL this._extended = 0; // NONE this._lastShown = null; this._passNextKey = false; this._passAllKeys = false; this._recording = false; this._replaying = false; // playing a macro this._modeStack = update([], { pop: function pop() { if (this.length <= 1) throw Error("Trying to pop last element in mode stack"); return pop.superapply(this, arguments); } }); this._modes = []; this._mainModes = []; this._modeMap = {}; this.boundProperties = {}; this.addMode("BASE", { char: "b", description: "The base mode for all other modes", bases: [], count: false }); this.addMode("MAIN", { char: "m", description: "The base mode for most other modes", bases: [this.BASE], count: false }); this.addMode("COMMAND", { char: "C", description: "The base mode for most modes which accept commands rather than input" }); this.addMode("NORMAL", { char: "n", description: "Active when nothing is focused", bases: [this.COMMAND] }); this.addMode("VISUAL", { char: "v", description: "Active when text is selected", display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : ""), bases: [this.COMMAND], ownsFocus: true, passUnknown: false }, { leave: function (stack, newMode) { if (newMode.main == modes.CARET) { let selection = content.getSelection(); if (selection && !selection.isCollapsed) selection.collapseToStart(); } else if (stack.pop) editor.unselectText(); } }); this.addMode("CARET", { description: "Active when the caret is visible in the web content", bases: [this.COMMAND] }, { get pref() prefs.get("accessibility.browsewithcaret"), set pref(val) prefs.set("accessibility.browsewithcaret", val), enter: function (stack) { if (stack.pop && !this.pref) modes.pop(); else if (!stack.pop && !this.pref) this.pref = true; }, leave: function (stack) { if (!stack.push && this.pref) this.pref = false; } }); this.addMode("TEXT_EDIT", { char: "t", description: "Vim-like editing of input elements", bases: [this.COMMAND], input: true, ownsFocus: true, passUnknown: false }); this.addMode("OUTPUT_MULTILINE", { description: "Active when the multi-line output buffer is open", bases: [this.COMMAND], }); this.addMode("INPUT", { char: "I", description: "The base mode for input modes, including Insert and Command Line", bases: [this.MAIN], insert: true }); this.addMode("INSERT", { char: "i", description: "Active when an input element is focused", insert: true, ownsFocus: true }); this.addMode("AUTOCOMPLETE", { description: "Active when an input autocomplete pop-up is active", display: function () "AUTOCOMPLETE (insert)", bases: [this.INSERT] }); this.addMode("EMBED", { description: "Active when an or element is focused", insert: true, ownsFocus: true, passthrough: true }); this.addMode("PASS_THROUGH", { description: "All keys but are ignored by " + config.appName, bases: [this.BASE], hidden: true, insert: true, passthrough: true }); this.addMode("QUOTE", { description: "The next key sequence is ignored by " + config.appName + ", unless in Pass Through mode", bases: [this.BASE], hidden: true, passthrough: true, display: function () (modes.getStack(1).main == modes.PASS_THROUGH ? (modes.getStack(2).main.display() || modes.getStack(2).main.name) : "PASS THROUGH") + " (next)" }, { // Fix me. preExecute: function (map) { if (modes.main == modes.QUOTE && map.name !== "") modes.pop(); }, postExecute: function (map) { if (modes.main == modes.QUOTE && map.name === "") modes.pop(); }, onKeyPress: function (events) { if (modes.main == modes.QUOTE) modes.pop(); } }); this.addMode("IGNORE", { hidden: true }, { onKeyPress: function (events) Events.KILL, bases: [], passthrough: true }); this.addMode("MENU", { description: "Active when a menu or other pop-up is open", input: true, passthrough: true, ownsInput: false }, { leave: function leave(stack) { util.timeout(function () { if (stack.pop && !modes.main.input && Events.isInputElement(dactyl.focusedElement)) modes.push(modes.INSERT); }); } }); this.addMode("LINE", { extended: true, hidden: true }); this.push(this.NORMAL, 0, { enter: function (stack, prev) { if (prefs.get("accessibility.browsewithcaret")) prefs.set("accessibility.browsewithcaret", false); statusline.updateStatus(); if (!stack.fromFocus && prev.main.ownsFocus) dactyl.focusContent(true); if (prev.main == modes.NORMAL) { dactyl.focusContent(true); for (let frame in values(buffer.allFrames())) { // clear any selection made let selection = frame.getSelection(); if (selection && !selection.isCollapsed) selection.collapseToStart(); } } } }); }, cleanup: function cleanup() { modes.reset(); }, _getModeMessage: function _getModeMessage() { // when recording a macro let macromode = ""; if (this.recording) macromode = "recording"; else if (this.replaying) macromode = "replaying"; let val = this._modeMap[this._main].display(); if (val) return "-- " + val + " --" + macromode;; return macromode; }, NONE: 0, __iterator__: function __iterator__() array.iterValues(this.all), get all() this._modes.slice(), get mainModes() (mode for ([k, mode] in Iterator(modes._modeMap)) if (!mode.extended && mode.name == k)), get mainMode() this._modeMap[this._main], get passThrough() !!(this.main & (this.PASS_THROUGH|this.QUOTE)) ^ (this.getStack(1).main === this.PASS_THROUGH), get topOfStack() this._modeStack[this._modeStack.length - 1], addMode: function addMode(name, options, params) { let mode = Modes.Mode(name, options, params); this[name] = mode; if (mode.char) this.modeChars[mode.char] = (this.modeChars[mode.char] || []).concat(mode); this._modeMap[name] = mode; this._modeMap[mode] = mode; this._modes.push(mode); if (!mode.extended) this._mainModes.push(mode); dactyl.triggerObserver("mode-add", mode); }, dumpStack: function dumpStack() { util.dump("Mode stack:"); for (let [i, mode] in array.iterItems(this._modeStack)) util.dump(" " + i + ": " + mode); }, getMode: function getMode(name) this._modeMap[name], getStack: function getStack(idx) this._modeStack[this._modeStack.length - idx - 1] || this._modeStack[0], get stack() this._modeStack.slice(), getCharModes: function getCharModes(chr) (this.modeChars[chr] || []).slice(), have: function have(mode) this._modeStack.some(function (m) isinstance(m.main, mode)), matchModes: function matchModes(obj) this._modes.filter(function (mode) Object.keys(obj) .every(function (k) obj[k] == (mode[k] || false))), // show the current mode string in the command line show: function show() { let msg = null; if (options.get("showmode").getKey(this.main.name, true)) msg = this._getModeMessage(); if (msg || loaded.commandline) commandline.widgets.mode = msg || null; }, remove: function remove(mode, covert) { if (covert && this.topOfStack.main != mode) { util.assert(mode != this.NORMAL); for (let m; m = array.nth(this.modeStack, function (m) m.main == mode, 0);) this._modeStack.splice(this._modeStack.indexOf(m)); } else if (this.stack.some(function (m) m.main == mode)) { this.pop(mode); this.pop(); } }, delayed: [], delay: function delay(callback, self) { this.delayed.push([callback, self]); }, save: function save(id, obj, prop, test) { if (!(id in this.boundProperties)) for (let elem in array.iterValues(this._modeStack)) elem.saved[id] = { obj: obj, prop: prop, value: obj[prop], test: test }; this.boundProperties[id] = { obj: Cu.getWeakReference(obj), prop: prop, test: test }; }, inSet: false, // helper function to set both modes in one go set: function set(mainMode, extendedMode, params, stack) { var delayed, oldExtended, oldMain, prev, push; if (this.inSet) { dactyl.reportError(Error(_("mode.recursiveSet")), true); return; } params = params || this.getMode(mainMode || this.main).params; if (!stack && mainMode != null && this._modeStack.length > 1) this.reset(); this.withSavedValues(["inSet"], function set() { this.inSet = true; oldMain = this._main, oldExtended = this._extended; if (extendedMode != null) this._extended = extendedMode; if (mainMode != null) { this._main = mainMode; if (!extendedMode) this._extended = this.NONE; } if (stack && stack.pop && stack.pop.params.leave) dactyl.trapErrors("leave", stack.pop.params, stack, this.topOfStack); push = mainMode != null && !(stack && stack.pop) && Modes.StackElement(this._main, this._extended, params, {}); if (push && this.topOfStack) { if (this.topOfStack.params.leave) dactyl.trapErrors("leave", this.topOfStack.params, { push: push }, push); for (let [id, { obj, prop, test }] in Iterator(this.boundProperties)) { if (!obj.get()) delete this.boundProperties[id]; else this.topOfStack.saved[id] = { obj: obj.get(), prop: prop, value: obj.get()[prop], test: test }; } } delayed = this.delayed; this.delayed = []; prev = stack && stack.pop || this.topOfStack; if (push) this._modeStack.push(push); if (stack && stack.pop) for (let { obj, prop, value, test } in values(this.topOfStack.saved)) if (!test || !test(stack, prev)) dactyl.trapErrors(function () { obj[prop] = value }); this.show(); }); delayed.forEach(function ([fn, self]) dactyl.trapErrors(fn, self)); if (this.topOfStack.params.enter && prev) dactyl.trapErrors("enter", this.topOfStack.params, push ? { push: push } : stack || {}, prev); dactyl.triggerObserver("modeChange", [oldMain, oldExtended], [this._main, this._extended], stack); this.show(); }, onCaretChange: function onPrefChange(value) { if (!value && modes.main == modes.CARET) modes.pop(); if (value && modes.main == modes.NORMAL) modes.push(modes.CARET); }, push: function push(mainMode, extendedMode, params) { this.set(mainMode, extendedMode, params, { push: this.topOfStack }); }, pop: function pop(mode, args) { while (this._modeStack.length > 1 && this.main != mode) { let a = this._modeStack.pop(); this.set(this.topOfStack.main, this.topOfStack.extended, this.topOfStack.params, update({ pop: a }, args || {})); if (mode == null) return; } }, replace: function replace(mode, oldMode) { while (oldMode && this._modeStack.length > 1 && this.main != oldMode) this.pop(); if (this._modeStack.length > 1) this.set(mode, null, null, { push: this.topOfStack, pop: this._modeStack.pop() }); this.push(mode); }, reset: function reset() { if (this._modeStack.length == 1 && this.topOfStack.params.enter) this.topOfStack.params.enter({}, this.topOfStack); while (this._modeStack.length > 1) this.pop(); }, get recording() this._recording, set recording(value) { this._recording = value; this.show(); }, get replaying() this._replaying, set replaying(value) { this._replaying = value; this.show(); }, get main() this._main, set main(value) { this.set(value); }, get extended() this._extended, set extended(value) { this.set(null, value); } }, { Mode: Class("Mode", { init: function init(name, options, params) { update(this, { id: 1 << Modes.Mode._id++, name: name, params: params || {} }, options); }, isinstance: function isinstance(obj) this === obj || this.allBases.indexOf(obj) >= 0 || callable(obj) && this instanceof obj, allBases: Class.memoize(function () { let seen = {}, res = [], queue = this.bases; for (let mode in array.iterValues(queue)) if (!set.add(seen, mode)) { res.push(mode); queue.push.apply(queue, mode.bases); } return res; }), get bases() this.input ? [modes.INPUT] : [modes.MAIN], get count() !this.insert, get description() this._display, _display: Class.memoize(function _display() this.name.replace("_", " ", "g")), display: function display() this._display, extended: false, hidden: false, input: Class.memoize(function input() this.insert || this.bases.length && this.bases.some(function (b) b.input)), insert: Class.memoize(function insert() this.bases.length && this.bases.some(function (b) b.insert)), ownsFocus: Class.memoize(function ownsFocus() this.bases.length && this.bases.some(function (b) b.ownsFocus)), get passUnknown() this.input, get mask() this, get toStringParams() [this.name], valueOf: function valueOf() this.id }, { _id: 0 }), StackElement: (function () { const StackElement = Struct("main", "extended", "params", "saved"); StackElement.className = "Modes.StackElement"; StackElement.defaultValue("params", function () this.main.params); update(StackElement.prototype, { get toStringParams() !loaded.modes ? this.main.name : [ this.main.name, <>({ modes.all.filter(function (m) this.extended & m, this).map(function (m) m.name).join("|") }) ] }); return StackElement; })(), cacheId: 0, boundProperty: function BoundProperty(desc) { let id = this.cacheId++; let value; desc = desc || {}; return Class.Property(update({ configurable: true, enumerable: true, init: function bound_init(prop) update(this, { get: function bound_get() { if (desc.get) var val = desc.get.call(this, value); return val === undefined ? value : val; }, set: function bound_set(val) { modes.save(id, this, prop, desc.test); if (desc.set) value = desc.set.call(this, val); value = !desc.set || value === undefined ? val : value; } }) }, desc)); } }, { mappings: function initMappings() { mappings.add([modes.BASE, modes.NORMAL], ["", ""], "Return to NORMAL mode", function () { modes.reset(); }); mappings.add([modes.INPUT, modes.COMMAND, modes.PASS_THROUGH, modes.QUOTE], ["", ""], "Return to the previous mode", function () { modes.pop(); }); mappings.add([modes.MENU], [""], "Close the current popup", function () { modes.pop(); return Events.PASS_THROUGH; }); mappings.add([modes.MENU], [""], "Close the current popup", function () { events.feedkeys(""); }); }, options: function initOptions() { options.add(["showmode", "smd"], "Show the current mode in the command line when it matches this expression", "regexplist", "!^normal$", { regexpFlags: "i" }); }, prefs: function initPrefs() { prefs.watch("accessibility.browsewithcaret", function () modes.onCaretChange.apply(modes, arguments)); } }); // vim: set fdm=marker sw=4 ts=4 et: