00026c50f58f3d7044f847d78254e0e443d9bfbb
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TimelineDataGrid.js
1 /*
2  * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.TimelineDataGrid = class TimelineDataGrid extends WebInspector.DataGrid
27 {
28     constructor(treeOutline, columns, delegate, editCallback, deleteCallback)
29     {
30         super(columns, editCallback, deleteCallback);
31
32         if (treeOutline)
33             this._treeOutlineDataGridSynchronizer = new WebInspector.TreeOutlineDataGridSynchronizer(treeOutline, this, delegate);
34
35         this.element.classList.add("timeline");
36
37         this._filterableColumns = [];
38
39         // Check if any of the cells can be filtered.
40         for (var [identifier, column] of this.columns) {
41             var scopeBar = column.scopeBar;
42
43             if (!scopeBar)
44                 continue;
45
46             this._filterableColumns.push(identifier);
47             scopeBar.columnIdentifier = identifier;
48             scopeBar.addEventListener(WebInspector.ScopeBar.Event.SelectionChanged, this._scopeBarSelectedItemsDidChange, this);
49         }
50
51         if (this._filterableColumns.length > 1) {
52             console.error("Creating a TimelineDataGrid with more than one filterable column is not yet supported.");
53             return;
54         }
55
56         this.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._dataGridSelectedNodeChanged, this);
57         this.addEventListener(WebInspector.DataGrid.Event.SortChanged, this._sort, this);
58
59         window.addEventListener("resize", this);
60     }
61
62     static createColumnScopeBar(prefix, map)
63     {
64         prefix = prefix + "-timeline-data-grid-";
65
66         var scopeBarItems = [];
67         for (var [key, value] of map) {
68             var id = prefix + key;
69             var item = new WebInspector.ScopeBarItem(id, value);
70             item.value = key;
71             scopeBarItems.push(item);
72         }
73
74         var allItem = new WebInspector.ScopeBarItem(prefix + "type-all", WebInspector.UIString("All"));
75         scopeBarItems.unshift(allItem);
76
77         return new WebInspector.ScopeBar(prefix + "scope-bar", scopeBarItems, allItem, true);
78     }
79
80     // Public
81
82     reset()
83     {
84         // May be overridden by subclasses. If so, they should call the superclass.
85
86         if (!this._treeOutlineDataGridSynchronizer)
87             this.removeChildren();
88
89         this._hidePopover();
90     }
91
92     shown()
93     {
94         // May be overridden by subclasses. If so, they should call the superclass.
95
96         if (this._treeOutlineDataGridSynchronizer)
97             this._treeOutlineDataGridSynchronizer.synchronize();
98     }
99
100     hidden()
101     {
102         // May be overridden by subclasses. If so, they should call the superclass.
103
104         this._hidePopover();
105     }
106
107     closed()
108     {
109         window.removeEventListener("resize", this);
110     }
111
112     treeElementForDataGridNode(dataGridNode)
113     {
114         if (!this._treeOutlineDataGridSynchronizer)
115             return null;
116
117         return this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
118     }
119
120     dataGridNodeForTreeElement(treeElement)
121     {
122         if (!this._treeOutlineDataGridSynchronizer)
123             return null;
124
125         return this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(treeElement);
126     }
127
128     callFramePopoverAnchorElement()
129     {
130         // Implemented by subclasses.
131         return null;
132     }
133
134     treeElementMatchesActiveScopeFilters(treeElement)
135     {
136         if (!this._treeOutlineDataGridSynchronizer)
137             return false;
138
139         var dataGridNode = this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(treeElement);
140         console.assert(dataGridNode);
141
142         for (var identifier of this._filterableColumns) {
143             var scopeBar = this.columns.get(identifier).scopeBar;
144             if (!scopeBar || scopeBar.defaultItem.selected)
145                 continue;
146
147             var value = dataGridNode.data[identifier];
148             var matchesFilter = scopeBar.selectedItems.some(function(scopeBarItem) {
149                 return scopeBarItem.value === value;
150             });
151
152             if (!matchesFilter)
153                 return false;
154         }
155
156         return true;
157     }
158
159     addRowInSortOrder(treeElement, dataGridNode, parentTreeElementOrDataGridNode)
160     {
161         let parentDataGridNode;
162         let childElement = dataGridNode;
163
164         if (treeElement) {
165             console.assert(this._treeOutlineDataGridSynchronizer);
166             if (!this._treeOutlineDataGridSynchronizer)
167                 return;
168
169             this._treeOutlineDataGridSynchronizer.associate(treeElement, dataGridNode);
170
171             console.assert(!parentTreeElementOrDataGridNode || parentTreeElementOrDataGridNode instanceof WebInspector.TreeElement);
172
173             let parentTreeElement = parentTreeElementOrDataGridNode || this._treeOutlineDataGridSynchronizer.treeOutline;
174             parentDataGridNode = parentTreeElement.root ? this : this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(parentTreeElement);
175
176             parentTreeElementOrDataGridNode = parentTreeElement;
177             childElement = treeElement;
178         } else {
179             parentTreeElementOrDataGridNode = parentTreeElementOrDataGridNode || this;
180             parentDataGridNode = parentTreeElementOrDataGridNode;
181         }
182
183         if (this.sortColumnIdentifier) {
184             let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, parentDataGridNode.children, this._sortComparator.bind(this));
185
186             // If parent is a tree element, the synchronizer will insert into the data grid.
187             parentTreeElementOrDataGridNode.insertChild(childElement, insertionIndex);
188         } else {
189             // If parent is a tree element, the synchronizer will append to the data grid.
190             parentTreeElementOrDataGridNode.appendChild(childElement);
191         }
192     }
193
194     shouldIgnoreSelectionEvent()
195     {
196         return this._ignoreSelectionEvent || false;
197     }
198
199     // Protected
200
201     handleEvent(event)
202     {
203         console.assert(event.type === "resize");
204
205         this._windowResized(event);
206     }
207
208     dataGridNodeNeedsRefresh(dataGridNode)
209     {
210         if (!this._dirtyDataGridNodes)
211             this._dirtyDataGridNodes = new Set;
212         this._dirtyDataGridNodes.add(dataGridNode);
213
214         if (this._scheduledDataGridNodeRefreshIdentifier)
215             return;
216
217         this._scheduledDataGridNodeRefreshIdentifier = requestAnimationFrame(this._refreshDirtyDataGridNodes.bind(this));
218     }
219
220     // Private
221
222     _refreshDirtyDataGridNodes()
223     {
224         if (this._scheduledDataGridNodeRefreshIdentifier) {
225             cancelAnimationFrame(this._scheduledDataGridNodeRefreshIdentifier);
226             this._scheduledDataGridNodeRefreshIdentifier = undefined;
227         }
228
229         if (!this._dirtyDataGridNodes || !this._treeOutlineDataGridSynchronizer)
230             return;
231
232         var selectedNode = this.selectedNode;
233         var sortComparator = this._sortComparator.bind(this);
234         var treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
235
236         this._treeOutlineDataGridSynchronizer.enabled = false;
237
238         for (var dataGridNode of this._dirtyDataGridNodes) {
239             dataGridNode.refresh();
240
241             if (!this.sortColumnIdentifier)
242                 continue;
243
244             if (dataGridNode === selectedNode)
245                 this._ignoreSelectionEvent = true;
246
247             var treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
248             console.assert(treeElement);
249
250             console.assert(!treeElement.parent || treeElement.parent === treeOutline);
251             if (treeElement.parent === treeOutline)
252                 treeOutline.removeChild(treeElement);
253
254             console.assert(!dataGridNode.parent || dataGridNode.parent === this);
255             if (dataGridNode.parent === this)
256                 this.removeChild(dataGridNode);
257
258             var insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, this.children, sortComparator);
259             treeOutline.insertChild(treeElement, insertionIndex);
260             this.insertChild(dataGridNode, insertionIndex);
261
262             // Adding the tree element back to the tree outline subjects it to filters.
263             // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
264             dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
265
266             if (dataGridNode === selectedNode) {
267                 selectedNode.revealAndSelect();
268                 this._ignoreSelectionEvent = false;
269             }
270         }
271
272         this._treeOutlineDataGridSynchronizer.enabled = true;
273
274         this._dirtyDataGridNodes = null;
275     }
276
277     _sort()
278     {
279         let sortColumnIdentifier = this.sortColumnIdentifier;
280         if (!sortColumnIdentifier)
281             return;
282
283         let selectedNode = this.selectedNode;
284         this._ignoreSelectionEvent = true;
285
286         let treeOutline;
287         if (this._treeOutlineDataGridSynchronizer) {
288             this._treeOutlineDataGridSynchronizer.enabled = false;
289
290             treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
291             if (treeOutline.selectedTreeElement)
292                 treeOutline.selectedTreeElement.deselect(true);
293         }
294
295         // Collect parent nodes that need their children sorted. So this in two phases since
296         // traverseNextNode would get confused if we sort the tree while traversing it.
297         let parentDataGridNodes = [this];
298         let currentDataGridNode = this.children[0];
299         while (currentDataGridNode) {
300             if (currentDataGridNode.children.length)
301                 parentDataGridNodes.push(currentDataGridNode);
302             currentDataGridNode = currentDataGridNode.traverseNextNode(false, null, true);
303         }
304
305         // Sort the children of collected parent nodes.
306         for (let parentDataGridNode of parentDataGridNodes) {
307             let childDataGridNodes = parentDataGridNode.children.slice();
308             parentDataGridNode.removeChildren();
309
310             let parentTreeElement;
311             if (this._treeOutlineDataGridSynchronizer) {
312                 parentTreeElement = parentDataGridNode === this ? treeOutline : this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(parentDataGridNode);
313                 console.assert(parentTreeElement);
314
315                 parentTreeElement.removeChildren();
316             }
317
318             childDataGridNodes.sort(this._sortComparator.bind(this));
319
320             for (let dataGridNode of childDataGridNodes) {
321                 if (this._treeOutlineDataGridSynchronizer) {
322                     let treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
323                     console.assert(treeElement);
324
325                     if (parentTreeElement)
326                         parentTreeElement.appendChild(treeElement);
327
328                     // Adding the tree element back to the tree outline subjects it to filters.
329                     // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
330                     dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
331                 }
332
333                 parentDataGridNode.appendChild(dataGridNode);
334             }
335         }
336
337         if (this._treeOutlineDataGridSynchronizer)
338             this._treeOutlineDataGridSynchronizer.enabled = true;
339
340         if (selectedNode)
341             selectedNode.revealAndSelect();
342
343         this._ignoreSelectionEvent = false;
344     }
345
346     _sortComparator(node1, node2)
347     {
348         var sortColumnIdentifier = this.sortColumnIdentifier;
349         if (!sortColumnIdentifier)
350             return 0;
351
352         var sortDirection = this.sortOrder === WebInspector.DataGrid.SortOrder.Ascending ? 1 : -1;
353
354         var value1 = node1.data[sortColumnIdentifier];
355         var value2 = node2.data[sortColumnIdentifier];
356
357         if (typeof value1 === "number" && typeof value2 === "number") {
358             if (isNaN(value1) && isNaN(value2))
359                 return 0;
360             if (isNaN(value1))
361                 return -sortDirection;
362             if (isNaN(value2))
363                 return sortDirection;
364             return (value1 - value2) * sortDirection;
365         }
366
367         if (typeof value1 === "string" && typeof value2 === "string")
368             return value1.localeCompare(value2) * sortDirection;
369
370         if (value1 instanceof WebInspector.CallFrame || value2 instanceof WebInspector.CallFrame) {
371             // Sort by function name if available, then fall back to the source code object.
372             value1 = value1 && value1.functionName ? value1.functionName : (value1 && value1.sourceCodeLocation ? value1.sourceCodeLocation.sourceCode : "");
373             value2 = value2 && value2.functionName ? value2.functionName : (value2 && value2.sourceCodeLocation ? value2.sourceCodeLocation.sourceCode : "");
374         }
375
376         if (value1 instanceof WebInspector.SourceCode || value2 instanceof WebInspector.SourceCode) {
377             value1 = value1 ? value1.displayName || "" : "";
378             value2 = value2 ? value2.displayName || "" : "";
379         }
380
381         // For everything else (mostly booleans).
382         return (value1 < value2 ? -1 : (value1 > value2 ? 1 : 0)) * sortDirection;
383     }
384
385     _updateScopeBarForcedVisibility()
386     {
387         for (var identifier of this._filterableColumns) {
388             var scopeBar = this.columns.get(identifier).scopeBar;
389             if (scopeBar) {
390                 this.element.classList.toggle(WebInspector.TimelineDataGrid.HasNonDefaultFilterStyleClassName, scopeBar.hasNonDefaultItemSelected());
391                 break;
392             }
393         }
394     }
395
396     _scopeBarSelectedItemsDidChange(event)
397     {
398         this._updateScopeBarForcedVisibility();
399
400         var columnIdentifier = event.target.columnIdentifier;
401         this.dispatchEventToListeners(WebInspector.TimelineDataGrid.Event.FiltersDidChange, {columnIdentifier});
402     }
403
404     _dataGridSelectedNodeChanged(event)
405     {
406         if (!this.selectedNode) {
407             this._hidePopover();
408             return;
409         }
410
411         var record = this.selectedNode.record;
412         if (!record || !record.callFrames || !record.callFrames.length) {
413             this._hidePopover();
414             return;
415         }
416
417         this._showPopoverForSelectedNodeSoon();
418     }
419
420     _windowResized(event)
421     {
422         if (this._popover && this._popover.visible)
423             this._updatePopoverForSelectedNode(false);
424     }
425
426     _showPopoverForSelectedNodeSoon()
427     {
428         if (this._showPopoverTimeout)
429             return;
430
431         function delayedWork()
432         {
433             if (!this._popover)
434                 this._popover = new WebInspector.Popover;
435
436             this._updatePopoverForSelectedNode(true);
437         }
438
439         this._showPopoverTimeout = setTimeout(delayedWork.bind(this), WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout);
440     }
441
442     _hidePopover()
443     {
444         if (this._showPopoverTimeout) {
445             clearTimeout(this._showPopoverTimeout);
446             this._showPopoverTimeout = undefined;
447         }
448
449         if (this._popover)
450             this._popover.dismiss();
451
452         function delayedWork()
453         {
454             if (this._popoverCallStackTreeOutline)
455                 this._popoverCallStackTreeOutline.removeChildren();
456         }
457
458         if (this._hidePopoverContentClearTimeout)
459             clearTimeout(this._hidePopoverContentClearTimeout);
460         this._hidePopoverContentClearTimeout = setTimeout(delayedWork.bind(this), WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout);
461     }
462
463     _updatePopoverForSelectedNode(updateContent)
464     {
465         if (!this._popover || !this.selectedNode)
466             return;
467
468         var targetPopoverElement = this.callFramePopoverAnchorElement();
469         console.assert(targetPopoverElement, "TimelineDataGrid subclass should always return a valid element from callFramePopoverAnchorElement.");
470         if (!targetPopoverElement)
471             return;
472
473         var targetFrame = WebInspector.Rect.rectFromClientRect(targetPopoverElement.getBoundingClientRect());
474
475         // The element might be hidden if it does not have a width and height.
476         if (!targetFrame.size.width && !targetFrame.size.height)
477             return;
478
479         if (this._hidePopoverContentClearTimeout) {
480             clearTimeout(this._hidePopoverContentClearTimeout);
481             this._hidePopoverContentClearTimeout = undefined;
482         }
483
484         if (updateContent)
485             this._popover.content = this._createPopoverContent();
486
487         this._popover.present(targetFrame.pad(2), [WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MAX_X]);
488     }
489
490     _createPopoverContent()
491     {
492         if (!this._popoverCallStackTreeOutline) {
493             this._popoverCallStackTreeOutline = new WebInspector.TreeOutline;
494             this._popoverCallStackTreeOutline.disclosureButtons = false;
495             this._popoverCallStackTreeOutline.element.classList.add("timeline-data-grid");
496             this._popoverCallStackTreeOutline.addEventListener(WebInspector.TreeOutline.Event.SelectionDidChange, this._popoverCallStackTreeSelectionDidChange, this);
497         } else
498             this._popoverCallStackTreeOutline.removeChildren();
499
500         var callFrames = this.selectedNode.record.callFrames;
501         for (var i = 0; i < callFrames.length; ++i) {
502             var callFrameTreeElement = new WebInspector.CallFrameTreeElement(callFrames[i]);
503             this._popoverCallStackTreeOutline.appendChild(callFrameTreeElement);
504         }
505
506         var content = document.createElement("div");
507         content.className = "timeline-data-grid-popover";
508         content.appendChild(this._popoverCallStackTreeOutline.element);
509         return content;
510     }
511
512     _popoverCallStackTreeSelectionDidChange(event)
513     {
514         let treeElement = event.data.selectedElement;
515         if (!treeElement)
516             return;
517
518         this._popover.dismiss();
519
520         console.assert(treeElement instanceof WebInspector.CallFrameTreeElement, "TreeElements in TimelineDataGrid popover should always be CallFrameTreeElements");
521         var callFrame = treeElement.callFrame;
522         if (!callFrame.sourceCodeLocation)
523             return;
524
525         WebInspector.showSourceCodeLocation(callFrame.sourceCodeLocation);
526     }
527 };
528
529 WebInspector.TimelineDataGrid.HasNonDefaultFilterStyleClassName = "has-non-default-filter";
530 WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout = 250;
531 WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout = 500;
532
533 WebInspector.TimelineDataGrid.Event = {
534     FiltersDidChange: "timelinedatagrid-filters-did-change"
535 };
536