gecko/browser/devtools/tilt/TiltUtils.jsm

715 lines
21 KiB
JavaScript
Raw Normal View History

/* -*- 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 <vporof@mozilla.com> (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;
};
/**
* Sets the markup document viewer zoom for the currently selected browser.
*
* @param {Number} the zoom ammount
*/
TiltUtils.setDocumentZoom = function TU_getDocumentZoom(aZoom) {
TiltUtils.getBrowserWindow()
.gBrowser.selectedBrowser.markupDocumentViewer.fullZoom = aZoom;
};
/**
* 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");
});