blob: f4cba4f78a03a3e14f8ae80c437b1a188c136cc7 [file] [log] [blame]
/*
* Copyright (C) 2013-2018 Apple 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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.
*/
WI.TimelineDataGrid = class TimelineDataGrid extends WI.DataGrid
{
constructor(columns)
{
super(columns);
this.element.classList.add("timeline");
this._sortDelegate = null;
this._scopeBarColumns = [];
// Check if any of the cells can be filtered.
for (var [identifier, column] of this.columns) {
var scopeBar = column.scopeBar;
if (!scopeBar)
continue;
this._scopeBarColumns.push(identifier);
scopeBar.columnIdentifier = identifier;
scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectedItemsDidChange, this);
}
if (this._scopeBarColumns.length > 1) {
console.error("Creating a TimelineDataGrid with more than one filterable column is not yet supported.");
return;
}
this.addEventListener(WI.DataGrid.Event.SelectedNodeChanged, this._dataGridSelectedNodeChanged, this);
this.addEventListener(WI.DataGrid.Event.SortChanged, this._sort, this);
this.columnChooserEnabled = true;
}
static createColumnScopeBar(prefix, map)
{
prefix = prefix + "-timeline-data-grid-";
var scopeBarItems = [];
for (var [key, value] of map) {
var id = prefix + key;
var item = new WI.ScopeBarItem(id, value);
item.value = key;
scopeBarItems.push(item);
}
var allItem = new WI.ScopeBarItem(prefix + "type-all", WI.UIString("All"));
scopeBarItems.unshift(allItem);
return new WI.ScopeBar(prefix + "scope-bar", scopeBarItems, allItem, true);
}
// Public
get sortDelegate()
{
return this._sortDelegate;
}
set sortDelegate(delegate)
{
delegate = delegate || null;
if (this._sortDelegate === delegate)
return;
this._sortDelegate = delegate;
if (this.sortOrder !== WI.DataGrid.SortOrder.Indeterminate)
this.dispatchEventToListeners(WI.DataGrid.Event.SortChanged);
}
reset()
{
// May be overridden by subclasses. If so, they should call the superclass.
this.removeChildren();
this._hidePopover();
}
shown()
{
// May be overridden by subclasses. If so, they should call the superclass.
}
hidden()
{
// May be overridden by subclasses. If so, they should call the superclass.
this._hidePopover();
}
callFramePopoverAnchorElement()
{
// Implemented by subclasses.
return null;
}
shouldShowCallFramePopover()
{
// Implemented by subclasses.
return false;
}
addRowInSortOrder(dataGridNode, parentDataGridNode)
{
parentDataGridNode = parentDataGridNode || this;
if (this.sortColumnIdentifier) {
let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, parentDataGridNode.children, this._sortComparator.bind(this));
parentDataGridNode.insertChild(dataGridNode, insertionIndex);
} else
parentDataGridNode.appendChild(dataGridNode);
}
shouldIgnoreSelectionEvent()
{
return this._ignoreSelectionEvent || false;
}
// Protected
dataGridNodeNeedsRefresh(dataGridNode)
{
if (!this._dirtyDataGridNodes)
this._dirtyDataGridNodes = new Set;
this._dirtyDataGridNodes.add(dataGridNode);
if (this._scheduledDataGridNodeRefreshIdentifier)
return;
this._scheduledDataGridNodeRefreshIdentifier = requestAnimationFrame(this._refreshDirtyDataGridNodes.bind(this));
}
hasCustomFilters()
{
return true;
}
matchNodeAgainstCustomFilters(node)
{
if (!super.matchNodeAgainstCustomFilters(node))
return false;
for (let identifier of this._scopeBarColumns) {
let scopeBar = this.columns.get(identifier).scopeBar;
if (!scopeBar || scopeBar.defaultItem.selected)
continue;
let value = node.data[identifier];
if (!scopeBar.selectedItems.some((scopeBarItem) => scopeBarItem.value === value))
return false;
}
return true;
}
// Private
_refreshDirtyDataGridNodes()
{
if (this._scheduledDataGridNodeRefreshIdentifier) {
cancelAnimationFrame(this._scheduledDataGridNodeRefreshIdentifier);
this._scheduledDataGridNodeRefreshIdentifier = undefined;
}
if (!this._dirtyDataGridNodes)
return;
let selectedNode = this.selectedNode;
let sortComparator = this._sortComparator.bind(this);
for (let dataGridNode of this._dirtyDataGridNodes) {
dataGridNode.refresh();
if (!this.sortColumnIdentifier)
continue;
if (dataGridNode === selectedNode)
this._ignoreSelectionEvent = true;
console.assert(!dataGridNode.parent || dataGridNode.parent === this);
if (dataGridNode.parent === this)
this.removeChild(dataGridNode);
let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, this.children, sortComparator);
this.insertChild(dataGridNode, insertionIndex);
if (dataGridNode === selectedNode) {
selectedNode.revealAndSelect();
this._ignoreSelectionEvent = false;
}
}
this._dirtyDataGridNodes = null;
}
_sort()
{
if (!this.children.length)
return;
let sortColumnIdentifier = this.sortColumnIdentifier;
if (!sortColumnIdentifier)
return;
let selectedNode = this.selectedNode;
this._ignoreSelectionEvent = true;
// Collect parent nodes that need their children sorted. So this in two phases since
// traverseNextNode would get confused if we sort the tree while traversing it.
let parentDataGridNodes = [this];
let currentDataGridNode = this.children[0];
while (currentDataGridNode) {
if (currentDataGridNode.children.length)
parentDataGridNodes.push(currentDataGridNode);
currentDataGridNode = currentDataGridNode.traverseNextNode(false, null, true);
}
// Sort the children of collected parent nodes.
for (let parentDataGridNode of parentDataGridNodes) {
let childDataGridNodes = parentDataGridNode.children.slice();
parentDataGridNode.removeChildren();
childDataGridNodes.sort(this._sortComparator.bind(this));
for (let dataGridNode of childDataGridNodes)
parentDataGridNode.appendChild(dataGridNode);
}
if (selectedNode)
selectedNode.revealAndSelect();
this._ignoreSelectionEvent = false;
}
_sortComparator(node1, node2)
{
var sortColumnIdentifier = this.sortColumnIdentifier;
if (!sortColumnIdentifier)
return 0;
var sortDirection = this.sortOrder === WI.DataGrid.SortOrder.Ascending ? 1 : -1;
if (this._sortDelegate && typeof this._sortDelegate.dataGridSortComparator === "function") {
let result = this._sortDelegate.dataGridSortComparator(sortColumnIdentifier, sortDirection, node1, node2);
if (typeof result === "number")
return result;
}
var value1 = node1.data[sortColumnIdentifier];
var value2 = node2.data[sortColumnIdentifier];
if (typeof value1 === "number" && typeof value2 === "number") {
if (isNaN(value1) && isNaN(value2))
return 0;
if (isNaN(value1))
return -sortDirection;
if (isNaN(value2))
return sortDirection;
return (value1 - value2) * sortDirection;
}
if (typeof value1 === "string" && typeof value2 === "string")
return value1.extendedLocaleCompare(value2) * sortDirection;
if (value1 instanceof WI.CallFrame || value2 instanceof WI.CallFrame) {
// Sort by function name if available, then fall back to the source code object.
value1 = value1 && value1.functionName ? value1.functionName : (value1 && value1.sourceCodeLocation ? value1.sourceCodeLocation.sourceCode : "");
value2 = value2 && value2.functionName ? value2.functionName : (value2 && value2.sourceCodeLocation ? value2.sourceCodeLocation.sourceCode : "");
}
if (value1 instanceof WI.SourceCode || value2 instanceof WI.SourceCode) {
value1 = value1 ? value1.displayName || "" : "";
value2 = value2 ? value2.displayName || "" : "";
}
if (value1 instanceof WI.SourceCodeLocation || value2 instanceof WI.SourceCodeLocation) {
value1 = value1 ? value1.displayLocationString() || "" : "";
value2 = value2 ? value2.displayLocationString() || "" : "";
}
// For everything else (mostly booleans).
return (value1 < value2 ? -1 : (value1 > value2 ? 1 : 0)) * sortDirection;
}
_scopeBarSelectedItemsDidChange(event)
{
this.filterDidChange();
}
_dataGridSelectedNodeChanged(event)
{
if (!this.selectedNode) {
this._hidePopover();
return;
}
var record = this.selectedNode.record;
if (!record || !record.callFrames || !record.callFrames.length) {
this._hidePopover();
return;
}
if (this.shouldShowCallFramePopover())
this._showPopoverForSelectedNodeSoon();
}
_showPopoverForSelectedNodeSoon()
{
if (this._showPopoverTimeout)
return;
this._showPopoverTimeout = setTimeout(() => {
if (!this._popover) {
this._popover = new WI.Popover;
this._popover.windowResizeHandler = () => { this._updatePopoverForSelectedNode(false); };
}
this._updatePopoverForSelectedNode(true);
this._showPopoverTimeout = undefined;
}, WI.TimelineDataGrid.DelayedPopoverShowTimeout);
}
_hidePopover()
{
if (this._showPopoverTimeout) {
clearTimeout(this._showPopoverTimeout);
this._showPopoverTimeout = undefined;
}
if (this._popover)
this._popover.dismiss();
if (this._hidePopoverContentClearTimeout)
clearTimeout(this._hidePopoverContentClearTimeout);
this._hidePopoverContentClearTimeout = setTimeout(() => {
if (this._popoverCallStackTreeOutline)
this._popoverCallStackTreeOutline.removeChildren();
}, WI.TimelineDataGrid.DelayedPopoverHideContentClearTimeout);
}
_updatePopoverForSelectedNode(updateContent)
{
if (!this._popover || !this.selectedNode)
return;
let targetPopoverElement = this.callFramePopoverAnchorElement();
console.assert(targetPopoverElement, "TimelineDataGrid subclass should always return a valid element from callFramePopoverAnchorElement.");
if (!targetPopoverElement)
return;
// The element might be hidden if it does not have a width and height.
let rect = WI.Rect.rectFromClientRect(targetPopoverElement.getBoundingClientRect());
if (!rect.size.width && !rect.size.height)
return;
if (this._hidePopoverContentClearTimeout) {
clearTimeout(this._hidePopoverContentClearTimeout);
this._hidePopoverContentClearTimeout = undefined;
}
let targetFrame = rect.pad(2);
let preferredEdges = [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X];
if (updateContent)
this._popover.presentNewContentWithFrame(this._createPopoverContent(), targetFrame, preferredEdges);
else
this._popover.present(targetFrame, preferredEdges);
}
_createPopoverContent()
{
if (!this._popoverCallStackTreeOutline) {
this._popoverCallStackTreeOutline = new WI.TreeOutline;
this._popoverCallStackTreeOutline.disclosureButtons = false;
this._popoverCallStackTreeOutline.element.classList.add("timeline-data-grid");
this._popoverCallStackTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._popoverCallStackTreeSelectionDidChange, this);
} else
this._popoverCallStackTreeOutline.removeChildren();
var callFrames = this.selectedNode.record.callFrames;
for (var i = 0; i < callFrames.length; ++i) {
var callFrameTreeElement = new WI.CallFrameTreeElement(callFrames[i]);
this._popoverCallStackTreeOutline.appendChild(callFrameTreeElement);
}
let content = document.createElement("div");
content.appendChild(this._popoverCallStackTreeOutline.element);
return content;
}
_popoverCallStackTreeSelectionDidChange(event)
{
let treeElement = this._popoverCallStackTreeOutline.selectedTreeElement;
if (!treeElement)
return;
this._popover.dismiss();
console.assert(treeElement instanceof WI.CallFrameTreeElement, "TreeElements in TimelineDataGrid popover should always be CallFrameTreeElements");
var callFrame = treeElement.callFrame;
if (!callFrame.sourceCodeLocation)
return;
WI.showSourceCodeLocation(callFrame.sourceCodeLocation, {
ignoreNetworkTab: true,
ignoreSearchTab: true,
});
}
};
WI.TimelineDataGrid.HasNonDefaultFilterStyleClassName = "has-non-default-filter";
WI.TimelineDataGrid.DelayedPopoverShowTimeout = 250;
WI.TimelineDataGrid.DelayedPopoverHideContentClearTimeout = 500;