blob: 75e4062898de43b2647f44e76b304b8ddd1e72ae [file] [log] [blame]
/*
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
* Copyright (C) 2009 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
WebInspector.AbstractTimelinePanel = function()
{
WebInspector.Panel.call(this);
this._items = [];
this._staleItems = [];
}
WebInspector.AbstractTimelinePanel.prototype = {
get categories()
{
// Should be implemented by the concrete subclasses.
return {};
},
populateSidebar: function()
{
// Should be implemented by the concrete subclasses.
},
createItemTreeElement: function(item)
{
// Should be implemented by the concrete subclasses.
},
createItemGraph: function(item)
{
// Should be implemented by the concrete subclasses.
},
createInterface: function()
{
this._createFilterPanel();
this.containerElement = document.createElement("div");
this.containerElement.id = "resources-container";
this.containerElement.addEventListener("scroll", this._updateDividersLabelBarPosition.bind(this), false);
this.element.appendChild(this.containerElement);
this.createSidebar(this.containerElement, this.element);
this.sidebarElement.id = "resources-sidebar";
this.populateSidebar();
this._createGraph();
},
_createFilterPanel: function()
{
this.filterBarElement = document.createElement("div");
this.filterBarElement.id = "resources-filter";
this.filterBarElement.className = "scope-bar";
this.element.appendChild(this.filterBarElement);
function createFilterElement(category)
{
if (category === "all")
var label = WebInspector.UIString("All");
else if (this.categories[category])
var label = this.categories[category].title;
var categoryElement = document.createElement("li");
categoryElement.category = category;
categoryElement.addStyleClass(category);
categoryElement.appendChild(document.createTextNode(label));
categoryElement.addEventListener("click", this._updateFilter.bind(this), false);
this.filterBarElement.appendChild(categoryElement);
return categoryElement;
}
this.filterAllElement = createFilterElement.call(this, "all");
// Add a divider
var dividerElement = document.createElement("div");
dividerElement.addStyleClass("divider");
this.filterBarElement.appendChild(dividerElement);
for (var category in this.categories)
createFilterElement.call(this, category);
},
_showCategory: function(category)
{
var filterClass = "filter-" + category.toLowerCase();
this.itemsGraphsElement.addStyleClass(filterClass);
this.itemsTreeElement.childrenListElement.addStyleClass(filterClass);
},
_hideCategory: function(category)
{
var filterClass = "filter-" + category.toLowerCase();
this.itemsGraphsElement.removeStyleClass(filterClass);
this.itemsTreeElement.childrenListElement.removeStyleClass(filterClass);
},
filter: function(target, selectMultiple)
{
function unselectAll()
{
for (var i = 0; i < this.filterBarElement.childNodes.length; ++i) {
var child = this.filterBarElement.childNodes[i];
if (!child.category)
continue;
child.removeStyleClass("selected");
this._hideCategory(child.category);
}
}
if (target === this.filterAllElement) {
if (target.hasStyleClass("selected")) {
// We can't unselect All, so we break early here
return;
}
// If All wasn't selected, and now is, unselect everything else.
unselectAll.call(this);
} else {
// Something other than All is being selected, so we want to unselect All.
if (this.filterAllElement.hasStyleClass("selected")) {
this.filterAllElement.removeStyleClass("selected");
this._hideCategory("all");
}
}
if (!selectMultiple) {
// If multiple selection is off, we want to unselect everything else
// and just select ourselves.
unselectAll.call(this);
target.addStyleClass("selected");
this._showCategory(target.category);
return;
}
if (target.hasStyleClass("selected")) {
// If selectMultiple is turned on, and we were selected, we just
// want to unselect ourselves.
target.removeStyleClass("selected");
this._hideCategory(target.category);
} else {
// If selectMultiple is turned on, and we weren't selected, we just
// want to select ourselves.
target.addStyleClass("selected");
this._showCategory(target.category);
}
},
_updateFilter: function(e)
{
var isMac = InspectorController.platform().indexOf("mac-") === 0;
var selectMultiple = false;
if (isMac && e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey)
selectMultiple = true;
if (!isMac && e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey)
selectMultiple = true;
this.filter(e.target, selectMultiple);
// When we are updating our filtering, scroll to the top so we don't end up
// in blank graph under all the resources.
this.containerElement.scrollTop = 0;
},
_createGraph: function()
{
this._containerContentElement = document.createElement("div");
this._containerContentElement.id = "resources-container-content";
this.containerElement.appendChild(this._containerContentElement);
this.summaryBar = new WebInspector.SummaryBar(this.categories);
this.summaryBar.element.id = "resources-summary";
this._containerContentElement.appendChild(this.summaryBar.element);
this.itemsGraphsElement = document.createElement("div");
this.itemsGraphsElement.id = "resources-graphs";
this._containerContentElement.appendChild(this.itemsGraphsElement);
this.dividersElement = document.createElement("div");
this.dividersElement.id = "resources-dividers";
this._containerContentElement.appendChild(this.dividersElement);
this.eventDividersElement = document.createElement("div");
this.eventDividersElement.id = "resources-event-dividers";
this._containerContentElement.appendChild(this.eventDividersElement);
this.dividersLabelBarElement = document.createElement("div");
this.dividersLabelBarElement.id = "resources-dividers-label-bar";
this._containerContentElement.appendChild(this.dividersLabelBarElement);
},
updateGraphDividersIfNeeded: function(force)
{
if (!this.visible) {
this.needsRefresh = true;
return false;
}
if (document.body.offsetWidth <= 0) {
// The stylesheet hasn't loaded yet or the window is closed,
// so we can't calculate what is need. Return early.
return false;
}
var dividerCount = Math.round(this.dividersElement.offsetWidth / 64);
var slice = this.calculator.boundarySpan / dividerCount;
if (!force && this._currentDividerSlice === slice)
return false;
this._currentDividerSlice = slice;
this.dividersElement.removeChildren();
this.eventDividersElement.removeChildren();
this.dividersLabelBarElement.removeChildren();
for (var i = 1; i <= dividerCount; ++i) {
var divider = document.createElement("div");
divider.className = "resources-divider";
if (i === dividerCount)
divider.addStyleClass("last");
divider.style.left = ((i / dividerCount) * 100) + "%";
this.dividersElement.appendChild(divider.cloneNode());
var label = document.createElement("div");
label.className = "resources-divider-label";
if (!isNaN(slice))
label.textContent = this.calculator.formatValue(slice * i);
divider.appendChild(label);
this.dividersLabelBarElement.appendChild(divider);
}
},
_updateDividersLabelBarPosition: function()
{
var scrollTop = this.containerElement.scrollTop;
var dividersTop = (scrollTop < this.summaryBar.element.offsetHeight ? this.summaryBar.element.offsetHeight : scrollTop);
this.dividersElement.style.top = scrollTop + "px";
this.eventDividersElement.style.top = scrollTop + "px";
this.dividersLabelBarElement.style.top = dividersTop + "px";
},
get needsRefresh()
{
return this._needsRefresh;
},
set needsRefresh(x)
{
if (this._needsRefresh === x)
return;
this._needsRefresh = x;
if (x) {
if (this.visible && !("_refreshTimeout" in this))
this._refreshTimeout = setTimeout(this.refresh.bind(this), 500);
} else {
if ("_refreshTimeout" in this) {
clearTimeout(this._refreshTimeout);
delete this._refreshTimeout;
}
}
},
refreshIfNeeded: function()
{
if (this.needsRefresh)
this.refresh();
},
show: function()
{
WebInspector.Panel.prototype.show.call(this);
this._updateDividersLabelBarPosition();
this.refreshIfNeeded();
},
resize: function()
{
this.updateGraphDividersIfNeeded();
},
updateMainViewWidth: function(width)
{
this._containerContentElement.style.left = width + "px";
this.updateGraphDividersIfNeeded();
},
refresh: function()
{
this.needsRefresh = false;
var staleItemsLength = this._staleItems.length;
var boundariesChanged = false;
for (var i = 0; i < staleItemsLength; ++i) {
var item = this._staleItems[i];
if (!item._itemTreeElement) {
// Create the timeline tree element and graph.
item._itemTreeElement = this.createItemTreeElement(item);
item._itemTreeElement._itemGraph = this.createItemGraph(item);
this.itemsTreeElement.appendChild(item._itemTreeElement);
this.itemsGraphsElement.appendChild(item._itemTreeElement._itemGraph.graphElement);
}
if (item._itemTreeElement.refresh)
item._itemTreeElement.refresh();
if (this.calculator.updateBoundaries(item))
boundariesChanged = true;
}
if (boundariesChanged) {
// The boundaries changed, so all item graphs are stale.
this._staleItems = this._items;
staleItemsLength = this._staleItems.length;
}
for (var i = 0; i < staleItemsLength; ++i)
this._staleItems[i]._itemTreeElement._itemGraph.refresh(this.calculator);
this._staleItems = [];
this.updateGraphDividersIfNeeded();
},
reset: function()
{
this.containerElement.scrollTop = 0;
if (this._calculator)
this._calculator.reset();
if (this._items) {
var itemsLength = this._items.length;
for (var i = 0; i < itemsLength; ++i) {
var item = this._items[i];
delete item._itemsTreeElement;
}
}
this._items = [];
this._staleItems = [];
this.itemsTreeElement.removeChildren();
this.itemsGraphsElement.removeChildren();
this.updateGraphDividersIfNeeded(true);
},
get calculator()
{
return this._calculator;
},
set calculator(x)
{
if (!x || this._calculator === x)
return;
this._calculator = x;
this._calculator.reset();
this._staleItems = this._items;
this.refresh();
},
addItem: function(item)
{
this._items.push(item);
this.refreshItem(item);
},
removeItem: function(item)
{
this._items.remove(item, true);
if (item._itemTreeElement) {
this.itemsTreeElement.removeChild(resource._itemTreeElement);
this.itemsGraphsElement.removeChild(resource._itemTreeElement._itemGraph.graphElement);
}
delete item._itemTreeElement;
this.adjustScrollPosition();
},
refreshItem: function(item)
{
this._staleItems.push(item);
this.needsRefresh = true;
},
revealAndSelectItem: function(item)
{
if (item._itemsTreeElement) {
item._itemsTreeElement.reveal();
item._itemsTreeElement.select(true);
}
},
sortItems: function(sortingFunction)
{
var sortedElements = [].concat(this.itemsTreeElement.children);
sortedElements.sort(sortingFunction);
var sortedElementsLength = sortedElements.length;
for (var i = 0; i < sortedElementsLength; ++i) {
var treeElement = sortedElements[i];
if (treeElement === this.itemsTreeElement.children[i])
continue;
var wasSelected = treeElement.selected;
this.itemsTreeElement.removeChild(treeElement);
this.itemsTreeElement.insertChild(treeElement, i);
if (wasSelected)
treeElement.select(true);
var graphElement = treeElement._itemGraph.graphElement;
this.itemsGraphsElement.insertBefore(graphElement, this.itemsGraphsElement.children[i]);
}
},
adjustScrollPosition: function()
{
// Prevent the container from being scrolled off the end.
if ((this.containerElement.scrollTop + this.containerElement.offsetHeight) > this.sidebarElement.offsetHeight)
this.containerElement.scrollTop = (this.sidebarElement.offsetHeight - this.containerElement.offsetHeight);
}
}
WebInspector.AbstractTimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
WebInspector.AbstractTimelineCalculator = function()
{
}
WebInspector.AbstractTimelineCalculator.prototype = {
computeSummaryValues: function(items)
{
var total = 0;
var categoryValues = {};
var itemsLength = items.length;
for (var i = 0; i < itemsLength; ++i) {
var item = items[i];
var value = this._value(item);
if (typeof value === "undefined")
continue;
if (!(item.category.name in categoryValues))
categoryValues[item.category.name] = 0;
categoryValues[item.category.name] += value;
total += value;
}
return {categoryValues: categoryValues, total: total};
},
computeBarGraphPercentages: function(item)
{
return {start: 0, middle: 0, end: (this._value(item) / this.boundarySpan) * 100};
},
computeBarGraphLabels: function(item)
{
const label = this.formatValue(this._value(item));
return {left: label, right: label, tooltip: label};
},
get boundarySpan()
{
return this.maximumBoundary - this.minimumBoundary;
},
updateBoundaries: function(item)
{
this.minimumBoundary = 0;
var value = this._value(item);
if (typeof this.maximumBoundary === "undefined" || value > this.maximumBoundary) {
this.maximumBoundary = value;
return true;
}
return false;
},
reset: function()
{
delete this.minimumBoundary;
delete this.maximumBoundary;
},
_value: function(item)
{
return 0;
},
formatValue: function(value)
{
return value.toString();
}
}
WebInspector.AbstractTimelineCategory = function(name, title, color)
{
this.name = name;
this.title = title;
this.color = color;
}
WebInspector.AbstractTimelineCategory.prototype = {
toString: function()
{
return this.title;
}
}