/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Tilt: A WebGL-based 3D visualization of a webpage. * * The Initial Developer of the Original Code is * Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Victor Porof (original author) * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the LGPL or the GPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * ***** END LICENSE BLOCK *****/ /*global Components, Services, XPCOMUtils */ "use strict"; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); let EXPORTED_SYMBOLS = ["TiltUtils"]; /** * Module containing various helper functions used throughout Tilt. */ let TiltUtils = {}; /** * Various console/prompt output functions required by the engine. */ TiltUtils.Output = { /** * Logs a message to the console. * * @param {String} aMessage * the message to be logged */ log: function TUO_log(aMessage) { // get the console service let consoleService = Cc["@mozilla.org/consoleservice;1"] .getService(Ci.nsIConsoleService); // log the message consoleService.logStringMessage(aMessage); }, /** * Logs an error to the console. * * @param {String} aMessage * the message to be logged * @param {Object} aProperties * and object containing script error initialization details */ error: function TUO_error(aMessage, aProperties) { // make sure the properties parameter is a valid object aProperties = aProperties || {}; // get the console service let consoleService = Cc["@mozilla.org/consoleservice;1"] .getService(Ci.nsIConsoleService); // get the script error service let scriptError = Cc["@mozilla.org/scripterror;1"] .createInstance(Ci.nsIScriptError); // initialize a script error scriptError.init(aMessage, aProperties.sourceName || "", aProperties.sourceLine || "", aProperties.lineNumber || 0, aProperties.columnNumber || 0, aProperties.flags || 0, aProperties.category || ""); // log the error consoleService.logMessage(scriptError); }, /** * Shows a modal alert message popup. * * @param {String} aTitle * the title of the popup * @param {String} aMessage * the message to be logged */ alert: function TUO_alert(aTitle, aMessage) { if (!aMessage) { aMessage = aTitle; aTitle = ""; } // get the prompt service let prompt = Cc["@mozilla.org/embedcomp/prompt-service;1"] .getService(Ci.nsIPromptService); // show the alert message prompt.alert(null, aTitle, aMessage); } }; /** * Helper functions for managing preferences. */ TiltUtils.Preferences = { /** * Gets a custom Tilt preference. * If the preference does not exist, undefined is returned. If it does exist, * but the type is not correctly specified, null is returned. * * @param {String} aPref * the preference name * @param {String} aType * either "boolean", "string" or "integer" * * @return {Boolean | String | Number} the requested preference */ get: function TUP_get(aPref, aType) { if (!aPref || !aType) { return; } try { let prefs = this._branch; switch(aType) { case "boolean": return prefs.getBoolPref(aPref); case "string": return prefs.getCharPref(aPref); case "integer": return prefs.getIntPref(aPref); } return null; } catch(e) { // handle any unexpected exceptions TiltUtils.Output.error(e.message); return undefined; } }, /** * Sets a custom Tilt preference. * If the preference already exists, it is overwritten. * * @param {String} aPref * the preference name * @param {String} aType * either "boolean", "string" or "integer" * @param {String} aValue * a new preference value * * @return {Boolean} true if the preference was set successfully */ set: function TUP_set(aPref, aType, aValue) { if (!aPref || !aType || aValue === undefined || aValue === null) { return; } try { let prefs = this._branch; switch(aType) { case "boolean": return prefs.setBoolPref(aPref, aValue); case "string": return prefs.setCharPref(aPref, aValue); case "integer": return prefs.setIntPref(aPref, aValue); } } catch(e) { // handle any unexpected exceptions TiltUtils.Output.error(e.message); } return false; }, /** * Creates a custom Tilt preference. * If the preference already exists, it is left unchanged. * * @param {String} aPref * the preference name * @param {String} aType * either "boolean", "string" or "integer" * @param {String} aValue * the initial preference value * * @return {Boolean} true if the preference was initialized successfully */ create: function TUP_create(aPref, aType, aValue) { if (!aPref || !aType || aValue === undefined || aValue === null) { return; } try { let prefs = this._branch; if (!prefs.prefHasUserValue(aPref)) { switch(aType) { case "boolean": return prefs.setBoolPref(aPref, aValue); case "string": return prefs.setCharPref(aPref, aValue); case "integer": return prefs.setIntPref(aPref, aValue); } } } catch(e) { // handle any unexpected exceptions TiltUtils.Output.error(e.message); } return false; }, /** * The preferences branch for this extension. */ _branch: (function(aBranch) { return Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefService) .getBranch(aBranch); }("devtools.tilt.")) }; /** * Easy way to access the string bundle. */ TiltUtils.L10n = { /** * The string bundle element. */ stringBundle: null, /** * Returns a string in the string bundle. * If the string bundle is not found, null is returned. * * @param {String} aName * the string name in the bundle * * @return {String} the equivalent string from the bundle */ get: function TUL_get(aName) { // check to see if the parent string bundle document element is valid if (!this.stringBundle || !aName) { return null; } return this.stringBundle.GetStringFromName(aName); }, /** * Returns a formatted string using the string bundle. * If the string bundle is not found, null is returned. * * @param {String} aName * the string name in the bundle * @param {Array} aArgs * an array of arguments for the formatted string * * @return {String} the equivalent formatted string from the bundle */ format: function TUL_format(aName, aArgs) { // check to see if the parent string bundle document element is valid if (!this.stringBundle || !aName || !aArgs) { return null; } return this.stringBundle.formatStringFromName(aName, aArgs, aArgs.length); } }; /** * Utilities for accessing and manipulating a document. */ TiltUtils.DOM = { /** * Current parent node object used when creating canvas elements. */ parentNode: null, /** * Helper method, allowing to easily create and manage a canvas element. * If the width and height params are falsy, they default to the parent node * client width and height. * * @param {Document} aParentNode * the parent node used to create the canvas * if not specified, it will be reused from the cache * @param {Object} aProperties * optional, object containing some of the following props: * {Boolean} focusable * optional, true to make the canvas focusable * {Boolean} append * optional, true to append the canvas to the parent node * {Number} width * optional, specifies the width of the canvas * {Number} height * optional, specifies the height of the canvas * {String} id * optional, id for the created canvas element * * @return {HTMLCanvasElement} the newly created canvas element */ initCanvas: function TUD_initCanvas(aParentNode, aProperties) { // check to see if the parent node element is valid if (!(aParentNode = aParentNode || this.parentNode)) { return null; } // make sure the properties parameter is a valid object aProperties = aProperties || {}; // cache this parent node so that it can be reused this.parentNode = aParentNode; // create the canvas element let canvas = aParentNode.ownerDocument. createElementNS("http://www.w3.org/1999/xhtml", "canvas"); let width = aProperties.width || aParentNode.clientWidth; let height = aProperties.height || aParentNode.clientHeight; let id = aProperties.id || null; canvas.setAttribute("style", "min-width: 1px; min-height: 1px;"); canvas.setAttribute("width", width); canvas.setAttribute("height", height); canvas.setAttribute("id", id); // the canvas is unfocusable by default, we may require otherwise if (aProperties.focusable) { canvas.setAttribute("tabindex", "1"); canvas.style.outline = "none"; } // append the canvas element to the current parent node, if specified if (aProperties.append) { aParentNode.appendChild(canvas); } return canvas; }, /** * Gets the full webpage dimensions (width and height). * * @param {Window} aContentWindow * the content window holding the document * * @return {Object} an object containing the width and height coords */ getContentWindowDimensions: function TUD_getContentWindowDimensions( aContentWindow) { return { width: aContentWindow.innerWidth + aContentWindow.scrollMaxX, height: aContentWindow.innerHeight + aContentWindow.scrollMaxY }; }, /** * Returns the absolute x, y, width and height coordinates of a node, or null * if the passed node is not an ELEMENT_NODE. * * @param {Element} aNode * the node which coordinates need to be calculated * @param {Window} aContentWindow * optional, the window content holding the the document * * @return {Object} an object containing the top, left, width, height coords */ getNodeCoordinates: function TUD_getNodeCoordinates(aNode, aContentWindow) { // make sure the contentWindow parameter is a valid object aContentWindow = aContentWindow || {}; if (aNode.nodeType !== 1) { // Node.ELEMENT_NODE return null; } let rect = { top: 0, left: 0, width: 0, height: 0 }; // the preferred way of getting the bounding client rectangle let clientRect = aNode.getBoundingClientRect(); rect.top = clientRect.top + aContentWindow.pageYOffset; rect.left = clientRect.left + aContentWindow.pageXOffset; rect.width = clientRect.width; rect.height = clientRect.height; // compute the iframe position and its offset if necessary let frameRect = this.getFrameOffset( aNode.ownerDocument.defaultView.frameElement, aContentWindow); if (frameRect) { rect.top += frameRect.top; rect.left += frameRect.left; } return rect; }, /** * Retuns the parent iframe position and its offset (borders and padding), * or null if the passed frame is not valid. * * @param {Element} aNode * the iframe which offset need to be calculated * @param {Window} aContentWindow * optional, the window content holding the the document * * @return {Object} an object containing the top and left coords */ getFrameOffset: (function() { let cache = {}; return function TUD_getFrameOffset(aFrame, aContentWindow) { // make sure the contentWindow parameter is a valid object aContentWindow = aContentWindow || {}; if (!aFrame) { return null; } let id = TiltUtils.getWindowId(aFrame.contentWindow) + "," + aContentWindow.pageXOffset || 0 + "," + aContentWindow.pageYOffset || 0; // check the cache to see if this iframe offset wasn't calculated already if (cache[id] !== undefined) { return cache[id]; } let offset = { top: 0, left: 0 }; // take the parent iframe bounding rect position into account let frameRect = aFrame.getBoundingClientRect(); offset.top = frameRect.top; offset.left = frameRect.left; // compute the iframe content offset (iframe border + padding) // bug #626359 let style = aFrame.contentWindow.getComputedStyle(aFrame, null); if (style) { offset.top += parseInt(style.getPropertyValue("padding-top")) + parseInt(style.getPropertyValue("border-top-width")); offset.left += parseInt(style.getPropertyValue("padding-left")) + parseInt(style.getPropertyValue("border-left-width")); } return (cache[id] = offset); }; }()), /** * Traverses a document object model & calculates useful info for each node. * * @param {Window} aContentWindow * the window content holding the document * @param {Object} aProperties * optional, an object containing the following properties: * {Object} invisibleElements * elements which should be ignored * {Number} minSize * the minimum dimensions needed for a node to be traversed * {Number} maxX * the maximum left position of an element * {Number} maxY * the maximum top position of an element * * @return {Array} list containing nodes depths, coordinates and local names */ traverse: function TUD_traverse(aContentWindow, aProperties) { // make sure the properties parameter is a valid object aProperties = aProperties || {}; let aInvisibleElements = aProperties.invisibleElements || {}; let aMinSize = aProperties.minSize || -1; let aMaxX = aProperties.maxX || Number.MAX_VALUE; let aMaxY = aProperties.maxY || Number.MAX_VALUE; let nodes = aContentWindow.document.childNodes; let store = { info: [], nodes: [] }; let depth = 0; while (nodes.length) { let queue = []; for (let i = 0, len = nodes.length; i < len; i++) { let node = nodes[i]; // skip some nodes to avoid visualization meshes that are too bloated let name = node.localName; if (!name || aInvisibleElements[name]) { continue; } // get the x, y, width and height coordinates of the node let coord = this.getNodeCoordinates(node, aContentWindow); if (!coord) { continue; } // the maximum size slices the traversal where needed if (coord.left > aMaxX || coord.top > aMaxY) { continue; } // use this node only if it actually has visible dimensions if (coord.width > aMinSize && coord.height > aMinSize) { // save the necessary details into a list to be returned later store.info.push({ depth: depth, coord: coord, name: name }); store.nodes.push(node); } // prepare the queue array Array.prototype.push.apply(queue, name === "iframe" ? node.contentDocument.childNodes : node.childNodes); } nodes = queue; depth++; } return store; } }; /** * Binds a new owner object to the child functions. * If the new parent is not specified, it will default to the passed scope. * * @param {Object} aScope * the object from which all functions will be rebound * @param {String} aRegex * a regular expression to identify certain functions * @param {Object} aParent * the new parent for the object's functions */ TiltUtils.bindObjectFunc = function TU_bindObjectFunc(aScope, aRegex, aParent) { if (!aScope) { return; } for (let i in aScope) { try { if ("function" === typeof aScope[i] && (aRegex ? i.match(aRegex) : 1)) { aScope[i] = aScope[i].bind(aParent || aScope); } } catch(e) { TiltUtils.Output.error(e); } } }; /** * Destroys an object and deletes all members. * * @param {Object} aScope * the object from which all children will be destroyed */ TiltUtils.destroyObject = function TU_destroyObject(aScope) { if (!aScope) { return; } // objects in Tilt usually use a function to handle internal destruction if ("function" === typeof aScope.finalize) { aScope.finalize(); } for (let i in aScope) { if (aScope.hasOwnProperty(i)) { delete aScope[i]; } } }; /** * Gets the most recent browser window. * * @return {Window} the window */ TiltUtils.getBrowserWindow = function TU_getBrowserWindow() { return Cc["@mozilla.org/appshell/window-mediator;1"] .getService(Ci.nsIWindowMediator) .getMostRecentWindow("navigator:browser"); }; /** * Retrieve the unique ID of a window object. * * @param {Window} aWindow * the window to get the ID from * * @return {Number} the window ID */ TiltUtils.getWindowId = function TU_getWindowId(aWindow) { if (!aWindow) { return; } return aWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) .currentInnerWindowID; }; /** * Gets the markup document viewer zoom for the currently selected browser. * * @return {Number} the zoom ammount */ TiltUtils.getDocumentZoom = function TU_getDocumentZoom() { return TiltUtils.getBrowserWindow() .gBrowser.selectedBrowser.markupDocumentViewer.fullZoom; }; /** * Performs a garbage collection. */ TiltUtils.gc = function TU_gc() { TiltUtils.getBrowserWindow() .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) .garbageCollect(); }; /** * Clears the cache and sets all the variables to null. */ TiltUtils.clearCache = function TU_clearCache() { TiltUtils.DOM.parentNode = null; }; // bind the owner object to the necessary functions TiltUtils.bindObjectFunc(TiltUtils.Output); TiltUtils.bindObjectFunc(TiltUtils.Preferences); TiltUtils.bindObjectFunc(TiltUtils.L10n); TiltUtils.bindObjectFunc(TiltUtils.DOM); // set the necessary string bundle XPCOMUtils.defineLazyGetter(TiltUtils.L10n, "stringBundle", function() { return Services.strings.createBundle( "chrome://browser/locale/devtools/tilt.properties"); });