michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const {Cc, Ci, Cu} = require("chrome"); michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); michael@0: michael@0: const STACK_THICKNESS = 15; michael@0: michael@0: /** michael@0: * Module containing various helper functions used throughout Tilt. michael@0: */ michael@0: this.TiltUtils = {}; michael@0: module.exports = this.TiltUtils; michael@0: michael@0: /** michael@0: * Various console/prompt output functions required by the engine. michael@0: */ michael@0: TiltUtils.Output = { michael@0: michael@0: /** michael@0: * Logs a message to the console. michael@0: * michael@0: * @param {String} aMessage michael@0: * the message to be logged michael@0: */ michael@0: log: function TUO_log(aMessage) michael@0: { michael@0: if (this.suppressLogs) { michael@0: return; michael@0: } michael@0: // get the console service michael@0: let consoleService = Cc["@mozilla.org/consoleservice;1"] michael@0: .getService(Ci.nsIConsoleService); michael@0: michael@0: // log the message michael@0: consoleService.logStringMessage(aMessage); michael@0: }, michael@0: michael@0: /** michael@0: * Logs an error to the console. michael@0: * michael@0: * @param {String} aMessage michael@0: * the message to be logged michael@0: * @param {Object} aProperties michael@0: * and object containing script error initialization details michael@0: */ michael@0: error: function TUO_error(aMessage, aProperties) michael@0: { michael@0: if (this.suppressErrors) { michael@0: return; michael@0: } michael@0: // make sure the properties parameter is a valid object michael@0: aProperties = aProperties || {}; michael@0: michael@0: // get the console service michael@0: let consoleService = Cc["@mozilla.org/consoleservice;1"] michael@0: .getService(Ci.nsIConsoleService); michael@0: michael@0: // get the script error service michael@0: let scriptError = Cc["@mozilla.org/scripterror;1"] michael@0: .createInstance(Ci.nsIScriptError); michael@0: michael@0: // initialize a script error michael@0: scriptError.init(aMessage, michael@0: aProperties.sourceName || "", michael@0: aProperties.sourceLine || "", michael@0: aProperties.lineNumber || 0, michael@0: aProperties.columnNumber || 0, michael@0: aProperties.flags || 0, michael@0: aProperties.category || ""); michael@0: michael@0: // log the error michael@0: consoleService.logMessage(scriptError); michael@0: }, michael@0: michael@0: /** michael@0: * Shows a modal alert message popup. michael@0: * michael@0: * @param {String} aTitle michael@0: * the title of the popup michael@0: * @param {String} aMessage michael@0: * the message to be logged michael@0: */ michael@0: alert: function TUO_alert(aTitle, aMessage) michael@0: { michael@0: if (this.suppressAlerts) { michael@0: return; michael@0: } michael@0: if (!aMessage) { michael@0: aMessage = aTitle; michael@0: aTitle = ""; michael@0: } michael@0: michael@0: // get the prompt service michael@0: let prompt = Cc["@mozilla.org/embedcomp/prompt-service;1"] michael@0: .getService(Ci.nsIPromptService); michael@0: michael@0: // show the alert message michael@0: prompt.alert(null, aTitle, aMessage); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Helper functions for managing preferences. michael@0: */ michael@0: TiltUtils.Preferences = { michael@0: michael@0: /** michael@0: * Gets a custom Tilt preference. michael@0: * If the preference does not exist, undefined is returned. If it does exist, michael@0: * but the type is not correctly specified, null is returned. michael@0: * michael@0: * @param {String} aPref michael@0: * the preference name michael@0: * @param {String} aType michael@0: * either "boolean", "string" or "integer" michael@0: * michael@0: * @return {Boolean | String | Number} the requested preference michael@0: */ michael@0: get: function TUP_get(aPref, aType) michael@0: { michael@0: if (!aPref || !aType) { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: let prefs = this._branch; michael@0: michael@0: switch(aType) { michael@0: case "boolean": michael@0: return prefs.getBoolPref(aPref); michael@0: case "string": michael@0: return prefs.getCharPref(aPref); michael@0: case "integer": michael@0: return prefs.getIntPref(aPref); michael@0: } michael@0: return null; michael@0: michael@0: } catch(e) { michael@0: // handle any unexpected exceptions michael@0: TiltUtils.Output.error(e.message); michael@0: return undefined; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Sets a custom Tilt preference. michael@0: * If the preference already exists, it is overwritten. michael@0: * michael@0: * @param {String} aPref michael@0: * the preference name michael@0: * @param {String} aType michael@0: * either "boolean", "string" or "integer" michael@0: * @param {String} aValue michael@0: * a new preference value michael@0: * michael@0: * @return {Boolean} true if the preference was set successfully michael@0: */ michael@0: set: function TUP_set(aPref, aType, aValue) michael@0: { michael@0: if (!aPref || !aType || aValue === undefined || aValue === null) { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: let prefs = this._branch; michael@0: michael@0: switch(aType) { michael@0: case "boolean": michael@0: return prefs.setBoolPref(aPref, aValue); michael@0: case "string": michael@0: return prefs.setCharPref(aPref, aValue); michael@0: case "integer": michael@0: return prefs.setIntPref(aPref, aValue); michael@0: } michael@0: } catch(e) { michael@0: // handle any unexpected exceptions michael@0: TiltUtils.Output.error(e.message); michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Creates a custom Tilt preference. michael@0: * If the preference already exists, it is left unchanged. michael@0: * michael@0: * @param {String} aPref michael@0: * the preference name michael@0: * @param {String} aType michael@0: * either "boolean", "string" or "integer" michael@0: * @param {String} aValue michael@0: * the initial preference value michael@0: * michael@0: * @return {Boolean} true if the preference was initialized successfully michael@0: */ michael@0: create: function TUP_create(aPref, aType, aValue) michael@0: { michael@0: if (!aPref || !aType || aValue === undefined || aValue === null) { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: let prefs = this._branch; michael@0: michael@0: if (!prefs.prefHasUserValue(aPref)) { michael@0: switch(aType) { michael@0: case "boolean": michael@0: return prefs.setBoolPref(aPref, aValue); michael@0: case "string": michael@0: return prefs.setCharPref(aPref, aValue); michael@0: case "integer": michael@0: return prefs.setIntPref(aPref, aValue); michael@0: } michael@0: } michael@0: } catch(e) { michael@0: // handle any unexpected exceptions michael@0: TiltUtils.Output.error(e.message); michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * The preferences branch for this extension. michael@0: */ michael@0: _branch: (function(aBranch) { michael@0: return Cc["@mozilla.org/preferences-service;1"] michael@0: .getService(Ci.nsIPrefService) michael@0: .getBranch(aBranch); michael@0: michael@0: }("devtools.tilt.")) michael@0: }; michael@0: michael@0: /** michael@0: * Easy way to access the string bundle. michael@0: */ michael@0: TiltUtils.L10n = { michael@0: michael@0: /** michael@0: * The string bundle element. michael@0: */ michael@0: stringBundle: null, michael@0: michael@0: /** michael@0: * Returns a string in the string bundle. michael@0: * If the string bundle is not found, null is returned. michael@0: * michael@0: * @param {String} aName michael@0: * the string name in the bundle michael@0: * michael@0: * @return {String} the equivalent string from the bundle michael@0: */ michael@0: get: function TUL_get(aName) michael@0: { michael@0: // check to see if the parent string bundle document element is valid michael@0: if (!this.stringBundle || !aName) { michael@0: return null; michael@0: } michael@0: return this.stringBundle.GetStringFromName(aName); michael@0: }, michael@0: michael@0: /** michael@0: * Returns a formatted string using the string bundle. michael@0: * If the string bundle is not found, null is returned. michael@0: * michael@0: * @param {String} aName michael@0: * the string name in the bundle michael@0: * @param {Array} aArgs michael@0: * an array of arguments for the formatted string michael@0: * michael@0: * @return {String} the equivalent formatted string from the bundle michael@0: */ michael@0: format: function TUL_format(aName, aArgs) michael@0: { michael@0: // check to see if the parent string bundle document element is valid michael@0: if (!this.stringBundle || !aName || !aArgs) { michael@0: return null; michael@0: } michael@0: return this.stringBundle.formatStringFromName(aName, aArgs, aArgs.length); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Utilities for accessing and manipulating a document. michael@0: */ michael@0: TiltUtils.DOM = { michael@0: michael@0: /** michael@0: * Current parent node object used when creating canvas elements. michael@0: */ michael@0: parentNode: null, michael@0: michael@0: /** michael@0: * Helper method, allowing to easily create and manage a canvas element. michael@0: * If the width and height params are falsy, they default to the parent node michael@0: * client width and height. michael@0: * michael@0: * @param {Document} aParentNode michael@0: * the parent node used to create the canvas michael@0: * if not specified, it will be reused from the cache michael@0: * @param {Object} aProperties michael@0: * optional, object containing some of the following props: michael@0: * {Boolean} focusable michael@0: * optional, true to make the canvas focusable michael@0: * {Boolean} append michael@0: * optional, true to append the canvas to the parent node michael@0: * {Number} width michael@0: * optional, specifies the width of the canvas michael@0: * {Number} height michael@0: * optional, specifies the height of the canvas michael@0: * {String} id michael@0: * optional, id for the created canvas element michael@0: * michael@0: * @return {HTMLCanvasElement} the newly created canvas element michael@0: */ michael@0: initCanvas: function TUD_initCanvas(aParentNode, aProperties) michael@0: { michael@0: // check to see if the parent node element is valid michael@0: if (!(aParentNode = aParentNode || this.parentNode)) { michael@0: return null; michael@0: } michael@0: michael@0: // make sure the properties parameter is a valid object michael@0: aProperties = aProperties || {}; michael@0: michael@0: // cache this parent node so that it can be reused michael@0: this.parentNode = aParentNode; michael@0: michael@0: // create the canvas element michael@0: let canvas = aParentNode.ownerDocument. michael@0: createElementNS("http://www.w3.org/1999/xhtml", "canvas"); michael@0: michael@0: let width = aProperties.width || aParentNode.clientWidth; michael@0: let height = aProperties.height || aParentNode.clientHeight; michael@0: let id = aProperties.id || null; michael@0: michael@0: canvas.setAttribute("style", "min-width: 1px; min-height: 1px;"); michael@0: canvas.setAttribute("width", width); michael@0: canvas.setAttribute("height", height); michael@0: canvas.setAttribute("id", id); michael@0: michael@0: // the canvas is unfocusable by default, we may require otherwise michael@0: if (aProperties.focusable) { michael@0: canvas.setAttribute("tabindex", "1"); michael@0: canvas.style.outline = "none"; michael@0: } michael@0: michael@0: // append the canvas element to the current parent node, if specified michael@0: if (aProperties.append) { michael@0: aParentNode.appendChild(canvas); michael@0: } michael@0: michael@0: return canvas; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the full webpage dimensions (width and height). michael@0: * michael@0: * @param {Window} aContentWindow michael@0: * the content window holding the document michael@0: * michael@0: * @return {Object} an object containing the width and height coords michael@0: */ michael@0: getContentWindowDimensions: function TUD_getContentWindowDimensions( michael@0: aContentWindow) michael@0: { michael@0: return { michael@0: width: aContentWindow.innerWidth + aContentWindow.scrollMaxX, michael@0: height: aContentWindow.innerHeight + aContentWindow.scrollMaxY michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Calculates the position and depth to display a node, this can be overriden michael@0: * to change the visualization. michael@0: * michael@0: * @param {Window} aContentWindow michael@0: * the window content holding the document michael@0: * @param {Node} aNode michael@0: * the node to get the position for michael@0: * @param {Object} aParentPosition michael@0: * the position of the parent node, as returned by this michael@0: * function michael@0: * michael@0: * @return {Object} an object describing the node's position in 3D space michael@0: * containing the following properties: michael@0: * {Number} top michael@0: * distance along the x axis michael@0: * {Number} left michael@0: * distance along the y axis michael@0: * {Number} depth michael@0: * distance along the z axis michael@0: * {Number} width michael@0: * width of the node michael@0: * {Number} height michael@0: * height of the node michael@0: * {Number} thickness michael@0: * thickness of the node michael@0: */ michael@0: getNodePosition: function TUD_getNodePosition(aContentWindow, aNode, michael@0: aParentPosition) { michael@0: let lh = new LayoutHelpers(aContentWindow); michael@0: // get the x, y, width and height coordinates of the node michael@0: let coord = lh.getRect(aNode, aContentWindow); michael@0: if (!coord) { michael@0: return null; michael@0: } michael@0: michael@0: coord.depth = aParentPosition ? (aParentPosition.depth + aParentPosition.thickness) : 0; michael@0: coord.thickness = STACK_THICKNESS; michael@0: michael@0: return coord; michael@0: }, michael@0: michael@0: /** michael@0: * Traverses a document object model & calculates useful info for each node. michael@0: * michael@0: * @param {Window} aContentWindow michael@0: * the window content holding the document michael@0: * @param {Object} aProperties michael@0: * optional, an object containing the following properties: michael@0: * {Function} nodeCallback michael@0: * a function to call instead of TiltUtils.DOM.getNodePosition michael@0: * to get the position and depth to display nodes michael@0: * {Object} invisibleElements michael@0: * elements which should be ignored michael@0: * {Number} minSize michael@0: * the minimum dimensions needed for a node to be traversed michael@0: * {Number} maxX michael@0: * the maximum left position of an element michael@0: * {Number} maxY michael@0: * the maximum top position of an element michael@0: * michael@0: * @return {Array} list containing nodes positions and local names michael@0: */ michael@0: traverse: function TUD_traverse(aContentWindow, aProperties) michael@0: { michael@0: // make sure the properties parameter is a valid object michael@0: aProperties = aProperties || {}; michael@0: michael@0: let aInvisibleElements = aProperties.invisibleElements || {}; michael@0: let aMinSize = aProperties.minSize || -1; michael@0: let aMaxX = aProperties.maxX || Number.MAX_VALUE; michael@0: let aMaxY = aProperties.maxY || Number.MAX_VALUE; michael@0: michael@0: let nodeCallback = aProperties.nodeCallback || this.getNodePosition.bind(this); michael@0: michael@0: let nodes = aContentWindow.document.childNodes; michael@0: let store = { info: [], nodes: [] }; michael@0: let depth = 0; michael@0: michael@0: let queue = [ michael@0: { parentPosition: null, nodes: aContentWindow.document.childNodes } michael@0: ] michael@0: michael@0: while (queue.length) { michael@0: let { nodes, parentPosition } = queue.shift(); michael@0: michael@0: for (let node of nodes) { michael@0: // skip some nodes to avoid visualization meshes that are too bloated michael@0: let name = node.localName; michael@0: if (!name || aInvisibleElements[name]) { michael@0: continue; michael@0: } michael@0: michael@0: let coord = nodeCallback(aContentWindow, node, parentPosition); michael@0: if (!coord) { michael@0: continue; michael@0: } michael@0: michael@0: // the maximum size slices the traversal where needed michael@0: if (coord.left > aMaxX || coord.top > aMaxY) { michael@0: continue; michael@0: } michael@0: michael@0: // use this node only if it actually has visible dimensions michael@0: if (coord.width > aMinSize && coord.height > aMinSize) { michael@0: michael@0: // save the necessary details into a list to be returned later michael@0: store.info.push({ coord: coord, name: name }); michael@0: store.nodes.push(node); michael@0: } michael@0: michael@0: let childNodes = (name === "iframe" || name === "frame") ? node.contentDocument.childNodes : node.childNodes; michael@0: if (childNodes.length > 0) michael@0: queue.push({ parentPosition: coord, nodes: childNodes }); michael@0: } michael@0: } michael@0: michael@0: return store; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Binds a new owner object to the child functions. michael@0: * If the new parent is not specified, it will default to the passed scope. michael@0: * michael@0: * @param {Object} aScope michael@0: * the object from which all functions will be rebound michael@0: * @param {String} aRegex michael@0: * a regular expression to identify certain functions michael@0: * @param {Object} aParent michael@0: * the new parent for the object's functions michael@0: */ michael@0: TiltUtils.bindObjectFunc = function TU_bindObjectFunc(aScope, aRegex, aParent) michael@0: { michael@0: if (!aScope) { michael@0: return; michael@0: } michael@0: michael@0: for (let i in aScope) { michael@0: try { michael@0: if ("function" === typeof aScope[i] && (aRegex ? i.match(aRegex) : 1)) { michael@0: aScope[i] = aScope[i].bind(aParent || aScope); michael@0: } michael@0: } catch(e) { michael@0: TiltUtils.Output.error(e); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Destroys an object and deletes all members. michael@0: * michael@0: * @param {Object} aScope michael@0: * the object from which all children will be destroyed michael@0: */ michael@0: TiltUtils.destroyObject = function TU_destroyObject(aScope) michael@0: { michael@0: if (!aScope) { michael@0: return; michael@0: } michael@0: michael@0: // objects in Tilt usually use a function to handle internal destruction michael@0: if ("function" === typeof aScope._finalize) { michael@0: aScope._finalize(); michael@0: } michael@0: for (let i in aScope) { michael@0: if (aScope.hasOwnProperty(i)) { michael@0: delete aScope[i]; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Retrieve the unique ID of a window object. michael@0: * michael@0: * @param {Window} aWindow michael@0: * the window to get the ID from michael@0: * michael@0: * @return {Number} the window ID michael@0: */ michael@0: TiltUtils.getWindowId = function TU_getWindowId(aWindow) michael@0: { michael@0: if (!aWindow) { michael@0: return; michael@0: } michael@0: michael@0: return aWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils) michael@0: .currentInnerWindowID; michael@0: }; michael@0: michael@0: /** michael@0: * Sets the markup document viewer zoom for the currently selected browser. michael@0: * michael@0: * @param {Window} aChromeWindow michael@0: * the top-level browser window michael@0: * michael@0: * @param {Number} the zoom ammount michael@0: */ michael@0: TiltUtils.setDocumentZoom = function TU_setDocumentZoom(aChromeWindow, aZoom) { michael@0: aChromeWindow.gBrowser.selectedBrowser.markupDocumentViewer.fullZoom = aZoom; michael@0: }; michael@0: michael@0: /** michael@0: * Performs a garbage collection. michael@0: * michael@0: * @param {Window} aChromeWindow michael@0: * the top-level browser window michael@0: */ michael@0: TiltUtils.gc = function TU_gc(aChromeWindow) michael@0: { michael@0: aChromeWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils) michael@0: .garbageCollect(); michael@0: }; michael@0: michael@0: /** michael@0: * Clears the cache and sets all the variables to null. michael@0: */ michael@0: TiltUtils.clearCache = function TU_clearCache() michael@0: { michael@0: TiltUtils.DOM.parentNode = null; michael@0: }; michael@0: michael@0: // bind the owner object to the necessary functions michael@0: TiltUtils.bindObjectFunc(TiltUtils.Output); michael@0: TiltUtils.bindObjectFunc(TiltUtils.Preferences); michael@0: TiltUtils.bindObjectFunc(TiltUtils.L10n); michael@0: TiltUtils.bindObjectFunc(TiltUtils.DOM); michael@0: michael@0: // set the necessary string bundle michael@0: XPCOMUtils.defineLazyGetter(TiltUtils.L10n, "stringBundle", function() { michael@0: return Services.strings.createBundle( michael@0: "chrome://browser/locale/devtools/tilt.properties"); michael@0: });