author | Mike Ratcliffe <mratcliffe@mozilla.com> |
Tue, 30 Aug 2011 09:12:02 -0300 | |
changeset 76259 | 26bc47cdea9e1177b9cbc155dbe60b6e02fd1b6b |
parent 76258 | 2470993f6287d3b594a542023ce1cb077fda9bff |
child 76260 | 54685bf66136dc5e346e1e0873d2fc9afb4f85c0 |
push id | 21089 |
push user | rcampbell@mozilla.com |
push date | Wed, 31 Aug 2011 12:23:58 +0000 |
treeherder | mozilla-central@922f27baed98 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | dolske, msucan |
bugs | 582596 |
milestone | 9.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1010,16 +1010,19 @@ pref("services.sync.prefs.sync.xpinstall #endif // Disable the error console pref("devtools.errorconsole.enabled", false); // Enable the Inspector pref("devtools.inspector.enabled", true); +// Enable the style inspector +pref("devtools.styleinspector.enabled", true); + // Enable the Scratchpad tool. pref("devtools.scratchpad.enabled", true); // Enable tools for Chrome development. pref("devtools.chrome.enabled", false); // The last Web Console height. This is initially 0 which means that the Web // Console will use the default height next time it shows.
--- a/browser/devtools/Makefile.in +++ b/browser/devtools/Makefile.in @@ -16,16 +16,17 @@ # # The Initial Developer of the Original Code is # Netscape Communications Corporation. # Portions created by the Initial Developer are Copyright (C) 1998 # the Initial Developer. All Rights Reserved. # # Contributor(s): # Rob Campbell <rcampbell@mozilla.com> +# Mike Ratcliffe <mratcliffe@mozilla.com> # # 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 @@ -44,15 +45,17 @@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk include $(topsrcdir)/config/config.mk DIRS = \ webconsole \ scratchpad \ sourceeditor \ + styleinspector \ + shared \ $(NULL) ifdef ENABLE_TESTS # DIRS += test # no tests yet endif include $(topsrcdir)/config/rules.mk
--- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -1,6 +1,7 @@ browser.jar: content/browser/NetworkPanel.xhtml (webconsole/NetworkPanel.xhtml) * content/browser/scratchpad.xul (scratchpad/scratchpad.xul) * content/browser/scratchpad.js (scratchpad/scratchpad.js) + content/browser/csshtmltree.xhtml (styleinspector/csshtmltree.xhtml) content/browser/orion.js (sourceeditor/orion/orion.js) content/browser/orion.css (sourceeditor/orion/orion.css)
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/Makefile.in @@ -0,0 +1,55 @@ +# +# ***** 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 HUDService code. +# +# The Initial Developer of the Original Code is Mozilla Corporation. +# +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mike Ratcliffe <mratcliffe@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 GPL or the LGPL. 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 ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +ifdef ENABLE_TESTS +ifneq (mobile,$(MOZ_BUILD_APP)) + # DIRS += test # no tests yet +endif +endif + +include $(topsrcdir)/config/rules.mk + +libs:: + $(NSINSTALL) $(srcdir)/Templater.jsm $(FINAL_TARGET)/modules/devtools
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/Templater.jsm @@ -0,0 +1,398 @@ +/* -*- Mode: C++; tab-width: 8; 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 Bespin. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker (jwalker@mozilla.com) (original author) + * Mike Ratcliffe (mratcliffe@mozilla.com) + * + * 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 GPL or the LGPL. 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 ***** */ + +// WARNING: do not 'use_strict' without reading the notes in envEval; + +var EXPORTED_SYMBOLS = ["Templater"]; + +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +/** + * A templater that allows one to quickly template DOM nodes. + */ +function Templater() { + this.scope = []; +} + +/** + * Recursive function to walk the tree processing the attributes as it goes. + * @param node the node to process. + * @param data the data to use for node processing. + */ +Templater.prototype.processNode = function(node, data) { + this.scope.push(node.nodeName + (node.id ? '#' + node.id : '')); + try { + // Process attributes + if (node.attributes && node.attributes.length) { + // We need to handle 'foreach' and 'if' first because they might stop + // some types of processing from happening, and foreach must come first + // because it defines new data on which 'if' might depend. + if (node.hasAttribute('foreach')) { + this.processForEach(node, data); + return; + } + if (node.hasAttribute('if')) { + if (!this.processIf(node, data)) { + return; + } + } + // Only make the node available once we know it's not going away + data.__element = node; + // It's good to clean up the attributes when we've processed them, + // but if we do it straight away, we mess up the array index + var attrs = Array.prototype.slice.call(node.attributes); + for (let i = 0, attLen = attrs.length; i < attLen; i++) { + var value = attrs[i].value; + var name = attrs[i].name; + this.scope.push(name); + try { + if (name === 'save') { + // Save attributes are a setter using the node + value = this.stripBraces(value); + this.property(value, data, node); + node.removeAttribute('save'); + } else if (name.substring(0, 2) === 'on') { + // Event registration relies on property doing a bind + value = this.stripBraces(value); + var func = this.property(value, data); + if (typeof func !== 'function') { + this.handleError('Expected ' + value + + ' to resolve to a function, but got ' + typeof func); + } + node.removeAttribute(name); + var capture = node.hasAttribute('capture' + name.substring(2)); + node.addEventListener(name.substring(2), func, capture); + if (capture) { + node.removeAttribute('capture' + name.substring(2)); + } + } else { + // Replace references in all other attributes + var self = this; + var newValue = value.replace(/\$\{[^}]*\}/g, function(path) { + return self.envEval(path.slice(2, -1), data, value); + }); + // Remove '_' prefix of attribute names so the DOM won't try + // to use them before we've processed the template + if (name.charAt(0) === '_') { + node.removeAttribute(name); + node.setAttribute(name.substring(1), newValue); + } else if (value !== newValue) { + attrs[i].value = newValue; + } + } + } finally { + this.scope.pop(); + } + } + } + + // Loop through our children calling processNode. First clone them, so the + // set of nodes that we visit will be unaffected by additions or removals. + var children = Array.prototype.slice.call(node.childNodes); + for (let j = 0, numChildren = children.length; j < numChildren; j++) { + this.processNode(children[j], data); + } + + if (node.nodeType === Ci.nsIDOMNode.TEXT_NODE) { + this.processTextNode(node, data); + } + } finally { + this.scope.pop(); + } +}; + +/** + * Handle <x if="${...}"> + * @param node An element with an 'if' attribute + * @param data The data to use with envEval + * @returns true if processing should continue, false otherwise + */ +Templater.prototype.processIf = function(node, data) { + this.scope.push('if'); + try { + var originalValue = node.getAttribute('if'); + var value = this.stripBraces(originalValue); + var recurse = true; + try { + var reply = this.envEval(value, data, originalValue); + recurse = !!reply; + } catch (ex) { + this.handleError('Error with \'' + value + '\'', ex); + recurse = false; + } + if (!recurse) { + node.parentNode.removeChild(node); + } + node.removeAttribute('if'); + return recurse; + } finally { + this.scope.pop(); + } +}; + +/** + * Handle <x foreach="param in ${array}"> and the special case of + * <loop foreach="param in ${array}"> + * @param node An element with a 'foreach' attribute + * @param data The data to use with envEval + */ +Templater.prototype.processForEach = function(node, data) { + this.scope.push('foreach'); + try { + var originalValue = node.getAttribute('foreach'); + var value = originalValue; + + var paramName = 'param'; + if (value.charAt(0) === '$') { + // No custom loop variable name. Use the default: 'param' + value = this.stripBraces(value); + } else { + // Extract the loop variable name from 'NAME in ${ARRAY}' + var nameArr = value.split(' in '); + paramName = nameArr[0].trim(); + value = this.stripBraces(nameArr[1].trim()); + } + node.removeAttribute('foreach'); + try { + var self = this; + // Process a single iteration of a loop + var processSingle = function(member, node, ref) { + var clone = node.cloneNode(true); + clone.removeAttribute('foreach'); + ref.parentNode.insertBefore(clone, ref); + data[paramName] = member; + self.processNode(clone, data); + delete data[paramName]; + }; + + // processSingle is no good for <loop> nodes where we want to work on + // the children rather than the node itself + var processAll = function(scope, member) { + self.scope.push(scope); + try { + if (node.nodeName === 'loop') { + for (let i = 0, numChildren = node.children.length; i < numChildren; i++) { + processSingle(member, node.children[i], node); + } + } else { + processSingle(member, node, node); + } + } finally { + self.scope.pop(); + } + }; + + let reply = this.envEval(value, data, originalValue); + if (Array.isArray(reply)) { + reply.forEach(function(data, i) { + processAll('' + i, data) + }, this); + } else { + for (let param in reply) { + if (reply.hasOwnProperty(param)) { + processAll(param, param); + } + } + } + node.parentNode.removeChild(node); + } catch (ex) { + this.handleError('Error with \'' + value + '\'', ex); + } + } finally { + this.scope.pop(); + } +}; + +/** + * Take a text node and replace it with another text node with the ${...} + * sections parsed out. We replace the node by altering node.parentNode but + * we could probably use a DOM Text API to achieve the same thing. + * @param node The Text node to work on + * @param data The data to use in calls to envEval + */ +Templater.prototype.processTextNode = function(node, data) { + // Replace references in other attributes + var value = node.data; + // We can't use the string.replace() with function trick (see generic + // attribute processing in processNode()) because we need to support + // functions that return DOM nodes, so we can't have the conversion to a + // string. + // Instead we process the string as an array of parts. In order to split + // the string up, we first replace '${' with '\uF001$' and '}' with '\uF002' + // We can then split using \uF001 or \uF002 to get an array of strings + // where scripts are prefixed with $. + // \uF001 and \uF002 are just unicode chars reserved for private use. + value = value.replace(/\$\{([^}]*)\}/g, '\uF001$$$1\uF002'); + var parts = value.split(/\uF001|\uF002/); + if (parts.length > 1) { + parts.forEach(function(part) { + if (part === null || part === undefined || part === '') { + return; + } + if (part.charAt(0) === '$') { + part = this.envEval(part.slice(1), data, node.data); + } + // It looks like this was done a few lines above but see envEval + if (part === null) { + part = "null"; + } + if (part === undefined) { + part = "undefined"; + } + // if (isDOMElement(part)) { ... } + if (typeof part.cloneNode !== 'function') { + part = node.ownerDocument.createTextNode(part.toString()); + } + node.parentNode.insertBefore(part, node); + }, this); + node.parentNode.removeChild(node); + } +}; + +/** + * Warn of string does not begin '${' and end '}' + * @param str the string to check. + * @return The string stripped of ${ and }, or untouched if it does not match + */ +Templater.prototype.stripBraces = function(str) { + if (!str.match(/\$\{.*\}/g)) { + this.handleError('Expected ' + str + ' to match ${...}'); + return str; + } + return str.slice(2, -1); +}; + +/** + * Combined getter and setter that works with a path through some data set. + * For example: + * <ul> + * <li>property('a.b', { a: { b: 99 }}); // returns 99 + * <li>property('a', { a: { b: 99 }}); // returns { b: 99 } + * <li>property('a', { a: { b: 99 }}, 42); // returns 99 and alters the + * input data to be { a: { b: 42 }} + * </ul> + * @param path An array of strings indicating the path through the data, or + * a string to be cut into an array using <tt>split('.')</tt> + * @param data An object to look in for the <tt>path</tt> argument + * @param newValue (optional) If defined, this value will replace the + * original value for the data at the path specified. + * @return The value pointed to by <tt>path</tt> before any + * <tt>newValue</tt> is applied. + */ +Templater.prototype.property = function(path, data, newValue) { + this.scope.push(path); + try { + if (typeof path === 'string') { + path = path.split('.'); + } + var value = data[path[0]]; + if (path.length === 1) { + if (newValue !== undefined) { + data[path[0]] = newValue; + } + if (typeof value === 'function') { + return value.bind(data); + } + return value; + } + if (!value) { + this.handleError('Can\'t find path=' + path); + return null; + } + return this.property(path.slice(1), value, newValue); + } finally { + this.scope.pop(); + } +}; + +/** + * Like eval, but that creates a context of the variables in <tt>env</tt> in + * which the script is evaluated. + * WARNING: This script uses 'with' which is generally regarded to be evil. + * The alternative is to create a Function at runtime that takes X parameters + * according to the X keys in the env object, and then call that function using + * the values in the env object. This is likely to be slow, but workable. + * @param script The string to be evaluated. + * @param env The environment in which to eval the script. + * @param context Optional debugging string in case of failure + * @return The return value of the script, or the error message if the script + * execution failed. + */ +Templater.prototype.envEval = function(script, env, context) { + with (env) { + try { + this.scope.push(context); + return eval(script); + } catch (ex) { + this.handleError('Template error evaluating \'' + script + '\'', ex); + return script; + } finally { + this.scope.pop(); + } + } +}; + +/** + * A generic way of reporting errors, for easy overloading in different + * environments. + * @param message the error message to report. + * @param ex optional associated exception. + */ +Templater.prototype.handleError = function(message, ex) { + this.logError(message); + this.logError('In: ' + this.scope.join(' > ')); + if (ex) { + this.logError(ex); + } +}; + + +/** + * A generic way of reporting errors, for easy overloading in different + * environments. + * @param message the error message to report. + */ +Templater.prototype.logError = function(message) { + Services.console.logStringMessage(message); +}; +
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/CssHtmlTree.jsm @@ -0,0 +1,842 @@ +/* -*- Mode: C++; tab-width: 8; 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 the Mozilla Inspector Module. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker (jwalker@mozilla.com) (original author) + * Mihai Șucan <mihai.sucan@gmail.com> + * Michael Ratcliffe <mratcliffe@mozilla.com> + * + * 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 GPL or the LGPL. 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 ***** */ + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PluralForm.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/devtools/CssLogic.jsm"); +Cu.import("resource:///modules/devtools/Templater.jsm"); + +var EXPORTED_SYMBOLS = ["CssHtmlTree"]; + +/** + * CssHtmlTree is a panel that manages the display of a table sorted by style. + * There should be one instance of CssHtmlTree per style display (of which there + * will generally only be one). + * + * @params {Document} aStyleWin The main XUL browser document + * @params {CssLogic} aCssLogic How we dig into the CSS. See CssLogic.jsm + * @constructor + */ +function CssHtmlTree(aStyleWin, aCssLogic, aPanel) +{ + this.styleWin = aStyleWin; + this.cssLogic = aCssLogic; + this.doc = aPanel.ownerDocument; + this.win = this.doc.defaultView; + this.getRTLAttr = CssHtmlTree.getRTLAttr; + + // The document in which we display the results (csshtmltree.xhtml). + this.styleDocument = this.styleWin.contentWindow.document; + + // Nodes used in templating + this.root = this.styleDocument.getElementById("root"); + this.templateRoot = this.styleDocument.getElementById("templateRoot"); + this.panel = aPanel; + + // The element that we're inspecting, and the document that it comes from. + this.viewedElement = null; + this.viewedDocument = null; + + this.createStyleGroupViews(); +} + +/** + * Memonized lookup of a l10n string from a string bundle. + * @param {string} aName The key to lookup. + * @returns A localized version of the given key. + */ +CssHtmlTree.l10n = function CssHtmlTree_l10n(aName) +{ + try { + return CssHtmlTree._strings.GetStringFromName(aName); + } catch (ex) { + Services.console.logStringMessage("Error reading '" + aName + "'"); + throw new Error("l10n error with " + aName); + } +}; + +/** + * Clone the given template node, and process it by resolving ${} references + * in the template. + * + * @param {nsIDOMElement} aTemplate the template note to use. + * @param {nsIDOMElement} aDestination the destination node where the + * processed nodes will be displayed. + * @param {object} aData the data to pass to the template. + */ +CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate, aDestination, aData) +{ + aDestination.innerHTML = ""; + + // All the templater does is to populate a given DOM tree with the given + // values, so we need to clone the template first. + let duplicated = aTemplate.cloneNode(true); + new Templater().processNode(duplicated, aData); + while (duplicated.firstChild) { + aDestination.appendChild(duplicated.firstChild); + } +}; + +/** + * Checks whether the UI is RTL + * @return {Boolean} true or false + */ +CssHtmlTree.isRTL = function CssHtmlTree_isRTL() +{ + return CssHtmlTree.getRTLAttr == "rtl"; +}; + +/** + * Checks whether the UI is RTL + * @return {String} "ltr" or "rtl" + */ +XPCOMUtils.defineLazyGetter(CssHtmlTree, "getRTLAttr", function() { + let mainWindow = Services.wm.getMostRecentWindow("navigator:browser"); + return mainWindow.getComputedStyle(mainWindow.gBrowser).direction; +}); + +XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings + .createBundle("chrome://browser/locale/styleinspector.properties")); + +CssHtmlTree.prototype = { + /** + * Focus the output display on a specific element. + * @param {nsIDOMElement} aElement The highlighted node to get styles for. + */ + highlight: function CssHtmlTree_highlight(aElement) + { + this.viewedElement = aElement; + + // Reset the style groups. Without this previously expanded groups + // will fail to expand when inspecting subsequent nodes + let close = !aElement; + this.styleGroups.forEach(function(group) group.reset(close)); + + if (this.viewedElement) { + this.viewedDocument = this.viewedElement.ownerDocument; + CssHtmlTree.processTemplate(this.templateRoot, this.root, this); + } else { + this.viewedDocument = null; + this.root.innerHTML = ""; + } + }, + + /** + * Called when the user clicks on a parent element in the "current element" + * path. + * + * @param {Event} aEvent the DOM Event object. + */ + pathClick: function CssHtmlTree_pathClick(aEvent) + { + aEvent.preventDefault(); + if (aEvent.target && aEvent.target.pathElement) { + if (this.win.InspectorUI.selection) { + if (aEvent.target.pathElement != this.win.InspectorUI.selection) { + this.win.InspectorUI.inspectNode(aEvent.target.pathElement); + } + } else { + this.panel.selectNode(aEvent.target.pathElement); + } + } + }, + + /** + * Provide access to the path to get from document.body to the selected + * element. + * + * @return {array} the array holding the path from document.body to the + * selected element. + */ + get pathElements() + { + return CssLogic.getShortNamePath(this.viewedElement); + }, + + /** + * Returns arrays of categorized properties. + */ + _getPropertiesByGroup: function CssHtmlTree_getPropertiesByGroup() + { + return { + text: [ + "color", // inherit http://www.w3.org/TR/CSS21/propidx.html + "color-interpolation", // + "color-interpolation-filters", // + "direction", // inherit http://www.w3.org/TR/CSS21/propidx.html + "fill", // + "fill-opacity", // + "fill-rule", // + "filter", // + "flood-color", // + "flood-opacity", // + "font-family", // inherit http://www.w3.org/TR/CSS21/propidx.html + "font-size", // inherit http://www.w3.org/TR/CSS21/propidx.html + "font-size-adjust", // inherit http://www.w3.org/TR/WD-font/#font-size-props + "font-stretch", // inherit http://www.w3.org/TR/WD-font/#font-stretch + "font-style", // inherit http://www.w3.org/TR/CSS21/propidx.html + "font-variant", // inherit http://www.w3.org/TR/CSS21/propidx.html + "font-weight", // inherit http://www.w3.org/TR/CSS21/propidx.html + "ime-mode", // + "letter-spacing", // inherit http://www.w3.org/TR/CSS21/propidx.html + "lighting-color", // + "line-height", // inherit http://www.w3.org/TR/CSS21/propidx.html + "opacity", // no http://www.w3.org/TR/css3-color/#transparency + "quotes", // inherit http://www.w3.org/TR/CSS21/propidx.html + "stop-color", // + "stop-opacity", // + "stroke-opacity", // + "text-align", // inherit http://www.w3.org/TR/CSS21/propidx.html + "text-anchor", // + "text-decoration", // no http://www.w3.org/TR/CSS21/propidx.html + "text-indent", // inherit http://www.w3.org/TR/CSS21/propidx.html + "text-overflow", // + "text-rendering", // inherit http://www.w3.org/TR/SVG/painting.html#TextRenderingProperty ! + "text-shadow", // inherit http://www.w3.org/TR/css3-text/#text-shadow + "text-transform", // inherit http://www.w3.org/TR/CSS21/propidx.html + "vertical-align", // no http://www.w3.org/TR/CSS21/propidx.html + "white-space", // inherit http://www.w3.org/TR/CSS21/propidx.html + "word-spacing", // inherit http://www.w3.org/TR/css3-text/#word-spacing + "word-wrap", // inherit http://www.w3.org/TR/css3-text/#word-wrap + "-moz-column-count", // no http://www.w3.org/TR/css3-multicol/#column-count + "-moz-column-gap", // no http://www.w3.org/TR/css3-multicol/#column-gap + "-moz-column-rule-color", // no http://www.w3.org/TR/css3-multicol/#crc + "-moz-column-rule-style", // no http://www.w3.org/TR/css3-multicol/#column-rule-style + "-moz-column-rule-width", // no http://www.w3.org/TR/css3-multicol/#column-rule-width + "-moz-column-width", // no http://www.w3.org/TR/css3-multicol/#column-width + "-moz-font-feature-settings", // + "-moz-font-language-override", // + "-moz-hyphens", // + "-moz-text-decoration-color", // + "-moz-text-decoration-style", // + "-moz-text-decoration-line", // + "-moz-text-blink", // + "-moz-tab-size", // + ], + list: [ + "list-style-image", // inherit http://www.w3.org/TR/CSS21/propidx.html + "list-style-position", // inherit http://www.w3.org/TR/CSS21/propidx.html + "list-style-type", // inherit http://www.w3.org/TR/CSS21/propidx.html + "marker-end", // + "marker-mid", // + "marker-offset", // + "marker-start", // + ], + background: [ + "background-attachment", // no http://www.w3.org/TR/css3-background/#background-attachment + "background-clip", // no http://www.w3.org/TR/css3-background/#background-clip + "background-color", // no http://www.w3.org/TR/css3-background/#background-color + "background-image", // no http://www.w3.org/TR/css3-background/#background-image + "background-origin", // no http://www.w3.org/TR/css3-background/#background-origin + "background-position", // no http://www.w3.org/TR/css3-background/#background-position + "background-repeat", // no http://www.w3.org/TR/css3-background/#background-repeat + "background-size", // no http://www.w3.org/TR/css3-background/#background-size + "-moz-appearance", // + "-moz-background-inline-policy", // + ], + dims: [ + "width", // no http://www.w3.org/TR/CSS21/propidx.html + "height", // no http://www.w3.org/TR/CSS21/propidx.html + "max-width", // no http://www.w3.org/TR/CSS21/propidx.html + "max-height", // no http://www.w3.org/TR/CSS21/propidx.html + "min-width", // no http://www.w3.org/TR/CSS21/propidx.html + "min-height", // no http://www.w3.org/TR/CSS21/propidx.html + "margin-top", // no http://www.w3.org/TR/CSS21/propidx.html + "margin-right", // no http://www.w3.org/TR/CSS21/propidx.html + "margin-bottom", // no http://www.w3.org/TR/CSS21/propidx.html + "margin-left", // no http://www.w3.org/TR/CSS21/propidx.html + "padding-top", // no http://www.w3.org/TR/CSS21/propidx.html + "padding-right", // no http://www.w3.org/TR/CSS21/propidx.html + "padding-bottom", // no http://www.w3.org/TR/CSS21/propidx.html + "padding-left", // no http://www.w3.org/TR/CSS21/propidx.html + "clip", // no http://www.w3.org/TR/CSS21/propidx.html + "clip-path", // + "clip-rule", // + "resize", // no http://www.w3.org/TR/css3-ui/#resize + "stroke-width", // + "-moz-box-flex", // + "-moz-box-sizing", // no http://www.w3.org/TR/css3-ui/#box-sizing + ], + pos: [ + "top", // no http://www.w3.org/TR/CSS21/propidx.html + "right", // no http://www.w3.org/TR/CSS21/propidx.html + "bottom", // no http://www.w3.org/TR/CSS21/propidx.html + "left", // no http://www.w3.org/TR/CSS21/propidx.html + "display", // no http://www.w3.org/TR/CSS21/propidx.html + "float", // no http://www.w3.org/TR/CSS21/propidx.html + "clear", // no http://www.w3.org/TR/CSS21/propidx.html + "position", // no http://www.w3.org/TR/CSS21/propidx.html + "visibility", // inherit http://www.w3.org/TR/CSS21/propidx.html + "overflow", // + "overflow-x", // no http://www.w3.org/TR/CSS21/propidx.html + "overflow-y", // no http://www.w3.org/TR/CSS21/propidx.html + "z-index", // no http://www.w3.org/TR/CSS21/propidx.html + "dominant-baseline", // + "page-break-after", // + "page-break-before", // + "stroke-dashoffset", // + "unicode-bidi", // + "-moz-box-align", // + "-moz-box-direction", // + "-moz-box-ordinal-group", // + "-moz-box-orient", // + "-moz-box-pack", // + "-moz-float-edge", // + "-moz-orient", // + "-moz-stack-sizing", // + ], + border: [ + "border-top-width", // no http://www.w3.org/TR/CSS21/propidx.html + "border-right-width", // no http://www.w3.org/TR/CSS21/propidx.html + "border-bottom-width", // no http://www.w3.org/TR/CSS21/propidx.html + "border-left-width", // no http://www.w3.org/TR/CSS21/propidx.html + "border-top-color", // no http://www.w3.org/TR/CSS21/propidx.html + "border-right-color", // no http://www.w3.org/TR/CSS21/propidx.html + "border-bottom-color", // no http://www.w3.org/TR/CSS21/propidx.html + "border-left-color", // no http://www.w3.org/TR/CSS21/propidx.html + "border-top-style", // no http://www.w3.org/TR/CSS21/propidx.html + "border-right-style", // no http://www.w3.org/TR/CSS21/propidx.html + "border-bottom-style", // no http://www.w3.org/TR/CSS21/propidx.html + "border-left-style", // no http://www.w3.org/TR/CSS21/propidx.html + "border-collapse", // no http://www.w3.org/TR/CSS21/propidx.html + "border-spacing", // no http://www.w3.org/TR/CSS21/propidx.html + "outline-offset", // no http://www.w3.org/TR/CSS21/propidx.html + "outline-style", // + "outline-color", // + "outline-width", // + "border-top-left-radius", // no http://www.w3.org/TR/css3-background/#border-radius + "border-top-right-radius", // no http://www.w3.org/TR/css3-background/#border-radius + "border-bottom-right-radius", // no http://www.w3.org/TR/css3-background/#border-radius + "border-bottom-left-radius", // no http://www.w3.org/TR/css3-background/#border-radius + "-moz-border-bottom-colors", // + "-moz-border-image", // + "-moz-border-left-colors", // + "-moz-border-right-colors", // + "-moz-border-top-colors", // + "-moz-outline-radius-topleft", // no http://www.w3.org/TR/CSS2/ui.html#dynamic-outlines ? + "-moz-outline-radius-topright", // no http://www.w3.org/TR/CSS2/ui.html#dynamic-outlines ? + "-moz-outline-radius-bottomright", // no http://www.w3.org/TR/CSS2/ui.html#dynamic-outlines ? + "-moz-outline-radius-bottomleft", // no http://www.w3.org/TR/CSS2/ui.html#dynamic-outlines ? + ], + other: [ + "box-shadow", // no http://www.w3.org/TR/css3-background/#box-shadow + "caption-side", // inherit http://www.w3.org/TR/CSS21/propidx.html + "content", // no http://www.w3.org/TR/CSS21/propidx.html + "counter-increment", // no http://www.w3.org/TR/CSS21/propidx.html + "counter-reset", // no http://www.w3.org/TR/CSS21/propidx.html + "cursor", // inherit http://www.w3.org/TR/CSS21/propidx.html + "empty-cells", // inherit http://www.w3.org/TR/CSS21/propidx.html + "image-rendering", // inherit http://www.w3.org/TR/SVG/painting.html#ImageRenderingProperty + "mask", // + "pointer-events", // inherit http://www.w3.org/TR/SVG11/interact.html#PointerEventsProperty + "shape-rendering", // + "stroke", // + "stroke-dasharray", // + "stroke-linecap", // + "stroke-linejoin", // + "stroke-miterlimit", // + "table-layout", // no http://www.w3.org/TR/CSS21/propidx.html + "-moz-animation-delay", // + "-moz-animation-direction", // + "-moz-animation-duration", // + "-moz-animation-fill-mode", // + "-moz-animation-iteration-count", // + "-moz-animation-name", // + "-moz-animation-play-state", // + "-moz-animation-timing-function", // + "-moz-backface-visibility", // + "-moz-binding", // + "-moz-force-broken-image-icon", // + "-moz-image-region", // + "-moz-perspective", // + "-moz-perspective-origin", // + "-moz-transform", // no http://www.w3.org/TR/css3-2d-transforms/#transform-property + "-moz-transform-origin", // + "-moz-transition-delay", // + "-moz-transition-duration", // + "-moz-transition-property", // + "-moz-transition-timing-function", // + "-moz-user-focus", // inherit http://www.w3.org/TR/2000/WD-css3-userint-20000216#user-focus + "-moz-user-input", // inherit http://www.w3.org/TR/2000/WD-css3-userint-20000216#user-input + "-moz-user-modify", // inherit http://www.w3.org/TR/2000/WD-css3-userint-20000216#user-modify + "-moz-user-select", // no http://www.w3.org/TR/2000/WD-css3-userint-20000216#user-select + "-moz-window-shadow", // + ], + }; + }, + + /** + * The CSS groups as displayed by the UI. + */ + createStyleGroupViews: function CssHtmlTree_createStyleGroupViews() + { + if (!CssHtmlTree.propertiesByGroup) { + let pbg = CssHtmlTree.propertiesByGroup = this._getPropertiesByGroup(); + + // Add any supported properties that are not categorized to the "other" group + let mergedArray = Array.concat( + pbg.text, + pbg.list, + pbg.background, + pbg.dims, + pbg.pos, + pbg.border, + pbg.other + ); + + // Here we build and cache a list of css properties supported by the browser + // and store a list to check against. We could use any element but let's + // use the inspector style panel + let styles = this.styleWin.contentWindow.getComputedStyle(this.styleDocument.body); + CssHtmlTree.supportedPropertyLookup = {}; + for (let i = 0, numStyles = styles.length; i < numStyles; i++) { + let prop = styles.item(i); + CssHtmlTree.supportedPropertyLookup[prop] = true; + + if (mergedArray.indexOf(prop) == -1) { + pbg.other.push(prop); + } + } + + this.propertiesByGroup = CssHtmlTree.propertiesByGroup; + } + + let pbg = CssHtmlTree.propertiesByGroup; + + // These group titles are localized by their ID. See the styleinspector.properties file. + this.styleGroups = [ + new StyleGroupView(this, "Text_Fonts_and_Color", pbg.text), + new StyleGroupView(this, "Lists", pbg.list), + new StyleGroupView(this, "Background", pbg.background), + new StyleGroupView(this, "Dimensions", pbg.dims), + new StyleGroupView(this, "Positioning_and_Page_Flow", pbg.pos), + new StyleGroupView(this, "Borders", pbg.border), + new StyleGroupView(this, "Effects_and_Other", pbg.other), + ]; + }, +}; + +/** + * A container to give easy access to style group data from the template engine. + * + * @constructor + * @param {CssHtmlTree} aTree the instance of the CssHtmlTree object that we are + * working with. + * @param {string} aId the style group ID. + * @param {array} aPropertyNames the list of property names associated to this + * style group view. + */ +function StyleGroupView(aTree, aId, aPropertyNames) +{ + this.tree = aTree; + this.id = aId; + this.getRTLAttr = CssHtmlTree.getRTLAttr; + this.localName = CssHtmlTree.l10n("group." + this.id); + + this.propertyViews = []; + aPropertyNames.forEach(function(aPropertyName) { + if (this.isPropertySupported(aPropertyName)) { + this.propertyViews.push(new PropertyView(this.tree, this, aPropertyName)); + } + }, this); + + this.populated = false; + + this.templateProperties = this.tree.styleDocument.getElementById("templateProperties"); + + // Populated by templater: parent element containing the open attribute + this.element = null; + // Destination for templateProperties. + this.properties = null; +} + +StyleGroupView.prototype = { + /** + * The click event handler for the title of the style group view. + */ + click: function StyleGroupView_click() + { + // TODO: Animate opening/closing. See bug 587752. + if (this.element.hasAttribute("open")) { + this.element.removeAttribute("open"); + return; + } + + if (!this.populated) { + CssHtmlTree.processTemplate(this.templateProperties, this.properties, this); + this.populated = true; + } + + this.element.setAttribute("open", ""); + }, + + /** + * Close the style group view. + */ + close: function StyleGroupView_close() + { + if (this.element) { + this.element.removeAttribute("open"); + } + }, + + /** + * Reset the style group view and its property views. + * + * @param {boolean} aClosePanel tells if the style panel is closing or not. + */ + reset: function StyleGroupView_reset(aClosePanel) + { + this.close(); + this.populated = false; + for (let i = 0, numViews = this.propertyViews.length; i < numViews; i++) { + this.propertyViews[i].reset(); + } + + if (this.properties) { + if (aClosePanel) { + if (this.element) { + this.element.removeChild(this.properties); + } + + this.properties = null; + } else { + while (this.properties.hasChildNodes()) { + this.properties.removeChild(this.properties.firstChild); + } + } + } + }, + + /** + * Check if a CSS property is supported + * + * @param {string} aProperty the CSS property to check for + * + * @return {boolean} true or false + */ + isPropertySupported: function(aProperty) { + return aProperty && aProperty in CssHtmlTree.supportedPropertyLookup; + }, +}; + +/** + * A container to give easy access to property data from the template engine. + * + * @constructor + * @param {CssHtmlTree} aTree the CssHtmlTree instance we are working with. + * @param {StyleGroupView} aGroup the StyleGroupView instance we are working + * with. + * @param {string} aName the CSS property name for which this PropertyView + * instance will render the rules. + */ +function PropertyView(aTree, aGroup, aName) +{ + this.tree = aTree; + this.group = aGroup; + this.name = aName; + this.getRTLAttr = CssHtmlTree.getRTLAttr; + + this.populated = false; + this.showUnmatched = false; + + this.link = "https://developer.mozilla.org/en/CSS/" + aName; + + this.templateRules = this.tree.styleDocument.getElementById("templateRules"); + + // The parent element which contains the open attribute + this.element = null; + // Destination for templateRules. + this.rules = null; + + this.str = {}; +} + +PropertyView.prototype = { + /** + * The click event handler for the property name of the property view. If + * there are >0 rules then the rules are expanded. If there are 0 rules and + * >0 unmatched rules then the unmatched rules are expanded instead. + * + * @param {Event} aEvent the DOM event + */ + click: function PropertyView_click(aEvent) + { + // Clicking on the property link itself is already handled + if (aEvent.target.tagName.toLowerCase() == "a") { + return; + } + + // TODO: Animate opening/closing. See bug 587752. + if (this.element.hasAttribute("open")) { + this.element.removeAttribute("open"); + return; + } + + if (!this.populated) { + let matchedRuleCount = this.propertyInfo.matchedRuleCount; + + if (matchedRuleCount == 0 && this.showUnmatchedLink) { + this.showUnmatchedLinkClick(aEvent); + } else { + CssHtmlTree.processTemplate(this.templateRules, this.rules, this); + } + this.populated = true; + } + this.element.setAttribute("open", ""); + }, + + /** + * Get the computed style for the current property. + * + * @return {string} the computed style for the current property of the + * currently highlighted element. + */ + get value() + { + return this.propertyInfo.value; + }, + + /** + * An easy way to access the CssPropertyInfo behind this PropertyView + */ + get propertyInfo() + { + return this.tree.cssLogic.getPropertyInfo(this.name); + }, + + /** + * Compute the title of the property view. The title includes the number of + * rules that hold the current property. + * + * @param {nsIDOMElement} aElement reference to the DOM element where the rule + * title needs to be displayed. + * @return {string} The rule title. + */ + ruleTitle: function PropertyView_ruleTitle(aElement) + { + let result = ""; + let matchedRuleCount = this.propertyInfo.matchedRuleCount; + + if (matchedRuleCount > 0) { + aElement.classList.add("rule-count"); + + let str = CssHtmlTree.l10n("property.numberOfRules"); + result = PluralForm.get(matchedRuleCount, str).replace("#1", matchedRuleCount); + } else if (this.showUnmatchedLink) { + aElement.classList.add("rule-unmatched"); + + let unmatchedRuleCount = this.propertyInfo.unmatchedRuleCount; + let str = CssHtmlTree.l10n("property.numberOfUnmatchedRules"); + result = PluralForm.get(unmatchedRuleCount, str).replace("#1", unmatchedRuleCount); + } + return result; + }, + + /** + * Close the property view. + */ + close: function PropertyView_close() + { + if (this.rules && this.element) { + this.element.removeAttribute("open"); + } + }, + + /** + * Reset the property view. + */ + reset: function PropertyView_reset() + { + this.close(); + this.populated = false; + this.showUnmatched = false; + this.element = false; + }, + + /** + * Provide access to the SelectorViews that we are currently displaying + */ + get selectorViews() + { + var all = []; + + function convert(aSelectorInfo) { + all.push(new SelectorView(aSelectorInfo)); + } + + this.propertyInfo.matchedSelectors.forEach(convert); + if (this.showUnmatched) { + this.propertyInfo.unmatchedSelectors.forEach(convert); + } + + return all; + }, + + /** + * Should we display a 'X unmatched rules' link? + * @return {boolean} false if we are already showing the unmatched links or + * if there are none to display, true otherwise. + */ + get showUnmatchedLink() + { + return !this.showUnmatched && this.propertyInfo.unmatchedRuleCount > 0; + }, + + /** + * The UI has a link to allow the user to display unmatched selectors. + * This provides localized link text. + */ + get showUnmatchedLinkText() + { + let smur = CssHtmlTree.l10n("rule.showUnmatchedLink"); + let plural = PluralForm.get(this.propertyInfo.unmatchedRuleCount, smur); + return plural.replace("#1", this.propertyInfo.unmatchedRuleCount); + }, + + /** + * The action when a user clicks the 'show unmatched' link. + */ + showUnmatchedLinkClick: function PropertyView_showUnmatchedLinkClick(aEvent) + { + this.showUnmatched = true; + CssHtmlTree.processTemplate(this.templateRules, this.rules, this); + aEvent.preventDefault(); + }, +}; + +/** + * A container to view us easy access to display data from a CssRule + */ +function SelectorView(aSelectorInfo) +{ + this.selectorInfo = aSelectorInfo; + this._cacheStatusNames(); +} + +/** + * Decode for cssInfo.rule.status + * @see SelectorView.prototype._cacheStatusNames + * @see CssLogic.STATUS + */ +SelectorView.STATUS_NAMES = [ + // "Unmatched", "Parent Match", "Matched", "Best Match" +]; + +SelectorView.CLASS_NAMES = [ + "unmatched", "parentmatch", "matched", "bestmatch" +]; + +SelectorView.prototype = { + /** + * Cache localized status names. + * + * These statuses are localized inside the styleinspector.properties string bundle. + * @see CssLogic.jsm - the CssLogic.STATUS array. + * + * @return {void} + */ + _cacheStatusNames: function SelectorView_cacheStatusNames() + { + if (SelectorView.STATUS_NAMES.length) { + return; + } + + for (let status in CssLogic.STATUS) { + let i = CssLogic.STATUS[status]; + if (i > -1) { + let value = CssHtmlTree.l10n("rule.status." + status); + // Replace normal spaces with non-breaking spaces + SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0'); + } + } + }, + + /** + * A localized version of cssRule.status + */ + get statusText() + { + return SelectorView.STATUS_NAMES[this.selectorInfo.status]; + }, + + /** + * Get class name for selector depending on status + */ + get statusClass() + { + return SelectorView.CLASS_NAMES[this.selectorInfo.status]; + }, + + /** + * A localized Get localized human readable info + */ + humanReadableText: function SelectorView_humanReadableText(aElement) + { + if (CssHtmlTree.isRTL()) { + return this.selectorInfo.value + " \u2190 " + this.text(aElement); + } else { + return this.text(aElement) + " \u2192 " + this.selectorInfo.value; + } + }, + + text: function SelectorView_text(aElement) { + let result = this.selectorInfo.selector.text; + if (this.selectorInfo.elementStyle) { + if (this.selectorInfo.sourceElement == this.win.InspectorUI.selection) { + result = "this"; + } else { + result = CssLogic.getShortName(this.selectorInfo.sourceElement); + aElement.parentNode.querySelector(".rule-link > a"). + addEventListener("click", function(aEvent) { + this.win.InspectorUI.inspectNode(this.selectorInfo.sourceElement); + aEvent.preventDefault(); + }, false); + } + + result += ".style"; + } + return result; + }, +};
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/CssLogic.jsm @@ -0,0 +1,1603 @@ +/* -*- Mode: C++; tab-width: 8; 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 the Mozilla Inspector Module. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker <jwalker@mozilla.com> (original author) + * Mihai Șucan <mihai.sucan@gmail.com> + * Michael Ratcliffe <mratcliffe@mozilla.com> + * + * 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 GPL or the LGPL. 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 ***** */ + +/* + * About the objects defined in this file: + * - CssLogic contains style information about a view context. It provides + * access to 2 sets of objects: Css[Sheet|Rule|Selector] provide access to + * information that does not change when the selected element changes while + * Css[Property|Selector]Info provide information that is dependent on the + * selected element. + * Its key methods are highlight(), getPropertyInfo() and forEachSheet(), etc + * It also contains a number of static methods for l10n, naming, etc + * + * - CssSheet provides a more useful API to a DOM CSSSheet for our purposes, + * including shortSource and href. + * - CssRule a more useful API to a nsIDOMCSSRule including access to the group + * of CssSelectors that the rule provides properties for + * - CssSelector A single selector - i.e. not a selector group. In other words + * a CssSelector does not contain ','. This terminology is different from the + * standard DOM API, but more inline with the definition in the spec. + * + * - CssPropertyInfo contains style information for a single property for the + * highlighted element. It divides the CSS rules on the page into matched and + * unmatched rules. + * - CssSelectorInfo is a wrapper around CssSelector, which adds sorting with + * reference to the selected element. + */ + +/** + * Provide access to the style information in a page. + * CssLogic uses the standard DOM API, and the Gecko inIDOMUtils API to access + * styling information in the page, and present this to the user in a way that + * helps them understand: + * - why their expectations may not have been fulfilled + * - how browsers process CSS + * @constructor + */ +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"); + +var EXPORTED_SYMBOLS = ["CssLogic"]; + +function CssLogic() +{ + // The cache of examined CSS properties. + _propertyInfos: {}; +} + +/** + * Special values for filter, in addition to an href these values can be used + */ +CssLogic.FILTER = { + ALL: "all", // show properties from all user style sheets. + UA: "ua", // ALL, plus user-agent (i.e. browser) style sheets +}; + +/** + * Known media values. To distinguish "all" stylesheets (above) from "all" media + * The full list includes braille, embossed, handheld, print, projection, + * speech, tty, and tv, but this is only a hack because these are not defined + * in the DOM at all. + * @see http://www.w3.org/TR/CSS21/media.html#media-types + */ +CssLogic.MEDIA = { + ALL: "all", + SCREEN: "screen", +}; + +/** + * Each rule has a status, the bigger the number, the better placed it is to + * provide styling information. + * + * These statuses are localized inside the styleinspector.properties string bundle. + * @see csshtmltree.js RuleView._cacheStatusNames() + */ +CssLogic.STATUS = { + BEST: 3, + MATCHED: 2, + PARENT_MATCH: 1, + UNMATCHED: 0, + UNKNOWN: -1, +}; + +CssLogic.prototype = { + // Both setup by highlight(). + viewedElement: null, + viewedDocument: null, + + // The cache of the known sheets. + _sheets: null, + + // Have the sheets been cached? + _sheetsCached: false, + + // The total number of rules, in all stylesheets, after filtering. + _ruleCount: 0, + + // The computed styles for the viewedElement. + _computedStyle: null, + + // Source filter. Only display properties coming from the given source + _sourceFilter: CssLogic.FILTER.ALL, + + // Used for tracking unique CssSheet/CssRule/CssSelector objects, in a run of + // processMatchedSelectors(). + _passId: 0, + + // Used for tracking matched CssSelector objects, such that we can skip them + // in processUnmatchedSelectors(). + _matchId: 0, + + _matchedSelectors: null, + _unmatchedSelectors: null, + + domUtils: Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils), + + /** + * Reset various properties + */ + reset: function CssLogic_reset() + { + this._propertyInfos = {}; + this._ruleCount = 0; + this._sheetIndex = 0; + this._sheets = {}; + this._sheetsCached = false; + this._matchedSelectors = null; + this._unmatchedSelectors = null; + }, + + /** + * Focus on a new element - remove the style caches. + * + * @param {nsIDOMElement} aViewedElement the element the user has highlighted + * in the Inspector. + */ + highlight: function CssLogic_highlight(aViewedElement) + { + if (!aViewedElement) { + this.viewedElement = null; + this.viewedDocument = null; + this._computedStyle = null; + this.reset(); + return; + } + + this.viewedElement = aViewedElement; + + let doc = this.viewedElement.ownerDocument; + if (doc != this.viewedDocument) { + // New document: clear/rebuild the cache. + this.viewedDocument = doc; + + // Hunt down top level stylesheets, and cache them. + this._cacheSheets(); + } else { + // Clear cached data in the CssPropertyInfo objects. + this._propertyInfos = {}; + } + + this._matchedSelectors = null; + this._unmatchedSelectors = null; + let win = this.viewedDocument.defaultView; + this._computedStyle = win.getComputedStyle(this.viewedElement, ""); + }, + + /** + * Get the source filter. + * @returns {string} The source filter being used. + */ + get sourceFilter() { + return this._sourceFilter; + }, + + /** + * Source filter. Only display properties coming from the given source (web + * address). + * @see CssLogic.FILTER.* + */ + set sourceFilter(aValue) { + let oldValue = this._sourceFilter; + this._sourceFilter = aValue; + + let ruleCount = 0; + + // Update the CssSheet objects. + this.forEachSheet(function(aSheet) { + aSheet._sheetAllowed = -1; + if (!aSheet.systemSheet && aSheet.sheetAllowed) { + ruleCount += aSheet.ruleCount; + } + }, this); + + this._ruleCount = ruleCount; + + // Full update is needed because the this.processMatchedSelectors() method + // skips UA stylesheets if the filter does not allow such sheets. + let needFullUpdate = (oldValue == CssLogic.FILTER.UA || + aValue == CssLogic.FILTER.UA); + + if (needFullUpdate) { + this._matchedSelectors = null; + this._unmatchedSelectors = null; + this._propertyInfos = {}; + } else { + // Update the CssPropertyInfo objects. + for each (let propertyInfo in this._propertyInfos) { + propertyInfo.needRefilter = true; + } + } + }, + + /** + * Return a CssPropertyInfo data structure for the currently viewed element + * and the specified CSS property. If there is no currently viewed element we + * return an empty object. + * + * @param {string} aProperty The CSS property to look for. + * @return {CssPropertyInfo} a CssPropertyInfo structure for the given + * property. + */ + getPropertyInfo: function CssLogic_getPropertyInfo(aProperty) + { + if (!this.viewedElement) { + return {}; + } + + let info = this._propertyInfos[aProperty]; + if (!info) { + info = new CssPropertyInfo(this, aProperty); + this._propertyInfos[aProperty] = info; + } + + return info; + }, + + /** + * Cache all the stylesheets in the inspected document + * @private + */ + _cacheSheets: function CssLogic_cacheSheets() + { + this._passId++; + this.reset(); + + // styleSheets isn't an array, but forEach can work on it anyway + Array.prototype.forEach.call(this.viewedDocument.styleSheets, + this._cacheSheet, this); + + this._sheetsCached = true; + }, + + /** + * Cache a stylesheet if it falls within the requirements: if it's enabled, + * and if the @media is allowed. This method also walks through the stylesheet + * cssRules to find @imported rules, to cache the stylesheets of those rules + * as well. + * + * @private + * @param {CSSStyleSheet} aDomSheet the CSSStyleSheet object to cache. + */ + _cacheSheet: function CssLogic_cacheSheet(aDomSheet) + { + if (aDomSheet.disabled) { + return; + } + + // Only work with stylesheets that have their media allowed. + if (!CssLogic.sheetMediaAllowed(aDomSheet)) { + return; + } + + // Cache the sheet. + let cssSheet = this.getSheet(aDomSheet, false, this._sheetIndex++); + if (cssSheet._passId != this._passId) { + cssSheet._passId = this._passId; + + // Find import rules. + Array.prototype.forEach.call(aDomSheet.cssRules, function(aDomRule) { + if (aDomRule.type == Ci.nsIDOMCSSRule.IMPORT_RULE && aDomRule.styleSheet && + CssLogic.sheetMediaAllowed(aDomRule)) { + this._cacheSheet(aDomRule.styleSheet); + } + }, this); + } + }, + + /** + * Retrieve the list of stylesheets in the document. + * + * @return {array} the list of stylesheets in the document. + */ + get sheets() + { + if (!this._sheetsCached) { + this._cacheSheets(); + } + + let sheets = []; + this.forEachSheet(function (aSheet) { + if (!aSheet.systemSheet) { + sheets.push(aSheet); + } + }, this); + + return sheets; + }, + + /** + * Retrieve a CssSheet object for a given a CSSStyleSheet object. If the + * stylesheet is already cached, you get the existing CssSheet object, + * otherwise the new CSSStyleSheet object is cached. + * + * @param {CSSStyleSheet} aDomSheet the CSSStyleSheet object you want. + * @param {boolean} aSystemSheet tells if the stylesheet is a browser-provided + * sheet or not. + * @param {number} aIndex the index, within the document, of the stylesheet. + * + * @return {CssSheet} the CssSheet object for the given CSSStyleSheet object. + */ + getSheet: function CL_getSheet(aDomSheet, aSystemSheet, aIndex) + { + let cacheId = aSystemSheet ? "1" : "0"; + + if (aDomSheet.href) { + cacheId += aDomSheet.href; + } else if (aDomSheet.ownerNode && aDomSheet.ownerNode.ownerDocument) { + cacheId += aDomSheet.ownerNode.ownerDocument.location; + } + + let sheet = null; + let sheetFound = false; + + if (cacheId in this._sheets) { + for (let i = 0, numSheets = this._sheets[cacheId].length; i < numSheets; i++) { + sheet = this._sheets[cacheId][i]; + if (sheet.domSheet == aDomSheet) { + sheet.index = aIndex; + sheetFound = true; + break; + } + } + } + + if (!sheetFound) { + if (!(cacheId in this._sheets)) { + this._sheets[cacheId] = []; + } + + sheet = new CssSheet(this, aDomSheet, aSystemSheet, aIndex); + if (sheet.sheetAllowed && !aSystemSheet) { + this._ruleCount += sheet.ruleCount; + } + + this._sheets[cacheId].push(sheet); + } + + return sheet; + }, + + /** + * Process each cached stylesheet in the document using your callback. + * + * @param {function} aCallback the function you want executed for each of the + * CssSheet objects cached. + * @param {object} aScope the scope you want for the callback function. aScope + * will be the this object when aCallback executes. + */ + forEachSheet: function CssLogic_forEachSheet(aCallback, aScope) + { + for each (let sheet in this._sheets) { + sheet.forEach(aCallback, aScope); + } + }, + + /** + * Get the number nsIDOMCSSRule objects in the document, counted from all of + * the stylesheets. System sheets are excluded. If a filter is active, this + * tells only the number of nsIDOMCSSRule objects inside the selected + * CSSStyleSheet. + * + * WARNING: This only provides an estimate of the rule count, and the results + * could change at a later date. Todo remove this + * + * @return {number} the number of nsIDOMCSSRule (all rules). + */ + get ruleCount() + { + if (!this._sheetsCached) { + this._cacheSheets(); + } + + return this._ruleCount; + }, + + /** + * Process the CssSelector objects that match the highlighted element and its + * parent elements. aScope.aCallback() is executed for each CssSelector + * object, being passed the CssSelector object and the match status. + * + * This method also includes all of the element.style properties, for each + * highlighted element parent and for the highlighted element itself. + * + * Note that the matched selectors are cached, such that next time your + * callback is invoked for the cached list of CssSelector objects. + * + * @param {function} aCallback the function you want to execute for each of + * the matched selectors. + * @param {object} aScope the scope you want for the callback function. aScope + * will be the this object when aCallback executes. + */ + processMatchedSelectors: function CL_processMatchedSelectors(aCallback, aScope) + { + if (this._matchedSelectors) { + if (aCallback) { + this._passId++; + this._matchedSelectors.forEach(function(aValue) { + aCallback.call(aScope, aValue[0], aValue[1]); + aValue[0]._cssRule._passId = this._passId; + }, this); + } + return; + } + + this._matchedSelectors = []; + this._unmatchedSelectors = null; + this._passId++; + this._matchId++; + + let element = this.viewedElement; + let filter = this.sourceFilter; + let sheetIndex = 0; + let domRules = null; + do { + try { + domRules = this.domUtils.getCSSStyleRules(element); + } catch (ex) { + Services.console. + logStringMessage("CssLogic_processMatchedSelectors error: " + ex); + continue; + } + + let status = (this.viewedElement == element) ? + CssLogic.STATUS.MATCHED : CssLogic.STATUS.PARENT_MATCH; + + for (let i = 0, numRules = domRules.Count(); i < numRules; i++) { + let domRule = domRules.GetElementAt(i); + if (domRule.type !== Ci.nsIDOMCSSRule.STYLE_RULE) { + continue; + } + + let domSheet = domRule.parentStyleSheet; + let systemSheet = CssLogic.isSystemStyleSheet(domSheet); + if (filter !== CssLogic.FILTER.UA && systemSheet) { + continue; + } + + let sheet = this.getSheet(domSheet, systemSheet, sheetIndex); + let rule = sheet.getRule(domRule); + + rule.selectors.forEach(function (aSelector) { + if (aSelector._matchId !== this._matchId && + element.mozMatchesSelector(aSelector)) { + aSelector._matchId = this._matchId; + this._matchedSelectors.push([ aSelector, status ]); + if (aCallback) { + aCallback.call(aScope, aSelector, status); + } + } + }, this); + + if (sheet._passId !== this._passId) { + sheetIndex++; + sheet._passId = this._passId; + } + + if (rule._passId !== this._passId) { + rule._passId = this._passId; + } + } + + // Add element.style information. + if (element.style.length > 0) { + let rule = new CssRule(null, { style: element.style }, element); + let selector = rule.selectors[0]; + selector._matchId = this._matchId; + + this._matchedSelectors.push([ selector, status ]); + if (aCallback) { + aCallback.call(aScope, selector, status); + } + rule._passId = this._passId; + } + } while ((element = element.parentNode) && + element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE); + }, + + /** + * Process the CssSelector object that do not match the highlighted elements, + * nor its parents. Your callback function is invoked for every such + * CssSelector object. You receive one argument: the CssSelector object. + * + * The list of unmatched selectors is cached. + * + * @param {function} aCallback the function you want to execute for each of + * the unmatched selectors. + * @param {object} aScope the scope you want for the callback function. aScope + * will be the this object when aCallback executes. + */ + processUnmatchedSelectors: function CL_processUnmatchedSelectors(aCallback, aScope) + { + if (!this._matchedSelectors) { + this.processMatchedSelectors(); + } + + if (this._unmatchedSelectors) { + if (aCallback) { + this._unmatchedSelectors.forEach(aCallback, aScope); + } + return; + } + + this._unmatchedSelectors = []; + + this.forEachSheet(function (aSheet) { + aSheet.forEachRule(function (aRule) { + aRule.selectors.forEach(function (aSelector) { + if (aSelector._matchId != this._matchId) { + this._unmatchedSelectors.push(aSelector); + if (aCallback) { + aCallback.call(aScope, aSelector); + } + } + }, this); + }, this); + }, this); + }, +}; + +/** + * If the element has an id, return '#id'. Otherwise return 'tagname[n]' where + * n is the index of this element in its siblings. + * <p>A technically more 'correct' output from the no-id case might be: + * 'tagname:nth-of-type(n)' however this is unlikely to be more understood + * and it is longer. + * + * @param {nsIDOMElement} aElement the element for which you want the short name. + * @return {string} the string to be displayed for aElement. + */ +CssLogic.getShortName = function CssLogic_getShortName(aElement) +{ + if (!aElement) { + return "null"; + } + if (aElement.id) { + return "#" + aElement.id; + } + let priorSiblings = 0; + let temp = aElement; + while (temp = temp.previousElementSibling) { + priorSiblings++; + } + return aElement.tagName + "[" + priorSiblings + "]"; +}; + +/** + * Get an array of short names from the given element to document.body. + * + * @param {nsIDOMElement} aElement the element for which you want the array of + * short names. + * @return {array} The array of elements. + * <p>Each element is an object of the form: + * <ul> + * <li>{ display: "what to display for the given (parent) element", + * <li> element: referenceToTheElement } + * </ul> + */ +CssLogic.getShortNamePath = function CssLogic_getShortNamePath(aElement) +{ + let doc = aElement.ownerDocument; + let reply = []; + + if (!aElement) { + return reply; + } + + // We want to exclude nodes high up the tree (body/html) unless the user + // has selected that node, in which case we need to report something. + do { + reply.unshift({ + display: CssLogic.getShortName(aElement), + element: aElement + }); + aElement = aElement.parentNode; + } while (aElement && aElement != doc.body && aElement != doc.head && aElement != doc); + + return reply; +}; + +/** + * Memonized lookup of a l10n string from a string bundle. + * @param {string} aName The key to lookup. + * @returns A localized version of the given key. + */ +CssLogic.l10n = function(aName) CssLogic._strings.GetStringFromName(aName); + +XPCOMUtils.defineLazyGetter(CssLogic, "_strings", function() Services.strings + .createBundle("chrome://browser/locale/styleinspector.properties")); + +/** + * Is the given property sheet a system (user agent) stylesheet? + * + * @param {CSSStyleSheet} aSheet a stylesheet + * @return {boolean} true if the given stylesheet is a system stylesheet or + * false otherwise. + */ +CssLogic.isSystemStyleSheet = function CssLogic_isSystemStyleSheet(aSheet) +{ + if (!aSheet) { + return true; + } + + let url = aSheet.href; + + if (!url) return false; + if (url.length === 0) return true; + + // Check for http[s] + if (url[0] === 'h') return false; + if (url.substr(0, 9) === "resource:") return true; + if (url.substr(0, 7) === "chrome:") return true; + if (url === "XPCSafeJSObjectWrapper.cpp") return true; + if (url.substr(0, 6) === "about:") return true; + + return false; +}; + +/** + * Check if the given DOM CSS object holds an allowed media. Currently we only + * allow media screen or all. + * + * @param {CSSStyleSheet|CSSImportRule|CSSMediaRule} aDomObject the + * DOM object you want checked. + * @return {boolean} true if the media description is allowed, or false + * otherwise. + */ +CssLogic.sheetMediaAllowed = function CssLogic_sheetMediaAllowed(aDomObject) +{ + let result = false; + let media = aDomObject.media; + + if (media.length > 0) { + let mediaItem = null; + for (let m = 0, mediaLen = media.length; m < mediaLen; m++) { + mediaItem = media.item(m).toLowerCase(); + if (mediaItem === CssLogic.MEDIA.SCREEN || + mediaItem === CssLogic.MEDIA.ALL) { + result = true; + break; + } + } + } else { + result = true; + } + + return result; +}; + +/** + * A safe way to access cached bits of information about a stylesheet. + * + * @constructor + * @param {CssLogic} aCssLogic pointer to the CssLogic instance working with + * this CssSheet object. + * @param {CSSStyleSheet} aDomSheet reference to a DOM CSSStyleSheet object. + * @param {boolean} aSystemSheet tells if the stylesheet is system-provided. + * @param {number} aIndex tells the index/position of the stylesheet within the + * main document. + */ +function CssSheet(aCssLogic, aDomSheet, aSystemSheet, aIndex) +{ + this._cssLogic = aCssLogic; + this.domSheet = aDomSheet; + this.systemSheet = aSystemSheet; + this.index = this.systemSheet ? -100 * aIndex : aIndex; + + // Cache of the sheets href. Cached by the getter. + this._href = null; + // Short version of href for use in select boxes etc. Cached by getter. + this._shortSource = null; + + // null for uncached. + this._sheetAllowed = null; + + // Cached CssRules from the given stylesheet. + this._rules = {}; + + this._ruleCount = -1; +} + +CssSheet.prototype = { + /** + * Get a source for a stylesheet, taking into account embedded stylesheets + * for which we need to use document.defaultView.location.href rather than + * sheet.href + * + * @return {string} the address of the stylesheet. + */ + get href() + { + if (!this._href) { + this._href = this.domSheet.href; + if (!this._href) { + this._href = this.domSheet.ownerNode.ownerDocument.location; + } + } + + return this._href; + }, + + /** + * Create a shorthand version of the href of a stylesheet. + * + * @return {string} the shorthand source of the stylesheet. + */ + get shortSource() + { + if (this._shortSource) { + return this._shortSource; + } + + // Use a string like "inline" if there is no source href + if (!this.domSheet.href) { + this._shortSource = CssLogic.l10n("rule.sourceInline"); + return this._shortSource; + } + + // We try, in turn, the filename, filePath, query string, whole thing + let url = Services.io.newURI(this.domSheet.href, null, null); + url = url.QueryInterface(Ci.nsIURL); + if (url.fileName) { + this._shortSource = url.fileName; + return this._shortSource; + } + + if (url.filePath) { + this._shortSource = url.filePath; + return this._shortSource; + } + + if (url.query) { + this._shortSource = url.query; + return this._shortSource; + } + + this._shortSource = this.domSheet.href; + return this._shortSource; + }, + + /** + * Tells if the sheet is allowed or not by the current CssLogic.sourceFilter. + * + * @return {boolean} true if the stylesheet is allowed by the sourceFilter, or + * false otherwise. + */ + get sheetAllowed() + { + if (this._sheetAllowed !== null) { + return this._sheetAllowed; + } + + this._sheetAllowed = true; + + let filter = this._cssLogic.sourceFilter; + if (filter === CssLogic.FILTER.ALL && this.systemSheet) { + this._sheetAllowed = false; + } + if (filter !== CssLogic.FILTER.ALL && filter !== CssLogic.FILTER.UA) { + this._sheetAllowed = (filter === this.href); + } + + return this._sheetAllowed; + }, + + /** + * Retrieve the number of rules in this stylesheet. + * + * @return {number} the number of nsIDOMCSSRule objects in this stylesheet. + */ + get ruleCount() + { + return this._ruleCount > -1 ? + this._ruleCount : + this.domSheet.cssRules.length; + }, + + /** + * Retrieve a CssRule object for the given CSSStyleRule. The CssRule object is + * cached, such that subsequent retrievals return the same CssRule object for + * the same CSSStyleRule object. + * + * @param {CSSStyleRule} aDomRule the CSSStyleRule object for which you want a + * CssRule object. + * @return {CssRule} the cached CssRule object for the given CSSStyleRule + * object. + */ + getRule: function CssSheet_getRule(aDomRule) + { + let cacheId = aDomRule.type + aDomRule.selectorText; + + let rule = null; + let ruleFound = false; + + if (cacheId in this._rules) { + for (let i = 0, rulesLen = this._rules[cacheId].length; i < rulesLen; i++) { + rule = this._rules[cacheId][i]; + if (rule._domRule == aDomRule) { + ruleFound = true; + break; + } + } + } + + if (!ruleFound) { + if (!(cacheId in this._rules)) { + this._rules[cacheId] = []; + } + + rule = new CssRule(this, aDomRule); + this._rules[cacheId].push(rule); + } + + return rule; + }, + + /** + * Process each rule in this stylesheet using your callback function. Your + * function receives one argument: the CssRule object for each CSSStyleRule + * inside the stylesheet. + * + * Note that this method also iterates through @media rules inside the + * stylesheet. + * + * @param {function} aCallback the function you want to execute for each of + * the style rules. + * @param {object} aScope the scope you want for the callback function. aScope + * will be the this object when aCallback executes. + */ + forEachRule: function CssSheet_forEachRule(aCallback, aScope) + { + let ruleCount = 0; + let domRules = this.domSheet.cssRules; + + function _iterator(aDomRule) { + if (aDomRule.type == Ci.nsIDOMCSSRule.STYLE_RULE) { + aCallback.call(aScope, this.getRule(aDomRule)); + ruleCount++; + } else if (aDomRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE && + aDomRule.cssRules && CssLogic.sheetMediaAllowed(aDomRule)) { + Array.prototype.forEach.call(aDomRule.cssRules, _iterator, this); + } + } + + Array.prototype.forEach.call(domRules, _iterator, this); + + this._ruleCount = ruleCount; + }, + + toString: function CssSheet_toString() + { + return "CssSheet[" + this.shortSource + "]"; + }, +}; + +/** + * Information about a single CSSStyleRule. + * + * @param {CSSSheet|null} aCssSheet the CssSheet object of the stylesheet that + * holds the CSSStyleRule. If the rule comes from element.style, set this + * argument to null. + * @param {CSSStyleRule|object} aDomRule the DOM CSSStyleRule for which you want + * to cache data. If the rule comes from element.style, then provide + * an object of the form: {style: element.style}. + * @param {Element} [aElement] If the rule comes from element.style, then this + * argument must point to the element. + * @constructor + */ +function CssRule(aCssSheet, aDomRule, aElement) +{ + this._cssSheet = aCssSheet; + this._domRule = aDomRule; + + if (this._cssSheet) { + // parse _domRule.selectorText on call to this.selectors + this._selectors = null; + this.line = this._cssSheet._cssLogic.domUtils.getRuleLine(this._domRule); + this.source = this._cssSheet.shortSource + ":" + this.line; + this.href = this._cssSheet.href; + this.systemRule = this._cssSheet.systemSheet; + } else if (aElement) { + this._selectors = [ new CssSelector(this, "@element.style") ]; + this.line = -1; + this.source = CssLogic.l10n("rule.sourceElement"); + this.href = "#"; + this.systemRule = false; + this.sourceElement = aElement; + } +} + +CssRule.prototype = { + /** + * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter. + * + * @return {boolean} true if the parent stylesheet is allowed by the current + * sourceFilter, or false otherwise. + */ + get sheetAllowed() + { + return this._cssSheet ? this._cssSheet.sheetAllowed : true; + }, + + /** + * Retrieve the parent stylesheet index/position in the viewed document. + * + * @return {number} the parent stylesheet index/position in the viewed + * document. + */ + get sheetIndex() + { + return this._cssSheet ? this._cssSheet.index : 0; + }, + + /** + * Retrieve the style property value from the current CSSStyleRule. + * + * @param {string} aProperty the CSS property name for which you want the + * value. + * @return {string} the property value. + */ + getPropertyValue: function(aProperty) + { + return this._domRule.style.getPropertyValue(aProperty); + }, + + /** + * Retrieve the style property priority from the current CSSStyleRule. + * + * @param {string} aProperty the CSS property name for which you want the + * priority. + * @return {string} the property priority. + */ + getPropertyPriority: function(aProperty) + { + return this._domRule.style.getPropertyPriority(aProperty); + }, + + /** + * Retrieve the list of CssSelector objects for each of the parsed selectors + * of the current CSSStyleRule. + * + * @return {array} the array hold the CssSelector objects. + */ + get selectors() + { + if (this._selectors) { + return this._selectors; + } + + // Parse the CSSStyleRule.selectorText string. + this._selectors = []; + + if (!this._domRule.selectorText) { + return this._selectors; + } + + let selector = this._domRule.selectorText.trim(); + if (!selector) { + return this._selectors; + } + + let nesting = 0; + let currentSelector = []; + + // Parse a selector group into selectors. Normally we could just .split(',') + // however Gecko allows -moz-any(a, b, c) as a selector so we ignore commas + // inside brackets. + for (let i = 0, selLen = selector.length; i < selLen; i++) { + let c = selector.charAt(i); + switch (c) { + case ",": + if (nesting == 0 && currentSelector.length > 0) { + let selectorStr = currentSelector.join("").trim(); + if (selectorStr) { + this._selectors.push(new CssSelector(this, selectorStr)); + } + currentSelector = []; + } else { + currentSelector.push(c); + } + break; + + case "(": + nesting++; + currentSelector.push(c); + break; + + case ")": + nesting--; + currentSelector.push(c); + break; + + default: + currentSelector.push(c); + break; + } + } + + // Add the last selector. + if (nesting == 0 && currentSelector.length > 0) { + let selectorStr = currentSelector.join("").trim(); + if (selectorStr) { + this._selectors.push(new CssSelector(this, selectorStr)); + } + } + + return this._selectors; + }, + + toString: function CssRule_toString() + { + return "[CssRule " + this._domRule.selectorText + "]"; + }, +}; + +/** + * The CSS selector class allows us to document the ranking of various CSS + * selectors. + * + * @constructor + * @param {CssRule} aCssRule the CssRule instance from where the selector comes. + * @param {string} aSelector The selector that we wish to investigate. + */ +function CssSelector(aCssRule, aSelector) +{ + this._cssRule = aCssRule; + this.text = aSelector; + this.elementStyle = this.text == "@element.style"; +} + +CssSelector.prototype = { + /** + * Retrieve the CssSelector source, which is the source of the CssSheet owning + * the selector. + * + * @return {string} the selector source. + */ + get source() + { + return this._cssRule.source; + }, + + /** + * Retrieve the CssSelector source element, which is the source of the CssRule + * owning the selector. This is only available when the CssSelector comes from + * an element.style. + * + * @return {string} the source element selector. + */ + get sourceElement() + { + return this._cssRule.sourceElement; + }, + + /** + * Retrieve the address of the CssSelector. This points to the address of the + * CssSheet owning this selector. + * + * @return {string} the address of the CssSelector. + */ + get href() + { + return this._cssRule.href; + }, + + /** + * Check if the selector comes from a browser-provided stylesheet. + * + * @return {boolean} true if the selector comes from a browser-provided + * stylesheet, or false otherwise. + */ + get systemRule() + { + return this._cssRule.systemRule; + }, + + /** + * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter. + * + * @return {boolean} true if the parent stylesheet is allowed by the current + * sourceFilter, or false otherwise. + */ + get sheetAllowed() + { + return this._cssRule.sheetAllowed; + }, + + /** + * Retrieve the parent stylesheet index/position in the viewed document. + * + * @return {number} the parent stylesheet index/position in the viewed + * document. + */ + get sheetIndex() + { + return this._cssRule.sheetIndex; + }, + + /** + * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet. + * + * @return {number} the line of the parent CSSStyleRule in the parent + * stylesheet. + */ + get ruleLine() + { + return this._cssRule.line; + }, + + toString: function CssSelector_toString() + { + return this.text; + }, +}; + +/** + * A cache of information about the matched rules, selectors and values attached + * to a CSS property, for the highlighted element. + * + * The heart of the CssPropertyInfo object is the _findMatchedSelectors() and + * _findUnmatchedSelectors() methods. These are invoked when the PropertyView + * tries to access the .matchedSelectors and .unmatchedSelectors arrays. + * Results are cached, for later reuse. + * + * @param {CssLogic} aCssLogic Reference to the parent CssLogic instance + * @param {string} aProperty The CSS property we are gathering information for + * @constructor + */ +function CssPropertyInfo(aCssLogic, aProperty) +{ + this._cssLogic = aCssLogic; + this.property = aProperty; + this._value = ""; + + // The number of matched rules holding the this.property style property. + // Additionally, only rules that come from allowed stylesheets are counted. + this._matchedRuleCount = 0; + + // An array holding CssSelectorInfo objects for each of the matched selectors + // that are inside a CSS rule. Only rules that hold the this.property are + // counted. This includes rules that come from filtered stylesheets (those + // that have sheetAllowed = false). + this._matchedSelectors = null; +} + +CssPropertyInfo.prototype = { + /** + * Retrieve the computed style value for the current property, for the + * highlighted element. + * + * @return {string} the computed style value for the current property, for the + * highlighted element. + */ + get value() + { + if (!this._value && this._cssLogic._computedStyle) { + try { + this._value = this._cssLogic._computedStyle.getPropertyValue(this.property); + } catch (ex) { + Services.console.logStringMessage('Error reading computed style for ' + + this.property); + Services.console.logStringMessage(ex); + } + } + + return this._value; + }, + + /** + * Retrieve the number of matched rules holding the this.property style + * property. Only rules that come from allowed stylesheets are counted. + * + * @return {number} the number of matched rules. + */ + get matchedRuleCount() + { + if (!this._matchedSelectors) { + this._findMatchedSelectors(); + } else if (this.needRefilter) { + this._refilterSelectors(); + } + + return this._matchedRuleCount; + }, + + /** + * Retrieve the number of unmatched rules. + * + * @return {number} the number of rules that do not match the highlighted + * element or its parents. + */ + get unmatchedRuleCount() + { + if (!this._unmatchedSelectors) { + this._findUnmatchedSelectors(); + } else if (this.needRefilter) { + this._refilterSelectors(); + } + + return this._unmatchedRuleCount; + }, + + /** + * Retrieve the array holding CssSelectorInfo objects for each of the matched + * selectors, from each of the matched rules. Only selectors coming from + * allowed stylesheets are included in the array. + * + * @return {array} the list of CssSelectorInfo objects of selectors that match + * the highlighted element and its parents. + */ + get matchedSelectors() + { + if (!this._matchedSelectors) { + this._findMatchedSelectors(); + } else if (this.needRefilter) { + this._refilterSelectors(); + } + + return this._matchedSelectors; + }, + + /** + * Retrieve the array holding CssSelectorInfo objects for each of the + * unmatched selectors, from each of the unmatched rules. Only selectors + * coming from allowed stylesheets are included in the array. + * + * @return {array} the list of CssSelectorInfo objects of selectors that do + * not match the highlighted element or its parents. + */ + get unmatchedSelectors() + { + if (!this._unmatchedSelectors) { + this._findUnmatchedSelectors(); + } else if (this.needRefilter) { + this._refilterSelectors(); + } + + return this._unmatchedSelectors; + }, + + /** + * Find the selectors that match the highlighted element and its parents. + * Uses CssLogic.processMatchedSelectors() to find the matched selectors, + * passing in a reference to CssPropertyInfo._processMatchedSelector() to + * create CssSelectorInfo objects, which we then sort + * @private + */ + _findMatchedSelectors: function CssPropertyInfo_findMatchedSelectors() + { + this._matchedSelectors = []; + this._matchedRuleCount = 0; + this.needRefilter = false; + + this._cssLogic.processMatchedSelectors(this._processMatchedSelector, this); + + // Sort the selectors by how well they match the given element. + this._matchedSelectors.sort(function(aSelectorInfo1, aSelectorInfo2) { + if (aSelectorInfo1.status > aSelectorInfo2.status) { + return -1; + } else if (aSelectorInfo2.status > aSelectorInfo1.status) { + return 1; + } else { + return aSelectorInfo1.compareTo(aSelectorInfo2); + } + }); + + // Now we know which of the matches is best, we can mark it BEST_MATCH. + if (this._matchedSelectors.length > 0 && + this._matchedSelectors[0].status > CssLogic.STATUS.UNMATCHED) { + this._matchedSelectors[0].status = CssLogic.STATUS.BEST; + } + }, + + /** + * Process a matched CssSelector object. + * + * @private + * @param {CssSelector} aSelector the matched CssSelector object. + * @param {CssLogic.STATUS} aStatus the CssSelector match status. + */ + _processMatchedSelector: function CssPropertyInfo_processMatchedSelector(aSelector, aStatus) + { + let cssRule = aSelector._cssRule; + let value = cssRule.getPropertyValue(this.property); + if (value) { + let selectorInfo = new CssSelectorInfo(aSelector, this.property, value, + aStatus); + this._matchedSelectors.push(selectorInfo); + if (this._cssLogic._passId !== cssRule._passId && cssRule.sheetAllowed) { + this._matchedRuleCount++; + } + } + }, + + /** + * Find the selectors that do not match the highlighted element and its + * parents. + * @private + */ + _findUnmatchedSelectors: function CssPropertyInfo_findUnmatchedSelectors() + { + this._unmatchedSelectors = []; + this._unmatchedRuleCount = 0; + this.needRefilter = false; + this._cssLogic._passId++; + + this._cssLogic.processUnmatchedSelectors(this._processUnmatchedSelector, + this); + + // Sort the selectors by specificity. + this._unmatchedSelectors.sort(function(aSelectorInfo1, aSelectorInfo2) { + return aSelectorInfo1.compareTo(aSelectorInfo2); + }); + }, + + /** + * Process an unmatched CssSelector object. + * + * @private + * @param {CssSelector} aSelector the unmatched CssSelector object. + */ + _processUnmatchedSelector: function CPI_processUnmatchedSelector(aSelector) + { + let cssRule = aSelector._cssRule; + if (cssRule.systemRule) { + return; + } + + let value = cssRule.getPropertyValue(this.property); + if (value) { + let selectorInfo = new CssSelectorInfo(aSelector, this.property, value, + CssLogic.STATUS.UNMATCHED); + this._unmatchedSelectors.push(selectorInfo); + if (this._cssLogic._passId != cssRule._passId) { + if (cssRule.sheetAllowed) { + this._unmatchedRuleCount++; + } + cssRule._passId = this._cssLogic._passId; + } + } + }, + + /** + * Refilter the matched and unmatched selectors arrays when the + * CssLogic.sourceFilter changes. This allows for quick filter changes. + * @private + */ + _refilterSelectors: function CssPropertyInfo_refilterSelectors() + { + let passId = ++this._cssLogic._passId; + let ruleCount = 0; + + if (this._matchedSelectors) { + this._matchedSelectors.forEach(function(aSelectorInfo) { + let cssRule = aSelectorInfo.selector._cssRule; + if (cssRule._passId != passId) { + if (cssRule.sheetAllowed) { + ruleCount++; + } + cssRule._passId = passId; + } + }); + this._matchedRuleCount = ruleCount; + } + + if (this._unmatchedSelectors) { + ruleCount = 0; + this._unmatchedSelectors.forEach(function(aSelectorInfo) { + let cssRule = aSelectorInfo.selector._cssRule; + if (!cssRule.systemRule && cssRule._passId != passId) { + if (cssRule.sheetAllowed) { + ruleCount++; + } + cssRule._passId = passId; + } + }); + this._unmatchedRuleCount = ruleCount; + } + + this.needRefilter = false; + }, + + toString: function CssPropertyInfo_toString() + { + return "CssPropertyInfo[" + this.property + "]"; + }, +}; + +/** + * A class that holds information about a given CssSelector object. + * + * Instances of this class are given to CssHtmlTree in the arrays of matched and + * unmatched selectors. Each such object represents a displayable row in the + * PropertyView objects. The information given by this object blends data coming + * from the CssSheet, CssRule and from the CssSelector that own this object. + * + * @param {CssSelector} aSelector The CssSelector object for which to present information. + * @param {string} aProperty The property for which information should be retrieved. + * @param {string} aValue The property value from the CssRule that owns the selector. + * @param {CssLogic.STATUS} aStatus The selector match status. + * @constructor + */ +function CssSelectorInfo(aSelector, aProperty, aValue, aStatus) +{ + this.selector = aSelector; + this.property = aProperty; + this.value = aValue; + this.status = aStatus; + + let priority = this.selector._cssRule.getPropertyPriority(this.property); + this.important = (priority === "important"); +} + +CssSelectorInfo.prototype = { + /** + * Retrieve the CssSelector source, which is the source of the CssSheet owning + * the selector. + * + * @return {string} the selector source. + */ + get source() + { + return this.selector.source; + }, + + /** + * Retrieve the CssSelector source element, which is the source of the CssRule + * owning the selector. This is only available when the CssSelector comes from + * an element.style. + * + * @return {string} the source element selector. + */ + get sourceElement() + { + return this.selector.sourceElement; + }, + + /** + * Retrieve the address of the CssSelector. This points to the address of the + * CssSheet owning this selector. + * + * @return {string} the address of the CssSelector. + */ + get href() + { + return this.selector.href; + }, + + /** + * Check if the CssSelector comes from element.style or not. + * + * @return {boolean} true if the CssSelector comes from element.style, or + * false otherwise. + */ + get elementStyle() + { + return this.selector.elementStyle; + }, + + /** + * Retrieve the parent stylesheet index/position in the viewed document. + * + * @return {number} the parent stylesheet index/position in the viewed + * document. + */ + get sheetIndex() + { + return this.selector.sheetIndex; + }, + + /** + * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter. + * + * @return {boolean} true if the parent stylesheet is allowed by the current + * sourceFilter, or false otherwise. + */ + get sheetAllowed() + { + return this.selector.sheetAllowed; + }, + + /** + * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet. + * + * @return {number} the line of the parent CSSStyleRule in the parent + * stylesheet. + */ + get ruleLine() + { + return this.selector.ruleLine; + }, + + /** + * Check if the selector comes from a browser-provided stylesheet. + * + * @return {boolean} true if the selector comes from a browser-provided + * stylesheet, or false otherwise. + */ + get systemRule() + { + return this.selector.systemRule; + }, + + /** + * Compare the current CssSelectorInfo instance to another instance, based on + * specificity information. + * + * @param {CssSelectorInfo} aThat The instance to compare ourselves against. + * @return number -1, 0, 1 depending on how aThat compares with this. + */ + compareTo: function CssSelectorInfo_compareTo(aThat) + { + if (this.systemRule && !aThat.systemRule) return 1; + if (!this.systemRule && aThat.systemRule) return -1; + + if (this.elementStyle && !aThat.elementStyle) { + if (!this.important && aThat.important) return 1; + else return -1; + } + + if (!this.elementStyle && aThat.elementStyle) { + if (this.important && !aThat.important) return -1; + else return 1; + } + + if (this.important && !aThat.important) return -1; + if (aThat.important && !this.important) return 1; + + if (this.sheetIndex > aThat.sheetIndex) return -1; + if (aThat.sheetIndex > this.sheetIndex) return 1; + + if (this.ruleLine > aThat.ruleLine) return -1; + if (aThat.ruleLine > this.ruleLine) return 1; + + return 0; + }, + + toString: function CssSelectorInfo_toString() + { + return this.selector + " -> " + this.value; + }, +};
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/Makefile.in @@ -0,0 +1,55 @@ +# +# ***** 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 HUDService code. +# +# The Initial Developer of the Original Code is Mozilla Corporation. +# +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mike Ratcliffe <mratcliffe@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 GPL or the LGPL. 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 ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +ifdef ENABLE_TESTS +ifneq (mobile,$(MOZ_BUILD_APP)) + DIRS += test +endif +endif + +include $(topsrcdir)/config/rules.mk + +libs:: + $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/StyleInspector.jsm @@ -0,0 +1,182 @@ +/* -*- Mode: C++; tab-width: 8; 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 the Mozilla Inspector Module. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Mike Ratcliffe <mratcliffe@mozilla.com> + * + * 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 GPL or the LGPL. 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 ***** */ + +const Cu = Components.utils; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var EXPORTED_SYMBOLS = ["StyleInspector"]; + +var StyleInspector = { + /** + * Is the Style Inspector enabled? + * @returns {Boolean} true or false + */ + get isEnabled() + { + return Services.prefs.getBoolPref("devtools.styleinspector.enabled"); + }, + + createPanel: function SI_createPanel() + { + let win = Services.wm.getMostRecentWindow("navigator:browser"); + let popupSet = win.document.getElementById("mainPopupSet"); + let ns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let panel = win.document.createElementNS(ns, "panel"); + + panel.setAttribute("orient", "vertical"); + panel.setAttribute("ignorekeys", "true"); + panel.setAttribute("noautofocus", "true"); + panel.setAttribute("noautohide", "true"); + panel.setAttribute("titlebar", "normal"); + panel.setAttribute("close", "true"); + panel.setAttribute("label", StyleInspector.l10n("panelTitle")); + + // size panel to 200px wide by half browser height - 60. + let contentWindow = win.gBrowser.selectedBrowser.contentWindow; + panel.setAttribute("width", 200); + panel.setAttribute("height", contentWindow.outerHeight / 2 - 60); + + let vbox = win.document.createElement("vbox"); + vbox.setAttribute("flex", "1"); + panel.appendChild(vbox); + + let iframe = win.document.createElementNS(ns, "iframe"); + iframe.setAttribute("flex", "1"); + iframe.setAttribute("tooltip", "aHTMLTooltip"); + iframe.setAttribute("src", "chrome://browser/content/csshtmltree.xhtml"); + vbox.appendChild(iframe); + + let hbox = win.document.createElement("hbox"); + hbox.setAttribute("class", "resizerbox"); + vbox.appendChild(hbox); + + let spacer = win.document.createElement("spacer"); + spacer.setAttribute("flex", "1"); + hbox.appendChild(spacer); + + let resizer = win.document.createElement("resizer"); + resizer.setAttribute("dir", "bottomend"); + hbox.appendChild(resizer); + popupSet.appendChild(panel); + + panel.addEventListener("popupshown", function SI_popup_shown() { + if (!this.cssHtmlTree) { + this.cssLogic = new CssLogic(); + this.cssHtmlTree = new CssHtmlTree(iframe, this.cssLogic, this); + } + + this.cssLogic.highlight(this.selectedNode); + this.cssHtmlTree.highlight(this.selectedNode); + Services.obs.notifyObservers(null, "StyleInspector-opened", null); + }, false); + + panel.addEventListener("popuphidden", function SI_popup_hidden() { + Services.obs.notifyObservers(null, "StyleInspector-closed", null); + }, false); + + /** + * Check if the style inspector is open + */ + panel.isOpen = function SI_isOpen() + { + return this.state && this.state == "open"; + }; + + /** + * Select a node to inspect in the Style Inspector panel + * + * @param aNode The node to inspect + */ + panel.selectNode = function SI_selectNode(aNode) + { + this.selectedNode = aNode; + if (this.isOpen()) { + this.cssLogic.highlight(aNode); + this.cssHtmlTree.highlight(aNode); + } else { + let win = Services.wm.getMostRecentWindow("navigator:browser"); + this.openPopup(win.gBrowser.selectedBrowser, "end_before", 0, 0, false, false); + } + }; + + /** + * Is the Style Inspector initialized? + * @returns {Boolean} true or false + */ + function isInitialized() + { + return panel.cssLogic && panel.cssHtmlTree; + } + + return panel; + }, + + /** + * Memonized lookup of a l10n string from a string bundle. + * @param {string} aName The key to lookup. + * @returns A localized version of the given key. + */ + l10n: function SI_l10n(aName) + { + try { + return _strings.GetStringFromName(aName); + } catch (ex) { + Services.console.logStringMessage("Error reading '" + aName + "'"); + throw new Error("l10n error with " + aName); + } + }, +}; + +XPCOMUtils.defineLazyGetter(this, "_strings", function() Services.strings + .createBundle("chrome://browser/locale/styleinspector.properties")); + +XPCOMUtils.defineLazyGetter(this, "CssLogic", function() { + let tmp = {}; + Cu.import("resource:///modules/devtools/CssLogic.jsm", tmp); + return tmp.CssLogic; +}); + +XPCOMUtils.defineLazyGetter(this, "CssHtmlTree", function() { + let tmp = {}; + Cu.import("resource:///modules/devtools/CssHtmlTree.jsm", tmp); + return tmp.CssHtmlTree; +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/csshtmltree.xhtml @@ -0,0 +1,158 @@ +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % inspectorDTD SYSTEM "chrome://browser/locale/styleinspector.dtd"> + %inspectorDTD; + <!ELEMENT loop ANY> + <!ATTLIST li foreach CDATA #IMPLIED> + <!ATTLIST div foreach CDATA #IMPLIED> + <!ATTLIST loop foreach CDATA #IMPLIED> + <!ATTLIST a target CDATA #IMPLIED> + <!ATTLIST a __pathElement CDATA #IMPLIED> + <!ATTLIST div _id CDATA #IMPLIED> + <!ATTLIST div save CDATA #IMPLIED> + <!ATTLIST table save CDATA #IMPLIED> + <!ATTLIST loop if CDATA #IMPLIED> + <!ATTLIST tr if CDATA #IMPLIED> +]> +<!-- ***** 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 the Mozilla Inspector Module. + - + - The Initial Developer of the Original Code is The Mozilla Foundation. + - Portions created by the Initial Developer are Copyright (C) 2011 + - the Initial Developer. All Rights Reserved. + - + - Contributor(s): + - Joe Walker (jwalker@mozilla.com) (original author) + - Mihai Șucan <mihai.sucan@gmail.com> + - Michael Ratcliffe <mratcliffe@mozilla.com> + - + - 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 ***** --> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<head> + <meta http-equiv="Content-Type" + content="application/xhtml+xml; charset=UTF-8" /> + <link rel="stylesheet" type="text/css" + href="chrome://browser/skin/devtools/csshtmltree.css" /> +</head> +<body role="application"> + +<!-- The output from #templateRoot (below) is inserted here. --> +<div id="root"> +</div> + +<!-- +To visually debug the templates without running firefox, alter the display:none +--> +<div style="display:none;"> + <!-- + templateRoot sits at the top of the window showing what we're looking at. + For data it needs an instance of CssHtmlTree. + --> + <div id="templateRoot"> + <p class="path"> + <label dir="${getRTLAttr}">&lookingAtLabel;</label> + <ol> + <li foreach="item in ${pathElements}" dir="${getRTLAttr}"> + <a href="#" onclick="${pathClick}" __pathElement="${item.element}"> + ${__element.pathElement = item.element; item.display} + </a> + </li> + </ol> + </p> + + <div _id="groups"> + <div foreach="group in ${styleGroups}" class="group" save="${group.element}" dir="${getRTLAttr}"> + <h1 onclick="${group.click}" dir="${getRTLAttr}"> + ${group.localName} + <div class="groupexpander"></div> + </h1> + <div save="${group.properties}"></div> + </div> + </div> + </div> + + <!-- + A templateProperties sits inside each templateGroups to show the properties + themselves. Each needs data like this: + { + property: [ ..., ] // Array of PropertyViews from CssHtmlTree.jsm + } + --> + <div id="templateProperties"> + <div foreach="property in ${propertyViews}" class="property-view" save="${property.element}" dir="${getRTLAttr}"> + <div class="property-header" onclick="${property.click}"> + <span class="property-name" dir="${getRTLAttr}"> + <a class="link" target="_blank" title="&helpLinkTitle;" + href="${property.link}">${property.name}</a> + </span> + <span class="property-value" dir="ltr">${property.value}</span> + <span class="link" dir="${getRTLAttr}"> + ${property.ruleTitle(__element)}<div class="expander"></div> + </span> + </div> + <table class="rules" save="${property.rules}" dir="${getRTLAttr}"></table> + </div> + </div> + + <!-- + A templateRules sits inside each templateProperties showing the list of rules + that affect that property. Each needs data like this: + { + selectors: ..., // from cssLogic.getPropertyInfo(x).[un]matchedSelectors + statusText: function(status) {}, // convert rule.status to readable text + showUnmatchedRules: true / false, // show a "more unmatched rules" link + showUnmatchedRulesClick: function() {}, // click event handler for the + "show more unmatched rules" + } + This is a template so the parent does not need to be a table, except that + using a div as the parent causes the DOM to muck with the tr elements + --> + <table id="templateRules"> + <loop foreach="selector in ${selectorViews}" if="${selector.selectorInfo.sheetAllowed}"> + <tr> + <td dir="ltr" class="rule-text ${selector.statusClass}"> + ${selector.humanReadableText(__element)} + </td> + <td class="rule-link"> + <a target="_blank" href="view-source:${selector.selectorInfo.href}" class="link" + title="${selector.selectorInfo.href}">${selector.selectorInfo.source}</a> + </td> + </tr> + </loop> + <tr if="${showUnmatchedLink}"> + <td colspan="4"> + <a href="#" onclick="${showUnmatchedLinkClick}" + class="link">${showUnmatchedLinkText}</a> + </td> + </tr> + </table> +</div> + +</body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/Makefile.in @@ -0,0 +1,48 @@ +# +# ***** 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 Style Inspector code. +# +# The Initial Developer of the Original Code is +# Mozilla Corporation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mike Ratcliffe <mratcliffe@mozilla.com> (Original author) +# +# Alternatively, the contents of this file may be used under the terms of +# either of 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 GPL or the LGPL. 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 ***** + +DEPTH = ../../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +DIRS = browser + +include $(topsrcdir)/config/rules.mk
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser/Makefile.in @@ -0,0 +1,62 @@ +# +# ***** 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 Style Inspector code. +# +# The Initial Developer of the Original Code is +# Mozilla Corporation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mike Ratcliffe <mratcliffe@mozilla.com> (Original author) +# +# Alternatively, the contents of this file may be used under the terms of +# either of 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 GPL or the LGPL. 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 ***** + +DEPTH = ../../../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = browser/devtools/styleinspector/test/browser + +include $(DEPTH)/config/autoconf.mk +include $(topsrcdir)/config/rules.mk + +_BROWSER_TEST_FILES = \ + browser_styleinspector.js \ + browser_styleinspector_webconsole.js \ + head.js \ + $(NULL) + +_BROWSER_TEST_PAGES = \ + browser_styleinspector_webconsole.htm \ + $(NULL) + +libs:: $(_BROWSER_TEST_FILES) + $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir) + +libs:: $(_BROWSER_TEST_PAGES) + $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser/browser_styleinspector.js @@ -0,0 +1,145 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the style inspector works properly + +let doc; +let stylePanel; + +function createDocument() +{ + doc.body.innerHTML = '<style type="text/css"> ' + + 'span { font-variant: small-caps; color: #000000; } ' + + '.nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; ' + + 'font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">\n' + + '<h1>Some header text</h1>\n' + + '<p id="salutation" style="font-size: 12pt">hi.</p>\n' + + '<p id="body" style="font-size: 12pt">I am a test-case. This text exists ' + + 'solely to provide some things to <span style="color: yellow">' + + 'highlight</span> and <span style="font-weight: bold">count</span> ' + + 'style list-items in the box at right. If you are reading this, ' + + 'you should go do something else instead. Maybe read a book. Or better ' + + 'yet, write some test-cases for another bit of code. ' + + '<span style="font-style: italic">Maybe more inspector test-cases!</span></p>\n' + + '<p id="closing">end transmission</p>\n' + + '<p>Inspect using inspectstyle(document.querySelectorAll("span")[0])</p>' + + '</div>'; + doc.title = "Style Inspector Test"; + ok(window.StyleInspector, "StyleInspector exists"); + ok(StyleInspector.isEnabled, "style inspector preference is enabled"); + stylePanel = StyleInspector.createPanel(); + Services.obs.addObserver(runStyleInspectorTests, "StyleInspector-opened", false); + stylePanel.openPopup(gBrowser.selectedBrowser, "end_before", 0, 0, false, false); +} + +function runStyleInspectorTests() +{ + Services.obs.removeObserver(runStyleInspectorTests, "StyleInspector-opened", false); + + ok(stylePanel.isOpen(), "style inspector is open"); + + checkForNewProperties(); + + var spans = doc.querySelectorAll("span"); + ok(spans, "captain, we have the spans"); + + let htmlTree = stylePanel.cssHtmlTree; + + for (var i = 0, numSpans = spans.length; i < numSpans; i++) { + stylePanel.selectNode(spans[i]); + + is(spans[i], htmlTree.viewedElement, + "style inspector node matches the selected node"); + is(htmlTree.viewedElement, stylePanel.cssLogic.viewedElement, + "cssLogic node matches the cssHtmlTree node"); + + // The Fonts and Color group. + ok(groupRuleCount(0) > 0, "we have rules for the current span"); + } + + SI_CheckProperty(); + Services.obs.addObserver(finishUp, "StyleInspector-closed", false); + stylePanel.hidePopup(); +} + +function checkForNewProperties() +{ + let htmlTree = stylePanel.cssHtmlTree; + htmlTree.createStyleGroupViews(); + let otherProps = htmlTree._getPropertiesByGroup().other; + let otherPlusUnknownProps = htmlTree.propertiesByGroup.other; + + let missingProps = []; + for each (let prop in otherPlusUnknownProps) { + if (otherProps.indexOf(prop) == -1) { + missingProps.push(prop); + } + } + + if (missingProps.length > 0) { + let n = 1; + let msg = "The following css properties need to be categorized in " + + "CssHtmlTree.getPropertiesByGroup():\r\n"; + missingProps.forEach(function BSI_buildMissingProps(aProp) { + msg += " " + (n++) + ". " + aProp + "\n"; + }); + ok(false, msg); + } +} + +function SI_CheckProperty() +{ + let group = stylePanel.cssHtmlTree.styleGroups[0]; + let cssLogic = stylePanel.cssLogic; + + let propertyInfo = cssLogic.getPropertyInfo("color"); + ok(propertyInfo.matchedRuleCount > 0, "color property has matching rules"); + ok(propertyInfo.unmatchedRuleCount > 0, "color property has unmatched rules"); +} + +function groupRuleCount(groupId) +{ + let groupRules = 0; + let group = stylePanel.cssHtmlTree.styleGroups[groupId]; + + ok(group, "we have a StyleGroupView"); + ok(group.tree, "we have the CssHtmlTree object"); + + let cssLogic = stylePanel.cssLogic; + + ok(cssLogic, "we have the CssLogic object"); + + // we use the click method to populate the groups properties + group.click(); + + ok(group.properties.childElementCount > 0, "the StyleGroupView has properties"); + + group.propertyViews.forEach(function(property) { + groupRules += cssLogic.getPropertyInfo(property.name).matchedRuleCount; + }); + + return groupRules; +} + +function finishUp() +{ + Services.obs.removeObserver(finishUp, "StyleInspector-closed", false); + ok(!stylePanel.isOpen(), "style inspector is closed"); + doc = stylePanel = null; + gBrowser.removeCurrentTab(); + finish(); +} + +function test() +{ + waitForExplicitFinish(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function(evt) { + gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true); + doc = content.document; + waitForFocus(createDocument, content); + }, true); + + content.location = "data:text/html,basic style inspector tests"; +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser/browser_styleinspector_webconsole.htm @@ -0,0 +1,185 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <title>Style inspector test</title> + <style> + .text { + font-family: sans-serif; + } + + .container > .text { + font-family: serif; + } + + .text2 { + font-family: sans-serif; + } + + .text3 { + font-size: 100px; + } + + .text[dir=rtl] { + font-family: monospace; + } + + .container .text { + font-family: fantasy; + } + + #container .text { + font-family: fantasy; + } + + .container > .text { + font-family: cursive; + } + + #container > .text { + font-family: cursive; + } + + #container > .dummy, #container > .dummy2 { + font-family: cursive; + } + + div { + font-family: fantasy; + } + + #container { + font-family: fantasy; + } + + #container > span { + font-family: cursive; + } + + #container .dummy { + font-family: fantasy; + } + + html + .dummy { + font-family: fantasy; + } + + span + span { + font-family: fantasy; + } + + span[id=text] { + font-family: cursive; + } + + span[att=glue] { + font-family: cursive; + } + + span::before { + font-family: cursive; + content: "START "; + } + + span::after { + font-family: cursive; + content: " END"; + } + + spawn::before { + font-family: cursive; + content: "START "; + } + + spawn::after { + font-family: cursive; + content: " END"; + } + + a:link { + font-family: sans-serif; + } + + .link:link { + font-family: fantasy; + } + + a:visited { + font-family: sans-serif; + } + + .link:visited { + font-family: fantasy; + } + + a:active { + font-family: sans-serif; + } + + .link:active { + font-family: fantasy; + } + + a:hover { + font-family: sans-serif; + } + + .link:hover { + font-family: fantasy; + } + + a:focus { + font-family: sans-serif; + outline: 5px solid #0f0; + } + + .link:focus { + font-family: fantasy; + } + + span::first-letter { + font-family: sans-serif; + } + + .text::first-letter { + font-family: fantasy; + } + + span::first-line { + font-family: sans-serif; + } + + .text::first-line { + font-family: fantasy; + } + + #container:first-child { + font-family: sans-serif; + } + + div:first-child { + font-family: fantasy; + } + + span:lang(en) { + font-family: sans-serif; + } + + span:lang(it) { + font-family: fantasy; + } + + html::selection { + background-color: #f00; + font-family: fantasy; + } + </style> +</head> +<body> + <h2>font-size</h2> + <div id="container"> + <span id="text" lang="en" class="text">Use inspectstyle($('text')) to inspect me</span><br /> + <span id="text2" class="text2">Use inspectstyle($('text2'))</span><br /> + <a class="link" href="#">Some Link</a> + <h2>font-family has a single unmatched rule</h2> + </div> +</body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser/browser_styleinspector_webconsole.js @@ -0,0 +1,194 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** 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 DevTools test code. + * + * 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): + * Michael Ratcliffe <mratcliffe@mozilla.com> + * + * 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 GPL or the LGPL. 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 ***** */ + +// Tests that inspectstyle(node) works properly + +const TEST_URI = "http://example.com/browser/browser/devtools/styleinspector/test/browser/browser_styleinspector_webconsole.htm"; + +let doc; +let jsterm; +let hudBox; +let stylePanels = []; + +function test() { + addTab(TEST_URI); + browser.addEventListener("DOMContentLoaded", prepConsole, false); +} + +function prepConsole() { + browser.removeEventListener("DOMContentLoaded", prepConsole, false); + doc = content.document; + openConsole(); + + ok(window.StyleInspector, "StyleInspector exists"); + + let hud = HUDService.getHudByWindow(content); + ok(hud, "we have a console"); + + hudBox = hud.HUDBox; + ok(hudBox, "we have the console display"); + + jsterm = hud.jsterm; + ok(jsterm, "we have a jsterm"); + + openStyleInspector1(); +} + +function openStyleInspector1() { + info("opening style inspector instance 1"); + Services.obs.addObserver(openStyleInspector2, "StyleInspector-opened", false); + jsterm.execute("inspectstyle($('text'))"); +} + +function openStyleInspector2() { + Services.obs.removeObserver(openStyleInspector2, "StyleInspector-opened", false); + info("opening style inspector instance 2"); + Services.obs.addObserver(openStyleInspector3, "StyleInspector-opened", false); + jsterm.execute("inspectstyle($('text2'))"); +} + +function openStyleInspector3() { + Services.obs.removeObserver(openStyleInspector3, "StyleInspector-opened", false); + info("opening style inspector instance 3"); + Services.obs.addObserver(teststylePanels, "StyleInspector-opened", false); + jsterm.execute("inspectstyle($('container'))"); +} + +function teststylePanels() { + Services.obs.removeObserver(teststylePanels, "StyleInspector-opened", false); + + info("adding style inspector instances to stylePanels array"); + let popupSet = document.getElementById("mainPopupSet"); + let len = popupSet.childNodes.length - 3; + stylePanels.push(popupSet.childNodes[len++]); + stylePanels.push(popupSet.childNodes[len++]); + stylePanels.push(popupSet.childNodes[len++]); + + let eltArray = [ + doc.getElementById("text"), + doc.getElementById("text2"), + doc.getElementById("container") + ]; + + info("looping through array to check initialization"); + for (let i = 0, max = stylePanels.length; i < max; i++) { + ok(stylePanels[i], "style inspector instance " + i + + " correctly initialized"); + ok(stylePanels[i].isOpen(), "style inspector " + i + " is open"); + + let htmlTree = stylePanels[i].cssHtmlTree; + + is(eltArray[i], htmlTree.viewedElement, + "style inspector node matches the selected node (id=" + + eltArray[i].id + ")"); + is(htmlTree.viewedElement, stylePanels[i].cssLogic.viewedElement, + "cssLogic node matches the cssHtmlTree node (id=" + eltArray[i].id + ")"); + + ok(groupRuleCount(0, stylePanels[i]) > 0, + "we have rules for the current node (id=" + eltArray[i].id + ")"); + } + + info("hiding stylePanels[1]"); + Services.obs.addObserver(styleInspectorClosedByHide, + "StyleInspector-closed", false); + stylePanels[1].hidePopup(); +} + +function styleInspectorClosedByHide() +{ + Services.obs.removeObserver(styleInspectorClosedByHide, "StyleInspector-closed", false); + ok(stylePanels[0].isOpen(), "instance stylePanels[0] is still open"); + ok(!stylePanels[1].isOpen(), "instance stylePanels[1] is hidden"); + ok(stylePanels[2].isOpen(), "instance stylePanels[2] is still open"); + + info("closing web console"); + Services.obs.addObserver(styleInspectorClosedFromConsole1, + "StyleInspector-closed", false); + closeConsole(); +} + +function styleInspectorClosedFromConsole1() +{ + Services.obs.removeObserver(styleInspectorClosedFromConsole1, + "StyleInspector-closed", false); + info("Style Inspector 1 closed"); + Services.obs.addObserver(styleInspectorClosedFromConsole2, + "StyleInspector-closed", false); +} + +function styleInspectorClosedFromConsole2() +{ + Services.obs.removeObserver(styleInspectorClosedFromConsole2, + "StyleInspector-closed", false); + info("Style Inspector 2 closed"); + executeSoon(cleanUp); +} + +function cleanUp() +{ + let popupSet = document.getElementById("mainPopupSet"); + ok(!popupSet.lastChild.hasAttribute("hudToolId"), + "all style inspector panels are now detached and ready for garbage collection"); + + info("cleaning up"); + doc = hudBox = stylePanels = jsterm = null; + finishTest(); +} + +function groupRuleCount(groupId, aStylePanel) +{ + let groupRules = 0; + let group = aStylePanel.cssHtmlTree.styleGroups[groupId]; + + ok(group, "we have a StyleGroupView"); + ok(group.tree, "we have the CssHtmlTree object"); + + let cssLogic = aStylePanel.cssLogic; + + ok(cssLogic, "we have the CssLogic object"); + + // we use the click event to populate the groups properties + group.click(); + + ok(group.properties.childElementCount > 0, "the StyleGroupView has properties"); + + group.propertyViews.forEach(function(property) { + groupRules += cssLogic.getPropertyInfo(property.name).matchedRuleCount; + }); + + return groupRules; +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser/head.js @@ -0,0 +1,185 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** 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 DevTools test code. + * + * 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): + * Mike Ratcliffe <mratcliffe@mozilla.com> + * + * 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 GPL or the LGPL. 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 ***** */ + +Cu.import("resource:///modules/devtools/StyleInspector.jsm"); +Cu.import("resource://gre/modules/HUDService.jsm"); + +function log(aMsg) +{ + dump("*** WebConsoleTest: " + aMsg + "\n"); +} + +function pprint(aObj) +{ + for (let prop in aObj) { + if (typeof aObj[prop] == "function") { + log("function " + prop); + } + else { + log(prop + ": " + aObj[prop]); + } + } +} + +let tab, browser, hudId, hud, hudBox, filterBox, outputNode, cs; + +function addTab(aURL) +{ + gBrowser.selectedTab = gBrowser.addTab(); + content.location = aURL; + tab = gBrowser.selectedTab; + browser = gBrowser.getBrowserForTab(tab); +} + +function afterAllTabsLoaded(callback, win) { + win = win || window; + + let stillToLoad = 0; + + function onLoad() { + this.removeEventListener("load", onLoad, true); + stillToLoad--; + if (!stillToLoad) + callback(); + } + + for (let a = 0; a < win.gBrowser.tabs.length; a++) { + let browser = win.gBrowser.tabs[a].linkedBrowser; + if (browser.contentDocument.readyState != "complete") { + stillToLoad++; + browser.addEventListener("load", onLoad, true); + } + } + + if (!stillToLoad) + callback(); +} + +/** + * Check if a log entry exists in the HUD output node. + * + * @param {Element} aOutputNode + * the HUD output node. + * @param {string} aMatchString + * the string you want to check if it exists in the output node. + * @param {string} aMsg + * the message describing the test + * @param {boolean} [aOnlyVisible=false] + * find only messages that are visible, not hidden by the filter. + * @param {boolean} [aFailIfFound=false] + * fail the test if the string is found in the output node. + * @param {string} aClass [optional] + * find only messages with the given CSS class. + */ +function testLogEntry(aOutputNode, aMatchString, aMsg, aOnlyVisible, + aFailIfFound, aClass) +{ + let selector = ".hud-msg-node"; + // Skip entries that are hidden by the filter. + if (aOnlyVisible) { + selector += ":not(.hud-filtered-by-type)"; + } + if (aClass) { + selector += "." + aClass; + } + + let msgs = aOutputNode.querySelectorAll(selector); + let found = false; + for (let i = 0, n = msgs.length; i < n; i++) { + let message = msgs[i].textContent.indexOf(aMatchString); + if (message > -1) { + found = true; + break; + } + + // Search the labels too. + let labels = msgs[i].querySelectorAll("label"); + for (let j = 0; j < labels.length; j++) { + if (labels[j].getAttribute("value").indexOf(aMatchString) > -1) { + found = true; + break; + } + } + } + + is(found, !aFailIfFound, aMsg); +} + +/** + * A convenience method to call testLogEntry(). + * + * @param string aString + * The string to find. + */ +function findLogEntry(aString) +{ + testLogEntry(outputNode, aString, "found " + aString); +} + +function openConsole() +{ + HUDService.activateHUDForContext(tab); +} + +function closeConsole() +{ + HUDService.deactivateHUDForContext(tab); +} + +function finishTest() +{ + finish(); +} + +function tearDown() +{ + try { + HUDService.deactivateHUDForContext(gBrowser.selectedTab); + } + catch (ex) { + log(ex); + } + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } + tab = browser = hudId = hud = filterBox = outputNode = cs = null; +} + +registerCleanupFunction(tearDown); + +waitForExplicitFinish(); +
--- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -22,16 +22,17 @@ * * Contributor(s): * David Dahl <ddahl@mozilla.com> (original author) * Rob Campbell <rcampbell@mozilla.com> * Johnathan Nightingale <jnightingale@mozilla.com> * Patrick Walton <pcwalton@mozilla.com> * Julian Viereck <jviereck@mozilla.com> * Mihai Șucan <mihai.sucan@gmail.com> + * Michael Ratcliffe <mratcliffe@mozilla.com> * * 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 @@ -66,16 +67,22 @@ XPCOMUtils.defineLazyServiceGetter(this, XPCOMUtils.defineLazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1", "nsIMIMEService"); XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); +XPCOMUtils.defineLazyGetter(this, "StyleInspector", function () { + var obj = {}; + Cu.import("resource:///modules/devtools/StyleInspector.jsm", obj); + return obj.StyleInspector; +}); + XPCOMUtils.defineLazyGetter(this, "NetUtil", function () { var obj = {}; Cu.import("resource://gre/modules/NetUtil.jsm", obj); return obj.NetUtil; }); XPCOMUtils.defineLazyGetter(this, "PropertyPanel", function () { var obj = {}; @@ -1771,16 +1778,21 @@ HUD_SERVICE.prototype = this.unregisterActiveContext(aHUDId); let popupset = hud.chromeDocument.getElementById("mainPopupSet"); let panels = popupset.querySelectorAll("panel[hudId=" + aHUDId + "]"); for (let i = 0; i < panels.length; i++) { panels[i].hidePopup(); } + panels = popupset.querySelectorAll("panel[hudToolId=" + aHUDId + "]"); + for (let i = 0; i < panels.length; i++) { + panels[i].hidePopup(); + popupset.removeChild(panels[i]); + } let id = ConsoleUtils.supString(aHUDId); Services.obs.notifyObservers(id, "web-console-destroyed", null); if (Object.keys(this.hudReferences).length == 0) { let autocompletePopup = hud.chromeDocument. getElementById("webConsole_autocompletePopup"); if (autocompletePopup) { @@ -4392,16 +4404,47 @@ function JSTermHelper(aJSTerm) aJSTerm.sandbox.inspect = function JSTH_inspect(aObject) { aJSTerm.helperEvaluated = true; let propPanel = aJSTerm.openPropertyPanel(null, unwrap(aObject)); propPanel.panel.setAttribute("hudId", aJSTerm.hudId); }; /** + * Inspects the passed aNode in the style inspector. + * + * @param object aNode + * aNode to inspect. + * @returns void + */ + aJSTerm.sandbox.inspectstyle = function JSTH_inspectstyle(aNode) + { + let errstr = null; + aJSTerm.helperEvaluated = true; + + if (!Services.prefs.getBoolPref("devtools.styleinspector.enabled")) { + errstr = HUDService.getStr("inspectStyle.styleInspectorNotEnabled"); + } else if (!aNode) { + errstr = HUDService.getStr("inspectStyle.nullObjectPassed"); + } else if (!(aNode instanceof Ci.nsIDOMNode)) { + errstr = HUDService.getStr("inspectStyle.mustBeDomNode"); + } else if (!(aNode.style instanceof Ci.nsIDOMCSSStyleDeclaration)) { + errstr = HUDService.getStr("inspectStyle.nodeHasNoStyleProps"); + } + + if (!errstr) { + let stylePanel = StyleInspector.createPanel(); + stylePanel.setAttribute("hudToolId", aJSTerm.hudId); + stylePanel.selectNode(aNode, true); + } else { + aJSTerm.writeOutput(errstr + "\n", CATEGORY_OUTPUT, SEVERITY_ERROR); + } + }; + + /** * Prints aObject to the output. * * @param object aObject * Object to print to the output. * @returns void */ aJSTerm.sandbox.pprint = function JSTH_pprint(aObject) {
new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/styleinspector.dtd @@ -0,0 +1,10 @@ +<!-- LOCALIZATION NOTE (lookingAtLabel): This is the label for the path of + - the highlighted element in the web page. This path is based on the document + - tree. --> +<!ENTITY lookingAtLabel "Looking at:"> + +<!-- LOCALIZATION NOTE (helpLinkTitle): For each style property + - the user can hover it and get a help link button which allows one to + - quickly jump to the documentation from the Mozilla Developer Network site. + - This is the link title shown in the hover tooltip. --> +<!ENTITY helpLinkTitle "Read the documentation for this property">
new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/styleinspector.properties @@ -0,0 +1,53 @@ +# LOCALIZATION NOTE These strings are used inside the Style Inspector. + +# LOCALIZATION NOTE (panelTitle): This is the panel title +panelTitle=Style Inspector + +# LOCALIZATION NOTE (property.numberOfRules): For each style property the panel +# shows the number of rules which hold that specific property, counted from all +# of the stylesheets in the web page inspected. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +property.numberOfRules=#1 rule;#1 rules + +# LOCALIZATION NOTE (property.numberOfUnmatchedRules): Each style property is +# inside a rule. A rule is a selector that can match (or not) the highlighted +# element in the web page. The property view shows no unmatched rules. If the +# user wants to expand the property to view unmatched rules, he/she must click +# this link displayed to the right of each property. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +property.numberOfUnmatchedRules=One unmatched rule;#1 unmatched rules + +# LOCALIZATION NOTE (rule.status): For each style property the panel shows +# the rules which hold that specific property. For every rule, the rule status +# is also displayed: a rule can be the best match, a match, a parent match, or a +# rule did not match the element the user has highlighted. +rule.status.BEST=Best Match +rule.status.MATCHED=Matched +rule.status.PARENT_MATCH=Parent Match +rule.status.UNMATCHED=Unmatched + +# LOCALIZATION NOTE (rule.sourceElement, rule.sourceInline): For each +# style property the panel shows the rules which hold that specific property. +# For every rule, the rule source is also displayed: a rule can come from a +# file, from the same page (inline), or from the element itself (element). +rule.sourceInline=inline +rule.sourceElement=element + +# LOCALIZATION NOTE (rule.showUnmatchedLink): Each style property +# is inside a rule. A rule is a selector that can match (or not) the highlighted +# element in the web page. The property view shows only a few of the unmatched +# rules. If the user wants to see all of the unmatched rules, he/she must click +# the link displayed at the bottom of the rules table. That link shows how many +# rules are not displayed. This is the string used when the link is generated. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +rule.showUnmatchedLink=One unmatched rule...;#1 unmatched rules... + +# LOCALIZATION NOTE (group): Style properties are displayed in categories and +# these are the category names. +group.Text_Fonts_and_Color=Text, Fonts & Color +group.Background=Background +group.Dimensions=Dimensions +group.Positioning_and_Page_Flow=Positioning and Page Flow +group.Borders=Borders +group.Lists=Lists +group.Effects_and_Other=Effects and Other
--- a/browser/locales/jar.mn +++ b/browser/locales/jar.mn @@ -9,16 +9,18 @@ locale/browser/aboutHome.dtd (%chrome/browser/aboutHome.dtd) locale/browser/aboutSessionRestore.dtd (%chrome/browser/aboutSessionRestore.dtd) #ifdef MOZ_SERVICES_SYNC locale/browser/aboutSyncTabs.dtd (%chrome/browser/aboutSyncTabs.dtd) #endif * locale/browser/browser.dtd (%chrome/browser/browser.dtd) locale/browser/baseMenuOverlay.dtd (%chrome/browser/baseMenuOverlay.dtd) locale/browser/browser.properties (%chrome/browser/browser.properties) + locale/browser/styleinspector.properties (%chrome/browser/styleinspector.properties) + locale/browser/styleinspector.dtd (%chrome/browser/styleinspector.dtd) locale/browser/scratchpad.properties (%chrome/browser/scratchpad.properties) locale/browser/scratchpad.dtd (%chrome/browser/scratchpad.dtd) locale/browser/inspector.properties (%chrome/browser/inspector.properties) locale/browser/openLocation.dtd (%chrome/browser/openLocation.dtd) locale/browser/openLocation.properties (%chrome/browser/openLocation.properties) * locale/browser/pageInfo.dtd (%chrome/browser/pageInfo.dtd) locale/browser/pageInfo.properties (%chrome/browser/pageInfo.properties) locale/browser/quitDialog.properties (%chrome/browser/quitDialog.properties)
new file mode 100644 index 0000000000000000000000000000000000000000..95e7217927854ed98dd7e55123709ad8baf126f9 GIT binary patch literal 933 zc$@*H16urvP)<h;3K|Lk000e1NJLTq001xm000OG1^@s6zbkx400001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipe( z2{9a0;%#{V00SjSL_t(I%Z-#zY!qb}#(&SZ1hxkRgceivVA94v7qr+KPFlDbf`^Xo z8e^fxvlk=AizX%*!qKaQieA|8ZKH&EQQCTHw8f-}iRczRxj>1Pa&WiH&O9DwyKNIO ze3?u#Gw=7#^XGj&(X;<F_Boo%ztPhNjb$EaZQu0@_BhZY=ou_i2nHEK3P8K{b9wCi zKkfEfHOA8P*ntaF7t0USek`dRch)`{!nQS}rvU`EEu_52o(qs2LH>ba?<6n?K+pdF z0lxbfJ9aG~O%n7h(pk{sKp&_V*oV~z3~+DtR6A{r0#}lxGS48R0B67VecLBVWjskL z^&ow27y^AQ7?M2%9F#O97$Se33ON(qMH<DPr`UTAdmtyWccXLh;iK%>^CK{c)sJan znbztR@b8@}b?(M|1uOxQgrtLkFr6fo`ENeG@Djk8Lw{@lf3@;vl2q!zdBh{w2o&RM z7j56L*v#6%LX4$p(X;m_U|;98IzQh7u7XdIr3VPx-bD<6*190U;0BGALALKc224^Y zG%*>3ok+tpmTO5;`9;!Gx#LEXv1ORYoCFUdeFQuz$)-CXr0KouQ&VpYBuQmFCnj=Y zA|pFYjhO%z5P$+A00FrPJeh+pECRn`6@e@VND5@241m_1OGwu{5*V1Gv0NjUKkMq& zf!2|-A3O-E0d;k?^(L7ufEY_(05=5ANqV6g%QMx*GJ!M`X&OjVk)}nmtd5zWxpIis z-H1Ys=E?~uJcq+~5OzW#!-b8o=HNaC@5tTLC+j&f1Z<2umv17*V=O%utL4u|>Lar; zmPS%%qqeUqH`w_kcFh3szHMPEbN1ePjsmg}MdUg#!P?&=G*>QnH#YuS2e?G@_FG89 z*fYovZAo8q<uYaoT&20ZMB^4)ls7kl<1v={<HgdK>z_`}JpsVW@04d_EDdL5hizYV z?nKv)&YnLF^HbH=XWxEN9RiuJVNbBO`aG>Wm+pH!Yyhg!ygh;S0)%h3)MV{5;L}^e zZ(qyo6ET+hJEQQQYaI|ZW@0Q214q&<o!e4me~9^DbNK%SaZ!ks&1VtM00000NkvXX Hu0mjfz!{kU
new file mode 100644 --- /dev/null +++ b/browser/themes/gnomestripe/browser/devtools/csshtmltree.css @@ -0,0 +1,236 @@ +/* ***** 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 the Mozilla Inspector Module. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker <jwalker@mozilla.com> (original author) + * Mihai Șucan <mihai.sucan@gmail.com> + * Michael Ratcliffe <mratcliffe@mozilla.com> + * + * 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 GPL or the LGPL. 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 ***** */ + +body { + font-family: Lucida Grande, sans-serif; + font-size: 11px; + background: #EEE; +} + +.path { + font-size: 11px; + word-spacing: -1px; +} +.path ol { + list-style: none; + margin: 0; + padding: 0; +} +.path li { + border-radius: 3px; + padding: 2px 3px; + text-shadow: #FFF 0 1px 0; + font-weight: bold; + font-size: 11px; + background: -moz-linear-gradient(top, #F6F6FF, #E3E3FF); + display: inline-block; +} +.path li:after { + content: " > "; +} +.path li:last-child { + background: -moz-linear-gradient(top, #FFC, #DD8); +} +.path li:last-child:after { + color: red; + content: ""; +} + +#sheetList, #sheetList menuitem { + font-size: 1em; + font-weight: normal; +} + +.sheet_line input { + vertical-align: middle; +} + +.sheet_line label { + cursor: pointer; +} + +#header, #footer { + padding: 5px; +} + +#header label { + font-weight: bold; +} +#sheets { + -moz-margin-end: 10px; + margin-top: 5px; +} +h1 { + font-size: 13px; + padding: 2px 10px; + margin: 0; + background: -moz-linear-gradient(top, #CCC, #AAA); + border-radius: 3px; + text-shadow: #FFF 0 1px 0; + cursor: pointer; +} + +.property-header { + padding: 2px 5px; + background: -moz-linear-gradient(top, #F8F8F8, #E8E8E8); + color: #666; +} + +.property-name, .property-value, .rule-count, .rule-unmatched { + cursor: pointer; +} + +/* Take away these two :visited rules to get a core dumper */ +/* See https://bugzilla.mozilla.org/show_bug.cgi?id=575675#c30 */ +.link { color: #55A; } +.link:visited { color: #55A; } +a.link { text-decoration: none; cursor: pointer } +a.link:visited { text-decoration: none; } +.rule-count, .rule-unmatched { + float: right; +} +.rule-count[dir="rtl"], .rule-unmatched[dir="rtl"] { + float: left; +} +.property-view .rule-count .expander, +.property-view .rule-unmatched .expander { + width: 8px; + height: 8px; + -moz-margin-start: 5px; + display: inline-block; + background: url("chrome://browser/skin/devtools/arrows.png"); +} + +.property-view .rule-count .expander, +.property-view .rule-unmatched .expander { + background-position: 24px 0; +} +.property-view[dir="rtl"] .rule-count .expander, +.property-view[dir="rtl"] .rule-unmatched .expander { + background-position: 16px 0; +} +.property-view[open] .rule-count .expander, +.property-view[open] .rule-unmatched .expander { + background-position: 8px 0; +} + +.property-name { + font-size: 12px; + font-weight: bold; + -moz-padding-end: 4px; + color: #000; +} +span.property-value { + -moz-padding-end: 5px; + font-size: 10px; +} +.group { + margin-top: 10px; +} + +.group > h1 { + color: #333; + font-size: 11px; +} +.group .groupexpander { + width: 8px; + height: 8px; + margin-top: 2px; + background: url("chrome://browser/skin/devtools/arrows.png"); +} +.group > div { + display: none; +} +.group[open] > div { + display: block; +} +.group .groupexpander { + background-position: 48px 0; + float: right; +} +.group[dir="rtl"] .groupexpander { + background-position: 40px 0; + float: left; +} +.group[open] .groupexpander { + background-position: 32px 0; + float: right; +} +.group[open][dir="rtl"] .groupexpander { + background-position: 32px 0; + float: left; +} + +.group, #header, #footer { + background: #FFF; + border: 1px solid #E1E1E1; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + border-radius: 4px 4px 4px 4px; +} + +.property-view > .rules { + display: none; +} +.property-view[open] > .rules { + display: table; +} +.rules { + max-height: 350px; + overflow-y: auto; +} +.rule-specificty, .rule-status { + white-space: nowrap; +} +.rule-link { + text-align: end; +} + +/* This rule is necessary because Templater.jsm breaks LTR TDs in RTL docs */ +.rule-text { + direction: ltr; +} + +.resizerbox { + background-color: window; +} + +.bestmatch { color: black; } +.matched { text-decoration: line-through; } +.parentmatch { color: #666; } +.unmatched { color: brown; }
--- a/browser/themes/gnomestripe/browser/jar.mn +++ b/browser/themes/gnomestripe/browser/jar.mn @@ -79,16 +79,18 @@ browser.jar: skin/classic/browser/tabbrowser/tab.png (tabbrowser/tab.png) skin/classic/browser/tabbrowser/tab-overflow-border.png (tabbrowser/tab-overflow-border.png) skin/classic/browser/tabbrowser/tabDragIndicator.png (tabbrowser/tabDragIndicator.png) skin/classic/browser/tabview/edit-light.png (tabview/edit-light.png) skin/classic/browser/tabview/search.png (tabview/search.png) skin/classic/browser/tabview/stack-expander.png (tabview/stack-expander.png) skin/classic/browser/tabview/tabview.png (tabview/tabview.png) skin/classic/browser/tabview/tabview.css (tabview/tabview.css) + skin/classic/browser/devtools/arrows.png (devtools/arrows.png) + skin/classic/browser/devtools/csshtmltree.css (devtools/csshtmltree.css) #ifdef MOZ_SERVICES_SYNC skin/classic/browser/sync-16-throbber.png skin/classic/browser/sync-16.png skin/classic/browser/sync-24-throbber.png skin/classic/browser/sync-32.png skin/classic/browser/sync-bg.png skin/classic/browser/sync-desktopIcon.png skin/classic/browser/sync-mobileIcon.png
new file mode 100644 index 0000000000000000000000000000000000000000..95e7217927854ed98dd7e55123709ad8baf126f9 GIT binary patch literal 933 zc$@*H16urvP)<h;3K|Lk000e1NJLTq001xm000OG1^@s6zbkx400001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipe( z2{9a0;%#{V00SjSL_t(I%Z-#zY!qb}#(&SZ1hxkRgceivVA94v7qr+KPFlDbf`^Xo z8e^fxvlk=AizX%*!qKaQieA|8ZKH&EQQCTHw8f-}iRczRxj>1Pa&WiH&O9DwyKNIO ze3?u#Gw=7#^XGj&(X;<F_Boo%ztPhNjb$EaZQu0@_BhZY=ou_i2nHEK3P8K{b9wCi zKkfEfHOA8P*ntaF7t0USek`dRch)`{!nQS}rvU`EEu_52o(qs2LH>ba?<6n?K+pdF z0lxbfJ9aG~O%n7h(pk{sKp&_V*oV~z3~+DtR6A{r0#}lxGS48R0B67VecLBVWjskL z^&ow27y^AQ7?M2%9F#O97$Se33ON(qMH<DPr`UTAdmtyWccXLh;iK%>^CK{c)sJan znbztR@b8@}b?(M|1uOxQgrtLkFr6fo`ENeG@Djk8Lw{@lf3@;vl2q!zdBh{w2o&RM z7j56L*v#6%LX4$p(X;m_U|;98IzQh7u7XdIr3VPx-bD<6*190U;0BGALALKc224^Y zG%*>3ok+tpmTO5;`9;!Gx#LEXv1ORYoCFUdeFQuz$)-CXr0KouQ&VpYBuQmFCnj=Y zA|pFYjhO%z5P$+A00FrPJeh+pECRn`6@e@VND5@241m_1OGwu{5*V1Gv0NjUKkMq& zf!2|-A3O-E0d;k?^(L7ufEY_(05=5ANqV6g%QMx*GJ!M`X&OjVk)}nmtd5zWxpIis z-H1Ys=E?~uJcq+~5OzW#!-b8o=HNaC@5tTLC+j&f1Z<2umv17*V=O%utL4u|>Lar; zmPS%%qqeUqH`w_kcFh3szHMPEbN1ePjsmg}MdUg#!P?&=G*>QnH#YuS2e?G@_FG89 z*fYovZAo8q<uYaoT&20ZMB^4)ls7kl<1v={<HgdK>z_`}JpsVW@04d_EDdL5hizYV z?nKv)&YnLF^HbH=XWxEN9RiuJVNbBO`aG>Wm+pH!Yyhg!ygh;S0)%h3)MV{5;L}^e zZ(qyo6ET+hJEQQQYaI|ZW@0Q214q&<o!e4me~9^DbNK%SaZ!ks&1VtM00000NkvXX Hu0mjfz!{kU
new file mode 100644 --- /dev/null +++ b/browser/themes/pinstripe/browser/devtools/csshtmltree.css @@ -0,0 +1,236 @@ +/* ***** 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 the Mozilla Inspector Module. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker <jwalker@mozilla.com> (original author) + * Mihai Șucan <mihai.sucan@gmail.com> + * Michael Ratcliffe <mratcliffe@mozilla.com> + * + * 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 GPL or the LGPL. 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 ***** */ + +body { + font-family: Lucida Grande, sans-serif; + font-size: 11px; + background: #EEE; +} + +.path { + font-size: 11px; + word-spacing: -1px; +} +.path ol { + list-style: none; + margin: 0; + padding: 0; +} +.path li { + border-radius: 3px; + padding: 2px 3px; + text-shadow: #FFF 0 1px 0; + font-weight: bold; + font-size: 11px; + background: -moz-linear-gradient(top, #F6F6FF, #E3E3FF); + display: inline-block; +} +.path li:after { + content: " > "; +} +.path li:last-child { + background: -moz-linear-gradient(top, #FFC, #DD8); +} +.path li:last-child:after { + color: red; + content: ""; +} + +#sheetList, #sheetList menuitem { + font-size: 1em; + font-weight: normal; +} + +.sheet_line input { + vertical-align: middle; +} + +.sheet_line label { + cursor: pointer; +} + +#header, #footer { + padding: 5px; +} + +#header label { + font-weight: bold; +} +#sheets { + -moz-margin-end: 10px; + margin-top: 5px; +} +h1 { + font-size: 13px; + padding: 2px 10px; + margin: 0; + background: -moz-linear-gradient(top, #CCC, #AAA); + border-radius: 3px; + text-shadow: #FFF 0 1px 0; + cursor: pointer; +} + +.property-header { + padding: 2px 5px; + background: -moz-linear-gradient(top, #F8F8F8, #E8E8E8); + color: #666; +} + +.property-name, .property-value, .rule-count, .rule-unmatched { + cursor: pointer; +} + +/* Take away these two :visited rules to get a core dumper */ +/* See https://bugzilla.mozilla.org/show_bug.cgi?id=575675#c30 */ +.link { color: #55A; } +.link:visited { color: #55A; } +a.link { text-decoration: none; cursor: pointer } +a.link:visited { text-decoration: none; } +.rule-count, .rule-unmatched { + float: right; +} +.rule-count[dir="rtl"], .rule-unmatched[dir="rtl"] { + float: left; +} +.property-view .rule-count .expander, +.property-view .rule-unmatched .expander { + width: 8px; + height: 8px; + -moz-margin-start: 5px; + display: inline-block; + background: url("chrome://browser/skin/devtools/arrows.png"); +} + +.property-view .rule-count .expander, +.property-view .rule-unmatched .expander { + background-position: 24px 0; +} +.property-view[dir="rtl"] .rule-count .expander, +.property-view[dir="rtl"] .rule-unmatched .expander { + background-position: 16px 0; +} +.property-view[open] .rule-count .expander, +.property-view[open] .rule-unmatched .expander { + background-position: 8px 0; +} + +.property-name { + font-size: 12px; + font-weight: bold; + -moz-padding-end: 4px; + color: #000; +} +span.property-value { + -moz-padding-end: 5px; + font-size: 10px; +} +.group { + margin-top: 10px; +} + +.group > h1 { + color: #333; + font-size: 11px; +} +.group .groupexpander { + width: 8px; + height: 8px; + margin-top: 2px; + background: url("chrome://browser/skin/devtools/arrows.png"); +} +.group > div { + display: none; +} +.group[open] > div { + display: block; +} +.group .groupexpander { + background-position: 48px 0; + float: right; +} +.group[dir="rtl"] .groupexpander { + background-position: 40px 0; + float: left; +} +.group[open] .groupexpander { + background-position: 32px 0; + float: right; +} +.group[open][dir="rtl"] .groupexpander { + background-position: 32px 0; + float: left; +} + +.group, #header, #footer { + background: #FFF; + border: 1px solid #E1E1E1; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + border-radius: 4px 4px 4px 4px; +} + +.property-view > .rules { + display: none; +} +.property-view[open] > .rules { + display: table; +} +.rules { + max-height: 350px; + overflow-y: auto; +} +.rule-specificty, .rule-status { + white-space: nowrap; +} +.rule-link { + text-align: end; +} + +/* This rule is necessary because Templater.jsm breaks LTR TDs in RTL docs */ +.rule-text { + direction: ltr; +} + +.resizerbox { + background-color: window; +} + +.bestmatch { color: black; } +.matched { text-decoration: line-through; } +.parentmatch { color: #666; } +.unmatched { color: brown; }
--- a/browser/themes/pinstripe/browser/jar.mn +++ b/browser/themes/pinstripe/browser/jar.mn @@ -118,16 +118,18 @@ browser.jar: skin/classic/browser/tabbrowser/tab-overflow-border.png (tabbrowser/tab-overflow-border.png) skin/classic/browser/tabbrowser/tabDragIndicator.png (tabbrowser/tabDragIndicator.png) skin/classic/browser/tabview/close.png (tabview/close.png) skin/classic/browser/tabview/edit-light.png (tabview/edit-light.png) skin/classic/browser/tabview/search.png (tabview/search.png) skin/classic/browser/tabview/stack-expander.png (tabview/stack-expander.png) skin/classic/browser/tabview/tabview.png (tabview/tabview.png) skin/classic/browser/tabview/tabview.css (tabview/tabview.css) + skin/classic/browser/devtools/arrows.png (devtools/arrows.png) + skin/classic/browser/devtools/csshtmltree.css (devtools/csshtmltree.css) #ifdef MOZ_SERVICES_SYNC skin/classic/browser/sync-throbber.png skin/classic/browser/sync-16.png skin/classic/browser/sync-32.png skin/classic/browser/sync-bg.png skin/classic/browser/sync-desktopIcon.png skin/classic/browser/sync-mobileIcon.png skin/classic/browser/sync-notification-24.png
new file mode 100644 index 0000000000000000000000000000000000000000..95e7217927854ed98dd7e55123709ad8baf126f9 GIT binary patch literal 933 zc$@*H16urvP)<h;3K|Lk000e1NJLTq001xm000OG1^@s6zbkx400001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipe( z2{9a0;%#{V00SjSL_t(I%Z-#zY!qb}#(&SZ1hxkRgceivVA94v7qr+KPFlDbf`^Xo z8e^fxvlk=AizX%*!qKaQieA|8ZKH&EQQCTHw8f-}iRczRxj>1Pa&WiH&O9DwyKNIO ze3?u#Gw=7#^XGj&(X;<F_Boo%ztPhNjb$EaZQu0@_BhZY=ou_i2nHEK3P8K{b9wCi zKkfEfHOA8P*ntaF7t0USek`dRch)`{!nQS}rvU`EEu_52o(qs2LH>ba?<6n?K+pdF z0lxbfJ9aG~O%n7h(pk{sKp&_V*oV~z3~+DtR6A{r0#}lxGS48R0B67VecLBVWjskL z^&ow27y^AQ7?M2%9F#O97$Se33ON(qMH<DPr`UTAdmtyWccXLh;iK%>^CK{c)sJan znbztR@b8@}b?(M|1uOxQgrtLkFr6fo`ENeG@Djk8Lw{@lf3@;vl2q!zdBh{w2o&RM z7j56L*v#6%LX4$p(X;m_U|;98IzQh7u7XdIr3VPx-bD<6*190U;0BGALALKc224^Y zG%*>3ok+tpmTO5;`9;!Gx#LEXv1ORYoCFUdeFQuz$)-CXr0KouQ&VpYBuQmFCnj=Y zA|pFYjhO%z5P$+A00FrPJeh+pECRn`6@e@VND5@241m_1OGwu{5*V1Gv0NjUKkMq& zf!2|-A3O-E0d;k?^(L7ufEY_(05=5ANqV6g%QMx*GJ!M`X&OjVk)}nmtd5zWxpIis z-H1Ys=E?~uJcq+~5OzW#!-b8o=HNaC@5tTLC+j&f1Z<2umv17*V=O%utL4u|>Lar; zmPS%%qqeUqH`w_kcFh3szHMPEbN1ePjsmg}MdUg#!P?&=G*>QnH#YuS2e?G@_FG89 z*fYovZAo8q<uYaoT&20ZMB^4)ls7kl<1v={<HgdK>z_`}JpsVW@04d_EDdL5hizYV z?nKv)&YnLF^HbH=XWxEN9RiuJVNbBO`aG>Wm+pH!Yyhg!ygh;S0)%h3)MV{5;L}^e zZ(qyo6ET+hJEQQQYaI|ZW@0Q214q&<o!e4me~9^DbNK%SaZ!ks&1VtM00000NkvXX Hu0mjfz!{kU
new file mode 100644 --- /dev/null +++ b/browser/themes/winstripe/browser/devtools/csshtmltree.css @@ -0,0 +1,236 @@ +/* ***** 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 the Mozilla Inspector Module. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker <jwalker@mozilla.com> (original author) + * Mihai Șucan <mihai.sucan@gmail.com> + * Michael Ratcliffe <mratcliffe@mozilla.com> + * + * 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 GPL or the LGPL. 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 ***** */ + +body { + font-family: Lucida Grande, sans-serif; + font-size: 11px; + background: #EEE; +} + +.path { + font-size: 11px; + word-spacing: -1px; +} +.path ol { + list-style: none; + margin: 0; + padding: 0; +} +.path li { + border-radius: 3px; + padding: 2px 3px; + text-shadow: #FFF 0 1px 0; + font-weight: bold; + font-size: 11px; + background: -moz-linear-gradient(top, #F6F6FF, #E3E3FF); + display: inline-block; +} +.path li:after { + content: " > "; +} +.path li:last-child { + background: -moz-linear-gradient(top, #FFC, #DD8); +} +.path li:last-child:after { + color: red; + content: ""; +} + +#sheetList, #sheetList menuitem { + font-size: 1em; + font-weight: normal; +} + +.sheet_line input { + vertical-align: middle; +} + +.sheet_line label { + cursor: pointer; +} + +#header, #footer { + padding: 5px; +} + +#header label { + font-weight: bold; +} +#sheets { + -moz-margin-end: 10px; + margin-top: 5px; +} +h1 { + font-size: 13px; + padding: 2px 10px; + margin: 0; + background: -moz-linear-gradient(top, #CCC, #AAA); + border-radius: 3px; + text-shadow: #FFF 0 1px 0; + cursor: pointer; +} + +.property-header { + padding: 2px 5px; + background: -moz-linear-gradient(top, #F8F8F8, #E8E8E8); + color: #666; +} + +.property-name, .property-value, .rule-count, .rule-unmatched { + cursor: pointer; +} + +/* Take away these two :visited rules to get a core dumper */ +/* See https://bugzilla.mozilla.org/show_bug.cgi?id=575675#c30 */ +.link { color: #55A; } +.link:visited { color: #55A; } +a.link { text-decoration: none; cursor: pointer } +a.link:visited { text-decoration: none; } +.rule-count, .rule-unmatched { + float: right; +} +.rule-count[dir="rtl"], .rule-unmatched[dir="rtl"] { + float: left; +} +.property-view .rule-count .expander, +.property-view .rule-unmatched .expander { + width: 8px; + height: 8px; + -moz-margin-start: 5px; + display: inline-block; + background: url("chrome://browser/skin/devtools/arrows.png"); +} + +.property-view .rule-count .expander, +.property-view .rule-unmatched .expander { + background-position: 24px 0; +} +.property-view[dir="rtl"] .rule-count .expander, +.property-view[dir="rtl"] .rule-unmatched .expander { + background-position: 16px 0; +} +.property-view[open] .rule-count .expander, +.property-view[open] .rule-unmatched .expander { + background-position: 8px 0; +} + +.property-name { + font-size: 12px; + font-weight: bold; + -moz-padding-end: 4px; + color: #000; +} +span.property-value { + -moz-padding-end: 5px; + font-size: 10px; +} +.group { + margin-top: 10px; +} + +.group > h1 { + color: #333; + font-size: 11px; +} +.group .groupexpander { + width: 8px; + height: 8px; + margin-top: 2px; + background: url("chrome://browser/skin/devtools/arrows.png"); +} +.group > div { + display: none; +} +.group[open] > div { + display: block; +} +.group .groupexpander { + background-position: 48px 0; + float: right; +} +.group[dir="rtl"] .groupexpander { + background-position: 40px 0; + float: left; +} +.group[open] .groupexpander { + background-position: 32px 0; + float: right; +} +.group[open][dir="rtl"] .groupexpander { + background-position: 32px 0; + float: left; +} + +.group, #header, #footer { + background: #FFF; + border: 1px solid #E1E1E1; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + border-radius: 4px 4px 4px 4px; +} + +.property-view > .rules { + display: none; +} +.property-view[open] > .rules { + display: table; +} +.rules { + max-height: 350px; + overflow-y: auto; +} +.rule-specificty, .rule-status { + white-space: nowrap; +} +.rule-link { + text-align: end; +} + +/* This rule is necessary because Templater.jsm breaks LTR TDs in RTL docs */ +.rule-text { + direction: ltr; +} + +.resizerbox { + background-color: window; +} + +.bestmatch { color: black; } +.matched { text-decoration: line-through; } +.parentmatch { color: #666; } +.unmatched { color: brown; }
--- a/browser/themes/winstripe/browser/jar.mn +++ b/browser/themes/winstripe/browser/jar.mn @@ -101,16 +101,18 @@ browser.jar: skin/classic/browser/tabview/close.png (tabview/close.png) skin/classic/browser/tabview/edit-light.png (tabview/edit-light.png) skin/classic/browser/tabview/grain.png (tabview/grain.png) skin/classic/browser/tabview/search.png (tabview/search.png) skin/classic/browser/tabview/stack-expander.png (tabview/stack-expander.png) skin/classic/browser/tabview/tabview.png (tabview/tabview.png) skin/classic/browser/tabview/tabview-inverted.png (tabview/tabview-inverted.png) skin/classic/browser/tabview/tabview.css (tabview/tabview.css) + skin/classic/browser/devtools/arrows.png (devtools/arrows.png) + skin/classic/browser/devtools/csshtmltree.css (devtools/csshtmltree.css) #ifdef MOZ_SERVICES_SYNC skin/classic/browser/sync-throbber.png skin/classic/browser/sync-16.png skin/classic/browser/sync-32.png skin/classic/browser/sync-bg.png skin/classic/browser/sync-desktopIcon.png skin/classic/browser/sync-mobileIcon.png skin/classic/browser/sync-notification-24.png
--- a/toolkit/locales/en-US/chrome/global/headsUpDisplay.properties +++ b/toolkit/locales/en-US/chrome/global/headsUpDisplay.properties @@ -110,16 +110,33 @@ NetworkPanel.imageSizeDeltaDurationMS=%Sx%Spx, Δ%Sms # flash data received from the server can't be displayed. # # The %S is replaced by the content type, that can't be displayed, examples are # o application/x-shockwave-flash # o music/crescendo NetworkPanel.responseBodyUnableToDisplay.content=Unable to display responses of type "%S" ConsoleAPIDisabled=The Web Console logging API (console.log, console.info, console.warn, console.error) has been disabled by a script on this page. +# LOCALIZATION NOTE (inspectStyle.nullObjectPassed): +# This message is returned when a null object is passed in to inspectstyle() +inspectStyle.nullObjectPassed=Object is null + +# LOCALIZATION NOTE (inspectStyle.mustBeDomNode): +# This message is returned when a non-DOM node is passed in to inspectstyle() +inspectStyle.mustBeDomNode=Object must be a valid DOM node + +# LOCALIZATION NOTE (inspectStyle.nodeHasNoStyleProps): +# This message is returned when an unstyleable object is passed in to inspectstyle() +inspectStyle.nodeHasNoStyleProps=Object cannot be styled + +# LOCALIZATION NOTE (inspectStyle.styleInspectorNotEnabled): +# This message is returned when devtools.styleinspector.enabled is not set to +# true +inspectStyle.styleInspectorNotEnabled=The style inspector is not enabled. Please set the option devtools.styleinspector.enabled to true in about:config to use this command. + # LOCALIZATION NOTE (webConsolePosition): The label shown for the menu which # allows the user to toggle between the Web Console positioning types. webConsolePosition=Position # LOCALIZATION NOTE (webConsolePositionTooltip): The tooltip shown when the user # hovers the Position button in the Web Console toolbar. webConsolePositionTooltip=Position the Web Console above or below the document