7f3e3eacef137bf35a4d92760120d04e8cfec4a9
[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(columns, treeOutline, 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         this._sortDelegate = null;
39
40         // Check if any of the cells can be filtered.
41         for (var [identifier, column] of this.columns) {
42             var scopeBar = column.scopeBar;
43
44             if (!scopeBar)
45                 continue;
46
47             this._filterableColumns.push(identifier);
48             scopeBar.columnIdentifier = identifier;
49             scopeBar.addEventListener(WebInspector.ScopeBar.Event.SelectionChanged, this._scopeBarSelectedItemsDidChange, this);
50         }
51
52         if (this._filterableColumns.length > 1) {
53             console.error("Creating a TimelineDataGrid with more than one filterable column is not yet supported.");
54             return;
55         }
56
57         this.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._dataGridSelectedNodeChanged, this);
58         this.addEventListener(WebInspector.DataGrid.Event.SortChanged, this._sort, this);
59
60         window.addEventListener("resize", this);
61     }
62
63     static createColumnScopeBar(prefix, map)
64     {
65         prefix = prefix + "-timeline-data-grid-";
66
67         var scopeBarItems = [];
68         for (var [key, value] of map) {
69             var id = prefix + key;
70             var item = new WebInspector.ScopeBarItem(id, value);
71             item.value = key;
72             scopeBarItems.push(item);
73         }
74
75         var allItem = new WebInspector.ScopeBarItem(prefix + "type-all", WebInspector.UIString("All"));
76         scopeBarItems.unshift(allItem);
77
78         return new WebInspector.ScopeBar(prefix + "scope-bar", scopeBarItems, allItem, true);
79     }
80
81     // Public
82
83     get sortDelegate()
84     {
85         return this._sortDelegate;
86     }
87
88     set sortDelegate(delegate)
89     {
90         delegate = delegate || null;
91         if (this._sortDelegate === delegate)
92             return;
93
94         this._sortDelegate = delegate;
95
96         if (this.sortOrder !== WebInspector.DataGrid.SortOrder.Indeterminate)
97             this.dispatchEventToListeners(WebInspector.DataGrid.Event.SortChanged);
98     }
99
100     reset()
101     {
102         // May be overridden by subclasses. If so, they should call the superclass.
103
104         if (!this._treeOutlineDataGridSynchronizer)
105             this.removeChildren();
106
107         this._hidePopover();
108     }
109
110     shown()
111     {
112         // May be overridden by subclasses. If so, they should call the superclass.
113
114         if (this._treeOutlineDataGridSynchronizer)
115             this._treeOutlineDataGridSynchronizer.synchronize();
116     }
117
118     hidden()
119     {
120         // May be overridden by subclasses. If so, they should call the superclass.
121
122         this._hidePopover();
123     }
124
125     closed()
126     {
127         window.removeEventListener("resize", this);
128     }
129
130     treeElementForDataGridNode(dataGridNode)
131     {
132         if (!this._treeOutlineDataGridSynchronizer)
133             return null;
134
135         return this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
136     }
137
138     dataGridNodeForTreeElement(treeElement)
139     {
140         if (!this._treeOutlineDataGridSynchronizer)
141             return null;
142
143         return this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(treeElement);
144     }
145
146     callFramePopoverAnchorElement()
147     {
148         // Implemented by subclasses.
149         return null;
150     }
151
152     treeElementMatchesActiveScopeFilters(treeElement)
153     {
154         if (!this._treeOutlineDataGridSynchronizer)
155             return false;
156
157         var dataGridNode = this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(treeElement);
158         console.assert(dataGridNode);
159
160         for (var identifier of this._filterableColumns) {
161             var scopeBar = this.columns.get(identifier).scopeBar;
162             if (!scopeBar || scopeBar.defaultItem.selected)
163                 continue;
164
165             var value = dataGridNode.data[identifier];
166             var matchesFilter = scopeBar.selectedItems.some(function(scopeBarItem) {
167                 return scopeBarItem.value === value;
168             });
169
170             if (!matchesFilter)
171                 return false;
172         }
173
174         return true;
175     }
176
177     addRowInSortOrder(treeElement, dataGridNode, parentTreeElementOrDataGridNode)
178     {
179         let parentDataGridNode;
180         let childElement = dataGridNode;
181
182         if (treeElement) {
183             console.assert(this._treeOutlineDataGridSynchronizer);
184             if (!this._treeOutlineDataGridSynchronizer)
185                 return;
186
187             this._treeOutlineDataGridSynchronizer.associate(treeElement, dataGridNode);
188
189             console.assert(!parentTreeElementOrDataGridNode || parentTreeElementOrDataGridNode instanceof WebInspector.TreeElement);
190
191             let parentTreeElement = parentTreeElementOrDataGridNode || this._treeOutlineDataGridSynchronizer.treeOutline;
192             parentDataGridNode = parentTreeElement.root ? this : this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(parentTreeElement);
193
194             parentTreeElementOrDataGridNode = parentTreeElement;
195             childElement = treeElement;
196         } else {
197             parentTreeElementOrDataGridNode = parentTreeElementOrDataGridNode || this;
198             parentDataGridNode = parentTreeElementOrDataGridNode;
199         }
200
201         if (this.sortColumnIdentifier) {
202             let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, parentDataGridNode.children, this._sortComparator.bind(this));
203
204             // If parent is a tree element, the synchronizer will insert into the data grid.
205             parentTreeElementOrDataGridNode.insertChild(childElement, insertionIndex);
206         } else {
207             // If parent is a tree element, the synchronizer will append to the data grid.
208             parentTreeElementOrDataGridNode.appendChild(childElement);
209         }
210     }
211
212     shouldIgnoreSelectionEvent()
213     {
214         return this._ignoreSelectionEvent || false;
215     }
216
217     // Protected
218
219     handleEvent(event)
220     {
221         console.assert(event.type === "resize");
222
223         this._windowResized(event);
224     }
225
226     dataGridNodeNeedsRefresh(dataGridNode)
227     {
228         if (!this._dirtyDataGridNodes)
229             this._dirtyDataGridNodes = new Set;
230         this._dirtyDataGridNodes.add(dataGridNode);
231
232         if (this._scheduledDataGridNodeRefreshIdentifier)
233             return;
234
235         this._scheduledDataGridNodeRefreshIdentifier = requestAnimationFrame(this._refreshDirtyDataGridNodes.bind(this));
236     }
237
238     // Private
239
240     _refreshDirtyDataGridNodes()
241     {
242         if (this._scheduledDataGridNodeRefreshIdentifier) {
243             cancelAnimationFrame(this._scheduledDataGridNodeRefreshIdentifier);
244             this._scheduledDataGridNodeRefreshIdentifier = undefined;
245         }
246
247         if (!this._dirtyDataGridNodes)
248             return;
249
250         let selectedNode = this.selectedNode;
251         let sortComparator = this._sortComparator.bind(this);
252
253         if (this._treeOutlineDataGridSynchronizer)
254             this._treeOutlineDataGridSynchronizer.enabled = false;
255
256         for (let dataGridNode of this._dirtyDataGridNodes) {
257             dataGridNode.refresh();
258
259             if (!this.sortColumnIdentifier)
260                 continue;
261
262             if (dataGridNode === selectedNode)
263                 this._ignoreSelectionEvent = true;
264
265             console.assert(!dataGridNode.parent || dataGridNode.parent === this);
266             if (dataGridNode.parent === this)
267                 this.removeChild(dataGridNode);
268
269             let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, this.children, sortComparator);
270             this.insertChild(dataGridNode, insertionIndex);
271
272             if (dataGridNode === selectedNode) {
273                 selectedNode.revealAndSelect();
274                 this._ignoreSelectionEvent = false;
275             }
276
277             if (!this._treeOutlineDataGridSynchronizer)
278                 continue;
279
280             let treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
281             let treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
282             console.assert(treeElement);
283
284             console.assert(!treeElement.parent || treeElement.parent === treeOutline);
285             if (treeElement.parent === treeOutline)
286                 treeOutline.removeChild(treeElement);
287
288             treeOutline.insertChild(treeElement, insertionIndex);
289
290             // Adding the tree element back to the tree outline subjects it to filters.
291             // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
292             dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
293         }
294
295         if (this._treeOutlineDataGridSynchronizer)
296             this._treeOutlineDataGridSynchronizer.enabled = true;
297
298         this._dirtyDataGridNodes = null;
299     }
300
301     _sort()
302     {
303         if (!this.children.length)
304             return;
305
306         let sortColumnIdentifier = this.sortColumnIdentifier;
307         if (!sortColumnIdentifier)
308             return;
309
310         let selectedNode = this.selectedNode;
311         this._ignoreSelectionEvent = true;
312
313         let treeOutline;
314         if (this._treeOutlineDataGridSynchronizer) {
315             this._treeOutlineDataGridSynchronizer.enabled = false;
316
317             treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
318             if (treeOutline.selectedTreeElement)
319                 treeOutline.selectedTreeElement.deselect(true);
320         }
321
322         // Collect parent nodes that need their children sorted. So this in two phases since
323         // traverseNextNode would get confused if we sort the tree while traversing it.
324         let parentDataGridNodes = [this];
325         let currentDataGridNode = this.children[0];
326         while (currentDataGridNode) {
327             if (currentDataGridNode.children.length)
328                 parentDataGridNodes.push(currentDataGridNode);
329             currentDataGridNode = currentDataGridNode.traverseNextNode(false, null, true);
330         }
331
332         // Sort the children of collected parent nodes.
333         for (let parentDataGridNode of parentDataGridNodes) {
334             let childDataGridNodes = parentDataGridNode.children.slice();
335             parentDataGridNode.removeChildren();
336
337             let parentTreeElement;
338             if (this._treeOutlineDataGridSynchronizer) {
339                 parentTreeElement = parentDataGridNode === this ? treeOutline : this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(parentDataGridNode);
340                 console.assert(parentTreeElement);
341
342                 parentTreeElement.removeChildren();
343             }
344
345             childDataGridNodes.sort(this._sortComparator.bind(this));
346
347             for (let dataGridNode of childDataGridNodes) {
348                 if (this._treeOutlineDataGridSynchronizer) {
349                     let treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
350                     console.assert(treeElement);
351
352                     if (parentTreeElement)
353                         parentTreeElement.appendChild(treeElement);
354
355                     // Adding the tree element back to the tree outline subjects it to filters.
356                     // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
357                     dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
358                 }
359
360                 parentDataGridNode.appendChild(dataGridNode);
361             }
362         }
363
364         if (this._treeOutlineDataGridSynchronizer)
365             this._treeOutlineDataGridSynchronizer.enabled = true;
366
367         if (selectedNode)
368             selectedNode.revealAndSelect();
369
370         this._ignoreSelectionEvent = false;
371     }
372
373     _sortComparator(node1, node2)
374     {
375         var sortColumnIdentifier = this.sortColumnIdentifier;
376         if (!sortColumnIdentifier)
377             return 0;
378
379         var sortDirection = this.sortOrder === WebInspector.DataGrid.SortOrder.Ascending ? 1 : -1;
380
381         if (this._sortDelegate && typeof this._sortDelegate.dataGridSortComparator === "function") {
382             let result = this._sortDelegate.dataGridSortComparator(sortColumnIdentifier, sortDirection, node1, node2);
383             if (typeof result === "number")
384                 return result;
385         }
386
387         var value1 = node1.data[sortColumnIdentifier];
388         var value2 = node2.data[sortColumnIdentifier];
389
390         if (typeof value1 === "number" && typeof value2 === "number") {
391             if (isNaN(value1) && isNaN(value2))
392                 return 0;
393             if (isNaN(value1))
394                 return -sortDirection;
395             if (isNaN(value2))
396                 return sortDirection;
397             return (value1 - value2) * sortDirection;
398         }
399
400         if (typeof value1 === "string" && typeof value2 === "string")
401             return value1.localeCompare(value2) * sortDirection;
402
403         if (value1 instanceof WebInspector.CallFrame || value2 instanceof WebInspector.CallFrame) {
404             // Sort by function name if available, then fall back to the source code object.
405             value1 = value1 && value1.functionName ? value1.functionName : (value1 && value1.sourceCodeLocation ? value1.sourceCodeLocation.sourceCode : "");
406             value2 = value2 && value2.functionName ? value2.functionName : (value2 && value2.sourceCodeLocation ? value2.sourceCodeLocation.sourceCode : "");
407         }
408
409         if (value1 instanceof WebInspector.SourceCode || value2 instanceof WebInspector.SourceCode) {
410             value1 = value1 ? value1.displayName || "" : "";
411             value2 = value2 ? value2.displayName || "" : "";
412         }
413
414         if (value1 instanceof WebInspector.SourceCodeLocation || value2 instanceof WebInspector.SourceCodeLocation) {
415             value1 = value1 ? value1.displayLocationString() || "" : "";
416             value2 = value2 ? value2.displayLocationString() || "" : "";
417         }
418
419         // For everything else (mostly booleans).
420         return (value1 < value2 ? -1 : (value1 > value2 ? 1 : 0)) * sortDirection;
421     }
422
423     _updateScopeBarForcedVisibility()
424     {
425         for (var identifier of this._filterableColumns) {
426             var scopeBar = this.columns.get(identifier).scopeBar;
427             if (scopeBar) {
428                 this.element.classList.toggle(WebInspector.TimelineDataGrid.HasNonDefaultFilterStyleClassName, scopeBar.hasNonDefaultItemSelected());
429                 break;
430             }
431         }
432     }
433
434     _scopeBarSelectedItemsDidChange(event)
435     {
436         this._updateScopeBarForcedVisibility();
437
438         var columnIdentifier = event.target.columnIdentifier;
439         this.dispatchEventToListeners(WebInspector.TimelineDataGrid.Event.FiltersDidChange, {columnIdentifier});
440     }
441
442     _dataGridSelectedNodeChanged(event)
443     {
444         if (!this.selectedNode) {
445             this._hidePopover();
446             return;
447         }
448
449         var record = this.selectedNode.record;
450         if (!record || !record.callFrames || !record.callFrames.length) {
451             this._hidePopover();
452             return;
453         }
454
455         this._showPopoverForSelectedNodeSoon();
456     }
457
458     _windowResized(event)
459     {
460         if (this._popover && this._popover.visible)
461             this._updatePopoverForSelectedNode(false);
462     }
463
464     _showPopoverForSelectedNodeSoon()
465     {
466         if (this._showPopoverTimeout)
467             return;
468
469         function delayedWork()
470         {
471             if (!this._popover)
472                 this._popover = new WebInspector.Popover;
473
474             this._updatePopoverForSelectedNode(true);
475         }
476
477         this._showPopoverTimeout = setTimeout(delayedWork.bind(this), WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout);
478     }
479
480     _hidePopover()
481     {
482         if (this._showPopoverTimeout) {
483             clearTimeout(this._showPopoverTimeout);
484             this._showPopoverTimeout = undefined;
485         }
486
487         if (this._popover)
488             this._popover.dismiss();
489
490         function delayedWork()
491         {
492             if (this._popoverCallStackTreeOutline)
493                 this._popoverCallStackTreeOutline.removeChildren();
494         }
495
496         if (this._hidePopoverContentClearTimeout)
497             clearTimeout(this._hidePopoverContentClearTimeout);
498         this._hidePopoverContentClearTimeout = setTimeout(delayedWork.bind(this), WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout);
499     }
500
501     _updatePopoverForSelectedNode(updateContent)
502     {
503         if (!this._popover || !this.selectedNode)
504             return;
505
506         var targetPopoverElement = this.callFramePopoverAnchorElement();
507         console.assert(targetPopoverElement, "TimelineDataGrid subclass should always return a valid element from callFramePopoverAnchorElement.");
508         if (!targetPopoverElement)
509             return;
510
511         var targetFrame = WebInspector.Rect.rectFromClientRect(targetPopoverElement.getBoundingClientRect());
512
513         // The element might be hidden if it does not have a width and height.
514         if (!targetFrame.size.width && !targetFrame.size.height)
515             return;
516
517         if (this._hidePopoverContentClearTimeout) {
518             clearTimeout(this._hidePopoverContentClearTimeout);
519             this._hidePopoverContentClearTimeout = undefined;
520         }
521
522         if (updateContent)
523             this._popover.content = this._createPopoverContent();
524
525         this._popover.present(targetFrame.pad(2), [WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MAX_X]);
526     }
527
528     _createPopoverContent()
529     {
530         if (!this._popoverCallStackTreeOutline) {
531             this._popoverCallStackTreeOutline = new WebInspector.TreeOutline;
532             this._popoverCallStackTreeOutline.disclosureButtons = false;
533             this._popoverCallStackTreeOutline.element.classList.add("timeline-data-grid");
534             this._popoverCallStackTreeOutline.addEventListener(WebInspector.TreeOutline.Event.SelectionDidChange, this._popoverCallStackTreeSelectionDidChange, this);
535         } else
536             this._popoverCallStackTreeOutline.removeChildren();
537
538         var callFrames = this.selectedNode.record.callFrames;
539         for (var i = 0; i < callFrames.length; ++i) {
540             var callFrameTreeElement = new WebInspector.CallFrameTreeElement(callFrames[i]);
541             this._popoverCallStackTreeOutline.appendChild(callFrameTreeElement);
542         }
543
544         var content = document.createElement("div");
545         content.className = "timeline-data-grid-popover";
546         content.appendChild(this._popoverCallStackTreeOutline.element);
547         return content;
548     }
549
550     _popoverCallStackTreeSelectionDidChange(event)
551     {
552         let treeElement = event.data.selectedElement;
553         if (!treeElement)
554             return;
555
556         this._popover.dismiss();
557
558         console.assert(treeElement instanceof WebInspector.CallFrameTreeElement, "TreeElements in TimelineDataGrid popover should always be CallFrameTreeElements");
559         var callFrame = treeElement.callFrame;
560         if (!callFrame.sourceCodeLocation)
561             return;
562
563         WebInspector.showSourceCodeLocation(callFrame.sourceCodeLocation);
564     }
565 };
566
567 WebInspector.TimelineDataGrid.HasNonDefaultFilterStyleClassName = "has-non-default-filter";
568 WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout = 250;
569 WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout = 500;
570
571 WebInspector.TimelineDataGrid.Event = {
572     FiltersDidChange: "timelinedatagrid-filters-did-change"
573 };
574