| /* |
| * Copyright (C) 2013, 2015 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, treeOutline, synchronizerDelegate, editCallback, deleteCallback) |
| { |
| super(columns, editCallback, deleteCallback); |
| |
| if (treeOutline) |
| this._treeOutlineDataGridSynchronizer = new WI.TreeOutlineDataGridSynchronizer(treeOutline, this, synchronizerDelegate); |
| |
| 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. |
| |
| if (!this._treeOutlineDataGridSynchronizer) |
| this.removeChildren(); |
| |
| this._hidePopover(); |
| } |
| |
| shown() |
| { |
| // May be overridden by subclasses. If so, they should call the superclass. |
| |
| if (this._treeOutlineDataGridSynchronizer) |
| this._treeOutlineDataGridSynchronizer.synchronize(); |
| } |
| |
| hidden() |
| { |
| // May be overridden by subclasses. If so, they should call the superclass. |
| |
| this._hidePopover(); |
| } |
| |
| treeElementForDataGridNode(dataGridNode) |
| { |
| if (!this._treeOutlineDataGridSynchronizer) |
| return null; |
| |
| return this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode); |
| } |
| |
| dataGridNodeForTreeElement(treeElement) |
| { |
| if (!this._treeOutlineDataGridSynchronizer) |
| return null; |
| |
| return this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(treeElement); |
| } |
| |
| callFramePopoverAnchorElement() |
| { |
| // Implemented by subclasses. |
| return null; |
| } |
| |
| addRowInSortOrder(treeElement, dataGridNode, parentTreeElementOrDataGridNode) |
| { |
| let parentDataGridNode; |
| let childElement = dataGridNode; |
| |
| if (treeElement) { |
| console.assert(this._treeOutlineDataGridSynchronizer); |
| if (!this._treeOutlineDataGridSynchronizer) |
| return; |
| |
| this._treeOutlineDataGridSynchronizer.associate(treeElement, dataGridNode); |
| |
| console.assert(!parentTreeElementOrDataGridNode || parentTreeElementOrDataGridNode instanceof WI.TreeElement); |
| |
| let parentTreeElement = parentTreeElementOrDataGridNode || this._treeOutlineDataGridSynchronizer.treeOutline; |
| parentDataGridNode = parentTreeElement.root ? this : this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(parentTreeElement); |
| |
| parentTreeElementOrDataGridNode = parentTreeElement; |
| childElement = treeElement; |
| } else { |
| parentTreeElementOrDataGridNode = parentTreeElementOrDataGridNode || this; |
| parentDataGridNode = parentTreeElementOrDataGridNode; |
| } |
| |
| if (this.sortColumnIdentifier) { |
| let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, parentDataGridNode.children, this._sortComparator.bind(this)); |
| |
| // If parent is a tree element, the synchronizer will insert into the data grid. |
| parentTreeElementOrDataGridNode.insertChild(childElement, insertionIndex); |
| } else { |
| // If parent is a tree element, the synchronizer will append to the data grid. |
| parentTreeElementOrDataGridNode.appendChild(childElement); |
| } |
| } |
| |
| 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); |
| |
| if (this._treeOutlineDataGridSynchronizer) |
| this._treeOutlineDataGridSynchronizer.enabled = false; |
| |
| 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; |
| } |
| |
| if (!this._treeOutlineDataGridSynchronizer) |
| continue; |
| |
| let treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline; |
| let treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode); |
| console.assert(treeElement); |
| |
| treeOutline.reattachIfIndexChanged(treeElement, insertionIndex); |
| |
| // Adding the tree element back to the tree outline subjects it to filters. |
| // Make sure we keep the hidden state in-sync while the synchronizer is disabled. |
| dataGridNode.element.classList.toggle("hidden", treeElement.hidden); |
| } |
| |
| if (this._treeOutlineDataGridSynchronizer) |
| this._treeOutlineDataGridSynchronizer.enabled = true; |
| |
| this._dirtyDataGridNodes = null; |
| } |
| |
| _sort() |
| { |
| if (!this.children.length) |
| return; |
| |
| let sortColumnIdentifier = this.sortColumnIdentifier; |
| if (!sortColumnIdentifier) |
| return; |
| |
| let selectedNode = this.selectedNode; |
| this._ignoreSelectionEvent = true; |
| |
| let treeOutline; |
| if (this._treeOutlineDataGridSynchronizer) { |
| this._treeOutlineDataGridSynchronizer.enabled = false; |
| |
| treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline; |
| if (treeOutline.selectedTreeElement) |
| treeOutline.selectedTreeElement.deselect(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(); |
| |
| let parentTreeElement; |
| if (this._treeOutlineDataGridSynchronizer) { |
| parentTreeElement = parentDataGridNode === this ? treeOutline : this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(parentDataGridNode); |
| console.assert(parentTreeElement); |
| |
| parentTreeElement.removeChildren(); |
| } |
| |
| childDataGridNodes.sort(this._sortComparator.bind(this)); |
| |
| for (let dataGridNode of childDataGridNodes) { |
| if (this._treeOutlineDataGridSynchronizer) { |
| let treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode); |
| console.assert(treeElement); |
| |
| if (parentTreeElement) |
| parentTreeElement.appendChild(treeElement); |
| |
| // Adding the tree element back to the tree outline subjects it to filters. |
| // Make sure we keep the hidden state in-sync while the synchronizer is disabled. |
| dataGridNode.element.classList.toggle("hidden", treeElement.hidden); |
| } |
| |
| parentDataGridNode.appendChild(dataGridNode); |
| } |
| } |
| |
| if (this._treeOutlineDataGridSynchronizer) |
| this._treeOutlineDataGridSynchronizer.enabled = true; |
| |
| 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; |
| } |
| |
| 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 = event.data.selectedElement; |
| 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; |