Bug 700194 - Speed up style inspector creation and refresh. r=msucan

This commit is contained in:
Dave Camp 2011-11-07 10:35:40 -08:00
Родитель 04e5c9080f
Коммит 65894629d3
4 изменённых файлов: 183 добавлений и 88 удалений

Просмотреть файл

@ -43,6 +43,9 @@
const Cu = Components.utils; const Cu = Components.utils;
const FILTER_CHANGED_TIMEOUT = 300; const FILTER_CHANGED_TIMEOUT = 300;
const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm"); Cu.import("resource://gre/modules/PluralForm.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -51,6 +54,94 @@ Cu.import("resource:///modules/devtools/Templater.jsm");
var EXPORTED_SYMBOLS = ["CssHtmlTree", "PropertyView"]; var EXPORTED_SYMBOLS = ["CssHtmlTree", "PropertyView"];
/**
* Helper for long-running processes that should yield occasionally to
* the mainloop.
*
* @param {Window} aWin
* Timeouts will be set on this window when appropriate.
* @param {Generator} aGenerator
* Will iterate this generator.
* @param {object} aOptions
* Options for the update process:
* onItem {function} Will be called with the value of each iteration.
* onBatch {function} Will be called after each batch of iterations,
* before yielding to the main loop.
* onDone {function} Will be called when iteration is complete.
* onCancel {function} Will be called if the process is canceled.
* threshold {int} How long to process before yielding, in ms.
*
* @constructor
*/
function UpdateProcess(aWin, aGenerator, aOptions)
{
this.win = aWin;
this.iter = Iterator(aGenerator);
this.onItem = aOptions.onItem || function() {};
this.onBatch = aOptions.onBatch || function () {};
this.onDone = aOptions.onDone || function() {};
this.onCancel = aOptions.onCancel || function() {};
this.threshold = aOptions.threshold || 45;
this.canceled = false;
}
UpdateProcess.prototype = {
/**
* Schedule a new batch on the main loop.
*/
schedule: function UP_schedule()
{
if (this.cancelled) {
return;
}
this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0);
},
/**
* Cancel the running process. onItem will not be called again,
* and onCancel will be called.
*/
cancel: function UP_cancel()
{
if (this._timeout) {
this.win.clearTimeout(this._timeout);
this._timeout = 0;
}
this.canceled = true;
this.onCancel();
},
_timeoutHandler: function UP_timeoutHandler() {
this._timeout = null;
try {
this._runBatch();
this.schedule();
} catch(e) {
if (e instanceof StopIteration) {
this.onBatch();
this.onDone();
return;
}
throw e;
}
},
_runBatch: function Y_runBatch()
{
let time = Date.now();
while(!this.cancelled) {
// Continue until iter.next() throws...
let next = this.iter.next();
this.onItem(next[1]);
if ((Date.now() - time) > this.threshold) {
this.onBatch();
return;
}
}
}
};
/** /**
* CssHtmlTree is a panel that manages the display of a table sorted by style. * 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 * There should be one instance of CssHtmlTree per style display (of which there
@ -78,7 +169,6 @@ function CssHtmlTree(aStyleInspector)
this.templateRoot = this.styleDocument.getElementById("templateRoot"); this.templateRoot = this.styleDocument.getElementById("templateRoot");
this.templatePath = this.styleDocument.getElementById("templatePath"); this.templatePath = this.styleDocument.getElementById("templatePath");
this.propertyContainer = this.styleDocument.getElementById("propertyContainer"); this.propertyContainer = this.styleDocument.getElementById("propertyContainer");
this.templateProperty = this.styleDocument.getElementById("templateProperty");
this.panel = aStyleInspector.panel; this.panel = aStyleInspector.panel;
// No results text. // No results text.
@ -135,6 +225,10 @@ CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate,
XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings
.createBundle("chrome://browser/locale/devtools/styleinspector.properties")); .createBundle("chrome://browser/locale/devtools/styleinspector.properties"));
XPCOMUtils.defineLazyGetter(CssHtmlTree, "HELP_LINK_TITLE", function() {
return CssHtmlTree.HELP_LINK_TITLE = CssHtmlTree.l10n("helpLinkTitle");
});
CssHtmlTree.prototype = { CssHtmlTree.prototype = {
// Cache the list of properties that have matched and unmatched properties. // Cache the list of properties that have matched and unmatched properties.
_matchedProperties: null, _matchedProperties: null,
@ -181,46 +275,38 @@ CssHtmlTree.prototype = {
if (this.htmlComplete) { if (this.htmlComplete) {
this.refreshPanel(); this.refreshPanel();
} else { } else {
if (this._panelRefreshTimeout) { if (this._refreshProcess) {
this.win.clearTimeout(this._panelRefreshTimeout); this._refreshProcess.cancel();
} }
CssHtmlTree.processTemplate(this.templateRoot, this.root, this); CssHtmlTree.processTemplate(this.templateRoot, this.root, this);
// We use a setTimeout loop to display the properties in batches of 15 at a this.numVisibleProperties = 0;
// time. This results in a perceptibly more responsive UI. let fragment = this.doc.createDocumentFragment();
let i = 0; this._refreshProcess = new UpdateProcess(this.win, CssHtmlTree.propertyNames, {
let batchSize = 15; onItem: function(aPropertyName) {
let max = CssHtmlTree.propertyNames.length - 1; // Per-item callback.
function displayProperties() { if (this.viewedElement != aElement || !this.styleInspector.isOpen()) {
if (this.viewedElement == aElement && this.styleInspector.isOpen()) { return false;
// Display the next 15 properties }
for (let step = i + batchSize; i < step && i <= max; i++) { let propView = new PropertyView(this, aPropertyName);
let name = CssHtmlTree.propertyNames[i]; fragment.appendChild(propView.build());
let propView = new PropertyView(this, name);
CssHtmlTree.processTemplate(this.templateProperty,
this.propertyContainer, propView, true);
if (propView.visible) { if (propView.visible) {
this.numVisibleProperties++; this.numVisibleProperties++;
} }
propView.refreshAllSelectors(); propView.refreshAllSelectors();
this.propertyViews.push(propView); this.propertyViews.push(propView);
} }.bind(this),
if (i < max) { onDone: function() {
// There are still some properties to display. We loop here to display // Completed callback.
// the next batch of 15.
this._panelRefreshTimeout =
this.win.setTimeout(displayProperties.bind(this), 15);
} else {
this.htmlComplete = true; this.htmlComplete = true;
this._panelRefreshTimeout = null; this.propertyContainer.appendChild(fragment);
this.noResults.hidden = this.numVisibleProperties > 0; this.noResults.hidden = this.numVisibleProperties > 0;
this._refreshProcess = null;
Services.obs.notifyObservers(null, "StyleInspector-populated", null); Services.obs.notifyObservers(null, "StyleInspector-populated", null);
} }.bind(this)});
}
} this._refreshProcess.schedule();
this._panelRefreshTimeout =
this.win.setTimeout(displayProperties.bind(this), 15);
} }
}, },
@ -229,8 +315,8 @@ CssHtmlTree.prototype = {
*/ */
refreshPanel: function CssHtmlTree_refreshPanel() refreshPanel: function CssHtmlTree_refreshPanel()
{ {
if (this._panelRefreshTimeout) { if (this._refreshProcess) {
this.win.clearTimeout(this._panelRefreshTimeout); this._refreshProcess.cancel();
} }
this.noResults.hidden = true; this.noResults.hidden = true;
@ -241,27 +327,18 @@ CssHtmlTree.prototype = {
// Reset zebra striping. // Reset zebra striping.
this._darkStripe = true; this._darkStripe = true;
// We use a setTimeout loop to display the properties in batches of 15 at a let display = this.propertyContainer.style.display;
// time. This results in a perceptibly more responsive UI. this._refreshProcess = new UpdateProcess(this.win, this.propertyViews, {
let i = 0; onItem: function(aPropView) {
let batchSize = 15; aPropView.refresh();
let max = this.propertyViews.length - 1; }.bind(this),
function refreshView() { onDone: function() {
// Refresh the next 15 property views this._refreshProcess = null;
for (let step = i + batchSize; i < step && i <= max; i++) { this.noResults.hidden = this.numVisibleProperties > 0
this.propertyViews[i].refresh();
}
if (i < max) {
// There are still some property views to refresh. We loop here to
// display the next batch of 15.
this._panelRefreshTimeout = this.win.setTimeout(refreshView.bind(this), 15);
} else {
this._panelRefreshTimeout = null;
this.noResults.hidden = this.numVisibleProperties > 0;
Services.obs.notifyObservers(null, "StyleInspector-populated", null); Services.obs.notifyObservers(null, "StyleInspector-populated", null);
} }.bind(this)
} });
this._panelRefreshTimeout = this.win.setTimeout(refreshView.bind(this), 15); this._refreshProcess.schedule();
}, },
/** /**
@ -423,7 +500,6 @@ CssHtmlTree.prototype = {
delete this.path; delete this.path;
delete this.templatePath; delete this.templatePath;
delete this.propertyContainer; delete this.propertyContainer;
delete this.templateProperty;
delete this.panel; delete this.panel;
// The document in which we display the results (csshtmltree.xul). // The document in which we display the results (csshtmltree.xul).
@ -570,6 +646,50 @@ PropertyView.prototype = {
return "property-view-hidden"; return "property-view-hidden";
}, },
build: function PropertyView_build()
{
let doc = this.tree.doc;
this.element = doc.createElementNS(HTML_NS, "div");
this.element.setAttribute("class", this.className);
this.propertyHeader = doc.createElementNS(XUL_NS, "hbox");
this.element.appendChild(this.propertyHeader);
this.propertyHeader.setAttribute("class", "property-header");
this.propertyHeader.addEventListener("click", this.propertyHeaderClick.bind(this), false);
this.matchedExpander = doc.createElementNS(HTML_NS, "div");
this.propertyHeader.appendChild(this.matchedExpander);
this.matchedExpander.setAttribute("class", "match expander");
let name = doc.createElementNS(HTML_NS, "div");
this.propertyHeader.appendChild(name);
name.setAttribute("class", "property-name");
name.textContent = this.name;
let helpcontainer = doc.createElementNS(HTML_NS, "div");
this.propertyHeader.appendChild(helpcontainer);
helpcontainer.setAttribute("class", "helplink-container");
let helplink = doc.createElementNS(HTML_NS, "a");
helpcontainer.appendChild(helplink);
helplink.setAttribute("class", "helplink");
helplink.setAttribute("title", CssHtmlTree.HELP_LINK_TITLE);
helplink.textContent = CssHtmlTree.HELP_LINK_TITLE;
helplink.addEventListener("click", this.mdnLinkClick.bind(this), false);
this.valueNode = doc.createElementNS(HTML_NS, "div");
this.propertyHeader.appendChild(this.valueNode);
this.valueNode.setAttribute("class", "property-value");
this.valueNode.setAttribute("dir", "ltr");
this.valueNode.textContent = this.value;
this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div");
this.element.appendChild(this.matchedSelectorsContainer);
this.matchedSelectorsContainer.setAttribute("class", "rulelink");
return this.element;
},
/** /**
* Refresh the panel's CSS property value. * Refresh the panel's CSS property value.
*/ */

Просмотреть файл

@ -118,30 +118,6 @@ To visually debug the templates without running firefox, alter the display:none
</ol> </ol>
</div> </div>
<!--
TemplateProperty lists the properties themselves. Each needs data like this:
{
property: ... // PropertyView from CssHtmlTree.jsm
}
-->
<div id="templateProperty">
<div class="${className}" save="${element}">
<xul:hbox class="property-header" save="${propertyHeader}"
onclick="${propertyHeaderClick}">
<div save="${matchedExpander}" class="match expander"/>
<div class="property-name">${name}</div>
<div class="helplink-container">
<a href="${link}" class="helplink" title="&helpLinkTitle;" onclick="${mdnLinkClick}">
&helpLinkTitle;
</a>
</div>
<div save="${valueNode}" class="property-value" dir="ltr">${value}</div>
</xul:hbox>
<div save="${matchedSelectorsContainer}" class="rulelink">
</div>
</div>
</div>
<!-- <!--
A templateMatchedSelectors sits inside each templateProperties showing the A templateMatchedSelectors sits inside each templateProperties showing the

Просмотреть файл

@ -12,12 +12,6 @@
- tree. --> - tree. -->
<!ENTITY selectedElementLabel "Selected element:"> <!ENTITY selectedElementLabel "Selected element:">
<!-- 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">
<!-- LOCALIZATION NOTE (noPropertiesFound): In the case where there are no CSS <!-- LOCALIZATION NOTE (noPropertiesFound): In the case where there are no CSS
- properties to display e.g. due to search criteria this message is - properties to display e.g. due to search criteria this message is
- displayed. --> - displayed. -->

Просмотреть файл

@ -41,3 +41,8 @@ style.highlighter.button.label1=Properties
style.highlighter.accesskey1=P style.highlighter.accesskey1=P
style.highlighter.button.tooltip=Inspect element styles style.highlighter.button.tooltip=Inspect element styles
# 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.
helpLinkTitle=Read the documentation for this property