X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=common%2Fmodules%2Foverlay.jsm;h=c52ba4ad95a9fceb4d599fa9131b302854be3a4f;hb=354a049cce8415487552ce405cce167b7071fe1f;hp=ea626b4c5c08c85cc4a4090ffef0770664904820;hpb=eeed0be1a8abf7e3c97f43b63c1d595e940fef21;p=dactyl.git diff --git a/common/modules/overlay.jsm b/common/modules/overlay.jsm index ea626b4..c52ba4a 100644 --- a/common/modules/overlay.jsm +++ b/common/modules/overlay.jsm @@ -1,4 +1,4 @@ -// Copyright (c) 2009-2011 by Kris Maglione +// Copyright (c) 2009-2013 Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. @@ -6,325 +6,429 @@ try { -Components.utils.import("resource://dactyl/bootstrap.jsm"); defineModule("overlay", { - exports: ["ModuleBase"], - require: ["config", "services", "util"] -}, this); - -/** - * @class ModuleBase - * The base class for all modules. - */ -var ModuleBase = Class("ModuleBase", { - /** - * @property {[string]} A list of module prerequisites which - * must be initialized before this module is loaded. - */ - requires: [], + exports: ["overlay"], + require: ["util"] +}); + +lazyRequire("highlight", ["highlight"]); + +var getAttr = function getAttr(elem, ns, name) + elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null; +var setAttr = function setAttr(elem, ns, name, val) { + if (val == null) + elem.removeAttributeNS(ns, name); + else + elem.setAttributeNS(ns, name, val); +}; + +var Overlay = Class("Overlay", { + init: function init(window) { + this.window = window; + }, + + cleanups: Class.Memoize(() => []), + objects: Class.Memoize(() => ({})), + + get doc() this.window.document, - toString: function () "[module " + this.constructor.className + "]" + get win() this.window, + + $: function $(sel, node) DOM(sel, node || this.doc), + + cleanup: function cleanup(window, reason) { + for (let fn in values(this.cleanups)) + util.trapErrors(fn, this, window, reason); + } }); -var Overlay = Module("Overlay", { +var Overlay = Module("Overlay", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), { init: function init() { - services["dactyl:"]; // Hack. Force module initialization. - - config.loadStyles(); - - util.overlayWindow(config.overlayChrome, function overlay(window) ({ - init: function onInit(document) { - /** - * @constructor Module - * - * Constructs a new ModuleBase class and makes arrangements for its - * initialization. Arguments marked as optional must be either - * entirely elided, or they must have the exact type specified. - * Loading semantics are as follows: - * - * - A module is guaranteed not to be initialized before any of its - * prerequisites as listed in its {@see ModuleBase#requires} member. - * - A module is considered initialized once it's been instantiated, - * its {@see Class#init} method has been called, and its - * instance has been installed into the top-level {@see modules} - * object. - * - Once the module has been initialized, its module-dependent - * initialization functions will be called as described hereafter. - * @param {string} name The module's name as it will appear in the - * top-level {@see modules} object. - * @param {ModuleBase} base The base class for this module. - * @optional - * @param {Object} prototype The prototype for instances of this - * object. The object itself is copied and not used as a prototype - * directly. - * @param {Object} classProperties The class properties for the new - * module constructor. - * @optional - * @param {Object} moduleInit The module initialization functions - * for the new module. Each function is called as soon as the named module - * has been initialized, but after the module itself. The constructors are - * guaranteed to be called in the same order that the dependent modules - * were initialized. - * @optional - * - * @returns {function} The constructor for the resulting module. - */ - function Module(name) { - let args = Array.slice(arguments); - - var base = ModuleBase; - if (callable(args[1])) - base = args.splice(1, 1)[0]; - let [, prototype, classProperties, moduleInit] = args; - const module = Class(name, base, prototype, classProperties); - - module.INIT = moduleInit || {}; - module.modules = modules; - module.prototype.INIT = module.INIT; - module.requires = prototype.requires || []; - Module.list.push(module); - Module.constructors[name] = module; - return module; - } - Module.list = []; - Module.constructors = {}; + util.addObserver(this); + this.overlays = {}; - const BASE = "resource://dactyl-content/"; + this.weakMap = WeakMap(); - const create = window.Object.create || (function () { - window.__dactyl_eval_string = "(function (proto) ({ __proto__: proto }))"; - JSMLoader.loadSubScript(BASE + "eval.js", window); + this.onWindowVisible = []; + }, - let res = window.__dactyl_eval_result; - delete window.__dactyl_eval_string; - delete window.__dactyl_eval_result; - return res; - })(); + id: Class.Memoize(() => config.addon.id), - const jsmodules = { NAME: "jsmodules" }; - const modules = update(create(jsmodules), { - yes_i_know_i_should_not_report_errors_in_these_branches_thanks: [], + /** + * 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) { + let doc = target.ownerDocument || target.document || target; + let listeners = this.getData(doc, "listeners"); + + if (!isObject(event)) + var [self, events] = [null, array.toObject([[event, callback]])]; + else + [self, events] = [event, event[callback || "events"]]; + + for (let [event, callback] in Iterator(events)) { + let args = [util.weakReference(target), + event, + util.wrapCallback(callback, self), + capture, + allowUntrusted]; + + target.addEventListener.apply(target, args.slice(1)); + listeners.push(args); + } + }, - jsmodules: jsmodules, + /** + * 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) { + let doc = target.ownerDocument || target.document || target; + let listeners = this.getData(doc, "listeners"); + if (event === true) + target = null; + + this.setData(doc, "listeners", listeners.filter(function (args) { + if (target == null || args[0].get() == target && args[1] == event && args[2].wrapped == callback && args[3] == capture) { + args[0].get().removeEventListener.apply(args[0].get(), args.slice(1)); + return false; + } + return !args[0].get(); + })); + }, + + cleanup: function cleanup(reason) { + for (let doc in util.iterDocuments()) { + for (let elem in values(this.getData(doc, "overlayElements"))) + if (elem.parentNode) + elem.parentNode.removeChild(elem); + + for (let [elem, ns, name, orig, value] in values(this.getData(doc, "overlayAttributes"))) + if (getAttr(elem, ns, name) === value) + setAttr(elem, ns, name, orig); + + for (let callback in values(this.getData(doc, "cleanup"))) + util.trapErrors(callback, doc, reason); + + this.unlisten(doc, true); + + delete doc[this.id]; + delete doc.defaultView[this.id]; + } + }, + + observers: { + "toplevel-window-ready": function (window, data) { + let listener = util.wrapCallback(function listener(event) { + if (event.originalTarget === window.document) { + window.removeEventListener("DOMContentLoaded", listener.wrapper, true); + window.removeEventListener("load", listener.wrapper, true); + overlay._loadOverlays(window); + } + }); + + window.addEventListener("DOMContentLoaded", listener, true); + window.addEventListener("load", listener, true); + }, + "chrome-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); }, + "content-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); }, + "xul-window-visible": function () { + if (this.onWindowVisible) + this.onWindowVisible.forEach(f => { f.call(this); }); + this.onWindowVisible = null; + } + }, + + getData: function getData(obj, key, constructor) { + + if (!this.weakMap.has(obj)) + try { + this.weakMap.set(obj, {}); + } + catch (e if e instanceof TypeError) { + // util.dump("Bad WeakMap key: " + obj + " " + Components.stack.caller); + let { id } = this; - get content() this.config.browser.contentWindow || window.content, + if (!(id in obj && obj[id])) + obj[id] = {}; - window: window, + var data = obj[id]; + } - Module: Module, + data = data || this.weakMap.get(obj); - load: function load(script) { - for (let [i, base] in Iterator(prefix)) { - try { - JSMLoader.loadSubScript(base + script + ".js", modules, "UTF-8"); - return; - } - catch (e) { - if (typeof e !== "string") { - util.dump("Trying: " + (base + script + ".js") + ":"); - util.reportError(e); - } - } - } - try { - require(jsmodules, script); - } - catch (e) { - util.dump("Loading script " + script + ":"); - util.reportError(e); - } - }, - - newContext: function newContext(proto, normal) { - if (normal) - return create(proto); - let sandbox = Components.utils.Sandbox(window, { sandboxPrototype: proto || modules, wantXrays: false }); - // Hack: - sandbox.Object = jsmodules.Object; - sandbox.Math = jsmodules.Math; - sandbox.__proto__ = proto || modules; - return sandbox; - }, - - get ownPropertyValues() array.compact( - Object.getOwnPropertyNames(this) - .map(function (name) Object.getOwnPropertyDescriptor(this, name).value, this)), - - get moduleList() this.ownPropertyValues.filter(function (mod) mod instanceof this.ModuleBase || mod.isLocalModule, this) - }); - modules.plugins = create(modules); - modules.modules = modules; - window.dactyl = { modules: modules }; - - let prefix = [BASE, "resource://dactyl-local-content/"]; - - defineModule.time("load", null, function _load() { - ["addons", - "base", - "io", - "commands", - "completion", - "config", - "contexts", - "downloads", - "finder", - "highlight", - "javascript", - "messages", - "options", - "overlay", - "prefs", - "sanitizer", - "services", - "storage", - "styles", - "template", - "util" - ].forEach(function (name) defineModule.time("load", name, require, null, jsmodules, name)); - - ["dactyl", - "modes", - "commandline", - "abbreviations", - "autocommands", - "buffer", - "editor", - "events", - "hints", - "mappings", - "marks", - "mow", - "statusline" - ].forEach(function (name) defineModule.time("load", name, modules.load, modules, name)); - }, this); - }, - load: function onLoad(document) { - // This is getting to be horrible. --Kris - - var { modules, Module } = window.dactyl.modules; - delete window.dactyl; - - const start = Date.now(); - const deferredInit = { load: {} }; - const seen = set(); - const loaded = set(); - modules.loaded = loaded; - - function load(module, prereq, frame) { - if (isString(module)) { - if (!Module.constructors.hasOwnProperty(module)) - modules.load(module); - module = Module.constructors[module]; - } + if (arguments.length == 1) + return data; - try { - if (module.className in loaded) - return; - if (module.className in seen) - throw Error("Module dependency loop."); - set.add(seen, module.className); + if (data[key] === undefined) + if (constructor === undefined || callable(constructor)) + data[key] = (constructor || Array)(); + else + data[key] = constructor; - for (let dep in values(module.requires)) - load(Module.constructors[dep], module.className); + return data[key]; + }, - defineModule.loadLog.push("Load" + (isString(prereq) ? " " + prereq + " dependency: " : ": ") + module.className); - if (frame && frame.filename) - defineModule.loadLog.push(" from: " + util.fixURI(frame.filename) + ":" + frame.lineNumber); + setData: function setData(obj, key, val) { + let data = this.getData(obj); - let obj = defineModule.time(module.className, "init", module); - Class.replaceProperty(modules, module.className, obj); - loaded[module.className] = true; + return data[key] = val; + }, - if (loaded.dactyl && obj.signals) - modules.dactyl.registerObservers(obj); + overlayWindow: function overlayWindow(url, fn) { + if (url instanceof Ci.nsIDOMWindow) + overlay._loadOverlay(url, fn); + else { + Array.concat(url).forEach(function (url) { + if (!this.overlays[url]) + this.overlays[url] = []; + this.overlays[url].push(fn); + }, this); - frob(module.className); - } - catch (e) { - util.dump("Loading " + (module && module.className) + ":"); - util.reportError(e); - } - return modules[module.className]; + for (let doc in util.iterDocuments()) + if (~["interactive", "complete"].indexOf(doc.readyState)) { + this.observe(doc.defaultView, "xul-window-visible"); + this._loadOverlays(doc.defaultView); + } + else { + if (!this.onWindowVisible) + this.onWindowVisible = []; + this.observe(doc.defaultView, "toplevel-window-ready"); + } + } + }, + + _loadOverlays: function _loadOverlays(window) { + let overlays = this.getData(window, "overlays"); + + for each (let obj in overlay.overlays[window.document.documentURI] || []) { + if (~overlays.indexOf(obj)) + continue; + overlays.push(obj); + this._loadOverlay(window, obj(window)); + } + }, + + _loadOverlay: function _loadOverlay(window, obj) { + let doc = window.document; + let savedElems = this.getData(doc, "overlayElements"); + let savedAttrs = this.getData(doc, "overlayAttributes"); + + function insert(key, fn) { + if (obj[key]) { + let iterator = Iterator(obj[key]); + if (isArray(obj[key])) { + iterator = ([elem[1].id, elem.slice(2), elem[1]] + for each (elem in obj[key])); } - function deferInit(name, INIT, mod) { - let init = deferredInit[name] = deferredInit[name] || {}; - let className = mod.className || mod.constructor.className; + for (let [elem, xml, attrs] in iterator) { + if (elem = doc.getElementById(String(elem))) { + // Urgh. Hack. + let namespaces; + if (attrs && !isXML(attrs)) + namespaces = iter([k.slice(6), DOM.fromJSON.namespaces[v] || v] + for ([k, v] in Iterator(attrs)) + if (/^xmlns(?:$|:)/.test(k))).toObject(); + + let node; + if (isXML(xml)) + node = DOM.fromXML(xml, doc, obj.objects); + else + node = DOM.fromJSON(xml, doc, obj.objects, namespaces); + + if (!(node instanceof Ci.nsIDOMDocumentFragment)) + savedElems.push(node); + else + for (let n in array.iterValues(node.childNodes)) + savedElems.push(n); + + fn(elem, node); + + if (isXML(attrs)) + // Evilness and such. + let (oldAttrs = attrs) { + attrs = (attr for each (attr in oldAttrs)); + } - init[className] = function callee() { - if (!callee.frobbed) - defineModule.time(className, name, INIT[name], mod, - modules.dactyl, modules, window); - callee.frobbed = true; - }; + for (let attr in attrs || []) { + let [ns, localName] = DOM.parseNamespace(attr); + let name = attr; + let val = attrs[attr]; - INIT[name].require = function (name) { init[name](); }; + savedAttrs.push([elem, ns, name, getAttr(elem, ns, name), val]); + if (name === "highlight") + highlight.highlightNode(elem, val); + else + elem.setAttributeNS(ns || "", name, val); + } + } } + } + } + + insert("before", (elem, dom) => elem.parentNode.insertBefore(dom, elem)); + insert("after", (elem, dom) => elem.parentNode.insertBefore(dom, elem.nextSibling)); + insert("append", (elem, dom) => elem.appendChild(dom)); + insert("prepend", (elem, dom) => elem.insertBefore(dom, elem.firstChild)); + if (obj.ready) + util.trapErrors("ready", obj, window); + + function load(event) { + util.trapErrors("load", obj, window, event); + if (obj.visible) + if (!event || !overlay.onWindowVisible || window != util.topWindow(window)) + util.trapErrors("visible", obj, window); + else + overlay.onWindowVisible.push(function () { obj.visible(window) }); + } + + if (obj.load) + if (doc.readyState === "complete") + load(); + else + window.addEventListener("load", util.wrapCallback(function onLoad(event) { + if (event.originalTarget === doc) { + window.removeEventListener("load", onLoad.wrapper, true); + load(event); + } + }), true); - function frobModules() { - Module.list.forEach(function frobModule(mod) { - if (!mod.frobbed) { - modules.__defineGetter__(mod.className, function () { - delete modules[mod.className]; - return load(mod.className, null, Components.stack.caller); - }); - Object.keys(mod.prototype.INIT) - .forEach(function (name) { deferInit(name, mod.prototype.INIT, mod); }); - } - mod.frobbed = true; - }); + if (obj.unload || obj.cleanup) + this.listen(window, "unload", function unload(event) { + if (event.originalTarget === doc) { + overlay.unlisten(window, "unload", unload); + if (obj.unload) + util.trapErrors("unload", obj, window, event); + + if (obj.cleanup) + util.trapErrors("cleanup", obj, window, "unload", event); } - defineModule.modules.forEach(function defModule(mod) { - let names = set(Object.keys(mod.INIT)); - if ("init" in mod.INIT) - set.add(names, "init"); + }); - keys(names).forEach(function (name) { deferInit(name, mod.INIT, mod); }); - }); + if (obj.cleanup) + this.getData(doc, "cleanup").push(bind("cleanup", obj, window)); + }, + + /** + * Overlays an object with the given property overrides. Each + * property in *overrides* is added to *object*, replacing any + * original value. Functions in *overrides* are augmented with the + * new properties *super*, *supercall*, and *superapply*, in the + * same manner as class methods, so that they may call their + * overridden counterparts. + * + * @param {object} object The object to overlay. + * @param {object} overrides An object containing properties to + * override. + * @returns {function} A function which, when called, will remove + * the overlay. + */ + overlayObject: function (object, overrides) { + let original = Object.create(object); + overrides = update(Object.create(original), overrides); + + Object.getOwnPropertyNames(overrides).forEach(function (k) { + let orig, desc = Object.getOwnPropertyDescriptor(overrides, k); + if (desc.value instanceof Class.Property) + desc = desc.value.init(k) || desc.value; + + if (k in object) { + for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj)) + if (orig = Object.getOwnPropertyDescriptor(obj, k)) + Object.defineProperty(original, k, orig); + + if (!orig) + if (orig = Object.getPropertyDescriptor(object, k)) + Object.defineProperty(original, k, orig); + } + + // Guard against horrible add-ons that use eval-based monkey + // patching. + let value = desc.value; + if (callable(desc.value)) { + + delete desc.value; + delete desc.writable; + desc.get = function get() value; + desc.set = function set(val) { + if (!callable(val) || Function.prototype.toString(val).indexOf(sentinel) < 0) + Class.replaceProperty(this, k, val); + else { + let package_ = util.newURI(Components.stack.caller.filename).host; + util.reportError(Error(_("error.monkeyPatchOverlay", package_))); + util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_)); + } + }; + } - function frob(name) { values(deferredInit[name] || {}).forEach(call); } + try { + Object.defineProperty(object, k, desc); - frobModules(); - frob("init"); - modules.config.scripts.forEach(modules.load); - frobModules(); + if (callable(value)) { + var sentinel = "(function DactylOverlay() {}())"; + value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&"); + value.toSource = function toSource() toSource.toSource.call(this).replace(/\}?$/, sentinel + "; $&"); + } + } + catch (e) { + try { + if (value) { + object[k] = value; + return; + } + } + catch (f) {} + util.reportError(e); + } + }, this); - defineModule.modules.forEach(function defModule({ lazyInit, constructor: { className } }) { - if (!lazyInit) { - frob(className); - Class.replaceProperty(modules, className, modules[className]); + return function unwrap() { + for each (let k in Object.getOwnPropertyNames(original)) + if (Object.getOwnPropertyDescriptor(object, k).configurable) + Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k)); + else { + try { + object[k] = original[k]; } - else - modules.__defineGetter__(className, function () { - delete modules[className]; - frob(className); - return modules[className] = modules[className]; - }); - }); + catch (e) {} + } + }; + }, - // Module.list.forEach(load); - frob("load"); - modules.times = update({}, defineModule.times); + get activeModules() this.activeWindow && this.activeWindow.dactyl.modules, - defineModule.loadLog.push("Loaded in " + (Date.now() - start) + "ms"); + get modules() this.windows.map(w => w.dactyl.modules), - modules.events.listen(window, "unload", function onUnload() { - window.removeEventListener("unload", onUnload.wrapped, false); + /** + * The most recently active dactyl window. + */ + get activeWindow() this.windows[0], - for each (let mod in modules.moduleList.reverse()) { - mod.stale = true; + set activeWindow(win) this.windows = [win].concat(this.windows.filter(w => w != win)), - if ("destroy" in mod) - util.trapErrors("destroy", mod); - } - }, false); - } - })); - } + /** + * A list of extant dactyl windows. + */ + windows: Class.Memoize(() => []) }); endModule(); } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); } -// vim: set fdm=marker sw=4 ts=4 et ft=javascript: +// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: