/* * 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;