gecko/browser/devtools/tilt/tilt-utils.js

613 lines
17 KiB
JavaScript

/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cc, Ci, Cu} = require("chrome");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
const STACK_THICKNESS = 15;
/**
* Module containing various helper functions used throughout Tilt.
*/
this.TiltUtils = {};
module.exports = this.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)
{
if (this.suppressLogs) {
return;
}
// 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)
{
if (this.suppressErrors) {
return;
}
// 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 (this.suppressAlerts) {
return;
}
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
};
},
/**
* Calculates the position and depth to display a node, this can be overriden
* to change the visualization.
*
* @param {Window} aContentWindow
* the window content holding the document
* @param {Node} aNode
* the node to get the position for
* @param {Object} aParentPosition
* the position of the parent node, as returned by this
* function
*
* @return {Object} an object describing the node's position in 3D space
* containing the following properties:
* {Number} top
* distance along the x axis
* {Number} left
* distance along the y axis
* {Number} depth
* distance along the z axis
* {Number} width
* width of the node
* {Number} height
* height of the node
* {Number} thickness
* thickness of the node
*/
getNodePosition: function TUD_getNodePosition(aContentWindow, aNode,
aParentPosition) {
// get the x, y, width and height coordinates of the node
let coord = LayoutHelpers.getRect(aNode, aContentWindow);
if (!coord) {
return null;
}
coord.depth = aParentPosition ? (aParentPosition.depth + aParentPosition.thickness) : 0;
coord.thickness = STACK_THICKNESS;
return coord;
},
/**
* 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:
* {Function} nodeCallback
* a function to call instead of TiltUtils.DOM.getNodePosition
* to get the position and depth to display nodes
* {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 positions 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 nodeCallback = aProperties.nodeCallback || this.getNodePosition.bind(this);
let nodes = aContentWindow.document.childNodes;
let store = { info: [], nodes: [] };
let depth = 0;
let queue = [
{ parentPosition: null, nodes: aContentWindow.document.childNodes }
]
while (queue.length) {
let { nodes, parentPosition } = queue.shift();
for (let node of nodes) {
// skip some nodes to avoid visualization meshes that are too bloated
let name = node.localName;
if (!name || aInvisibleElements[name]) {
continue;
}
let coord = nodeCallback(aContentWindow, node, parentPosition);
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({ coord: coord, name: name });
store.nodes.push(node);
}
let childNodes = (name === "iframe" || name === "frame") ? node.contentDocument.childNodes : node.childNodes;
if (childNodes.length > 0)
queue.push({ parentPosition: coord, nodes: childNodes });
}
}
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];
}
}
};
/**
* 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;
};
/**
* Sets the markup document viewer zoom for the currently selected browser.
*
* @param {Window} aChromeWindow
* the top-level browser window
*
* @param {Number} the zoom ammount
*/
TiltUtils.setDocumentZoom = function TU_setDocumentZoom(aChromeWindow, aZoom) {
aChromeWindow.gBrowser.selectedBrowser.markupDocumentViewer.fullZoom = aZoom;
};
/**
* Performs a garbage collection.
*
* @param {Window} aChromeWindow
* the top-level browser window
*/
TiltUtils.gc = function TU_gc(aChromeWindow)
{
aChromeWindow.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");
});