Web Inspector: hook up grid row filtering in the new Timelines UI
[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, synchronizerDelegate, editCallback, deleteCallback)
29     {
30         super(columns, editCallback, deleteCallback);
31
32         if (treeOutline)
33             this._treeOutlineDataGridSynchronizer = new WebInspector.TreeOutlineDataGridSynchronizer(treeOutline, this, synchronizerDelegate);
34
35         this.element.classList.add("timeline");
36
37         this._sortDelegate = null;
38         this._scopeBarColumns = [];
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._scopeBarColumns.push(identifier);
48             scopeBar.columnIdentifier = identifier;
49             scopeBar.addEventListener(WebInspector.ScopeBar.Event.SelectionChanged, this._scopeBarSelectedItemsDidChange, this);
50         }
51
52         if (this._scopeBarColumns.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     addRowInSortOrder(treeElement, dataGridNode, parentTreeElementOrDataGridNode)
153     {
154         let parentDataGridNode;
155         let childElement = dataGridNode;
156
157         if (treeElement) {
158             console.assert(this._treeOutlineDataGridSynchronizer);
159             if (!this._treeOutlineDataGridSynchronizer)
160                 return;
161
162             this._treeOutlineDataGridSynchronizer.associate(treeElement, dataGridNode);
163
164             console.assert(!parentTreeElementOrDataGridNode || parentTreeElementOrDataGridNode instanceof WebInspector.TreeElement);
165
166             let parentTreeElement = parentTreeElementOrDataGridNode || this._treeOutlineDataGridSynchronizer.treeOutline;
167             parentDataGridNode = parentTreeElement.root ? this : this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(parentTreeElement);
168
169             parentTreeElementOrDataGridNode = parentTreeElement;
170             childElement = treeElement;
171         } else {
172             parentTreeElementOrDataGridNode = parentTreeElementOrDataGridNode || this;
173             parentDataGridNode = parentTreeElementOrDataGridNode;
174         }
175
176         if (this.sortColumnIdentifier) {
177             let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, parentDataGridNode.children, this._sortComparator.bind(this));
178
179             // If parent is a tree element, the synchronizer will insert into the data grid.
180             parentTreeElementOrDataGridNode.insertChild(childElement, insertionIndex);
181         } else {
182             // If parent is a tree element, the synchronizer will append to the data grid.
183             parentTreeElementOrDataGridNode.appendChild(childElement);
184         }
185     }
186
187     shouldIgnoreSelectionEvent()
188     {
189         return this._ignoreSelectionEvent || false;
190     }
191
192     // Protected
193
194     handleEvent(event)
195     {
196         console.assert(event.type === "resize");
197
198         this._windowResized(event);
199     }
200
201     dataGridNodeNeedsRefresh(dataGridNode)
202     {
203         if (!this._dirtyDataGridNodes)
204             this._dirtyDataGridNodes = new Set;
205         this._dirtyDataGridNodes.add(dataGridNode);
206
207         if (this._scheduledDataGridNodeRefreshIdentifier)
208             return;
209
210         this._scheduledDataGridNodeRefreshIdentifier = requestAnimationFrame(this._refreshDirtyDataGridNodes.bind(this));
211     }
212
213     hasCustomFilters()
214     {
215         return true;
216     }
217
218     matchNodeAgainstCustomFilters(node)
219     {
220         if (!super.matchNodeAgainstCustomFilters(node))
221             return false;
222
223         for (let identifier of this._scopeBarColumns) {
224             let scopeBar = this.columns.get(identifier).scopeBar;
225             if (!scopeBar || scopeBar.defaultItem.selected)
226                 continue;
227
228             let value = node.data[identifier];
229             if (!scopeBar.selectedItems.some((scopeBarItem) => scopeBarItem.value === value))
230                 return false;
231         }
232
233         return true;
234     }
235
236     // Private
237
238     _refreshDirtyDataGridNodes()
239     {
240         if (this._scheduledDataGridNodeRefreshIdentifier) {
241             cancelAnimationFrame(this._scheduledDataGridNodeRefreshIdentifier);
242             this._scheduledDataGridNodeRefreshIdentifier = undefined;
243         }
244
245         if (!this._dirtyDataGridNodes)
246             return;
247
248         let selectedNode = this.selectedNode;
249         let sortComparator = this._sortComparator.bind(this);
250
251         if (this._treeOutlineDataGridSynchronizer)
252             this._treeOutlineDataGridSynchronizer.enabled = false;
253
254         for (let dataGridNode of this._dirtyDataGridNodes) {
255             dataGridNode.refresh();
256
257             if (!this.sortColumnIdentifier)
258                 continue;
259
260             if (dataGridNode === selectedNode)
261                 this._ignoreSelectionEvent = true;
262
263             console.assert(!dataGridNode.parent || dataGridNode.parent === this);
264             if (dataGridNode.parent === this)
265                 this.removeChild(dataGridNode);
266
267             let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, this.children, sortComparator);
268             this.insertChild(dataGridNode, insertionIndex);
269
270             if (dataGridNode === selectedNode) {
271                 selectedNode.revealAndSelect();
272                 this._ignoreSelectionEvent = false;
273             }
274
275             if (!this._treeOutlineDataGridSynchronizer)
276                 continue;
277
278             let treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
279             let treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
280             console.assert(treeElement);
281
282             console.assert(!treeElement.parent || treeElement.parent === treeOutline);
283             if (treeElement.parent === treeOutline)
284                 treeOutline.removeChild(treeElement);
285
286             treeOutline.insertChild(treeElement, insertionIndex);
287
288             // Adding the tree element back to the tree outline subjects it to filters.
289             // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
290             dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
291         }
292
293         if (this._treeOutlineDataGridSynchronizer)
294             this._treeOutlineDataGridSynchronizer.enabled = true;
295
296         this._dirtyDataGridNodes = null;
297     }
298
299     _sort()
300     {
301         if (!this.children.length)
302             return;
303
304         let sortColumnIdentifier = this.sortColumnIdentifier;
305         if (!sortColumnIdentifier)
306             return;
307
308         let selectedNode = this.selectedNode;
309         this._ignoreSelectionEvent = true;
310
311         let treeOutline;
312         if (this._treeOutlineDataGridSynchronizer) {
313             this._treeOutlineDataGridSynchronizer.enabled = false;
314
315             treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
316             if (treeOutline.selectedTreeElement)
317                 treeOutline.selectedTreeElement.deselect(true);
318         }
319
320         // Collect parent nodes that need their children sorted. So this in two phases since
321         // traverseNextNode would get confused if we sort the tree while traversing it.
322         let parentDataGridNodes = [this];
323         let currentDataGridNode = this.children[0];
324         while (currentDataGridNode) {
325             if (currentDataGridNode.children.length)
326                 parentDataGridNodes.push(currentDataGridNode);
327             currentDataGridNode = currentDataGridNode.traverseNextNode(false, null, true);
328         }
329
330         // Sort the children of collected parent nodes.
331         for (let parentDataGridNode of parentDataGridNodes) {
332             let childDataGridNodes = parentDataGridNode.children.slice();
333             parentDataGridNode.removeChildren();
334
335             let parentTreeElement;
336             if (this._treeOutlineDataGridSynchronizer) {
337                 parentTreeElement = parentDataGridNode === this ? treeOutline : this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(parentDataGridNode);
338                 console.assert(parentTreeElement);
339
340                 parentTreeElement.removeChildren();
341             }
342
343             childDataGridNodes.sort(this._sortComparator.bind(this));
344
345             for (let dataGridNode of childDataGridNodes) {
346                 if (this._treeOutlineDataGridSynchronizer) {
347                     let treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
348                     console.assert(treeElement);
349
350                     if (parentTreeElement)
351                         parentTreeElement.appendChild(treeElement);
352
353                     // Adding the tree element back to the tree outline subjects it to filters.
354                     // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
355                     dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
356                 }
357
358                 parentDataGridNode.appendChild(dataGridNode);
359             }
360         }
361
362         if (this._treeOutlineDataGridSynchronizer)
363             this._treeOutlineDataGridSynchronizer.enabled = true;
364
365         if (selectedNode)
366             selectedNode.revealAndSelect();
367
368         this._ignoreSelectionEvent = false;
369     }
370
371     _sortComparator(node1, node2)
372     {
373         var sortColumnIdentifier = this.sortColumnIdentifier;
374         if (!sortColumnIdentifier)
375             return 0;
376
377         var sortDirection = this.sortOrder === WebInspector.DataGrid.SortOrder.Ascending ? 1 : -1;
378
379         if (this._sortDelegate && typeof this._sortDelegate.dataGridSortComparator === "function") {
380             let result = this._sortDelegate.dataGridSortComparator(sortColumnIdentifier, sortDirection, node1, node2);
381             if (typeof result === "number")
382                 return result;
383         }
384
385         var value1 = node1.data[sortColumnIdentifier];
386         var value2 = node2.data[sortColumnIdentifier];
387
388         if (typeof value1 === "number" && typeof value2 === "number") {
389             if (isNaN(value1) && isNaN(value2))
390                 return 0;
391             if (isNaN(value1))
392                 return -sortDirection;
393             if (isNaN(value2))
394                 return sortDirection;
395             return (value1 - value2) * sortDirection;
396         }
397
398         if (typeof value1 === "string" && typeof value2 === "string")
399             return value1.localeCompare(value2) * sortDirection;
400
401         if (value1 instanceof WebInspector.CallFrame || value2 instanceof WebInspector.CallFrame) {
402             // Sort by function name if available, then fall back to the source code object.
403             value1 = value1 && value1.functionName ? value1.functionName : (value1 && value1.sourceCodeLocation ? value1.sourceCodeLocation.sourceCode : "");
404             value2 = value2 && value2.functionName ? value2.functionName : (value2 && value2.sourceCodeLocation ? value2.sourceCodeLocation.sourceCode : "");
405         }
406
407         if (value1 instanceof WebInspector.SourceCode || value2 instanceof WebInspector.SourceCode) {
408             value1 = value1 ? value1.displayName || "" : "";
409             value2 = value2 ? value2.displayName || "" : "";
410         }
411
412         if (value1 instanceof WebInspector.SourceCodeLocation || value2 instanceof WebInspector.SourceCodeLocation) {
413             value1 = value1 ? value1.displayLocationString() || "" : "";
414             value2 = value2 ? value2.displayLocationString() || "" : "";
415         }
416
417         // For everything else (mostly booleans).
418         return (value1 < value2 ? -1 : (value1 > value2 ? 1 : 0)) * sortDirection;
419     }
420
421     _scopeBarSelectedItemsDidChange(event)
422     {
423         this.filterDidChange();
424     }
425
426     _dataGridSelectedNodeChanged(event)
427     {
428         if (!this.selectedNode) {
429             this._hidePopover();
430             return;
431         }
432
433         var record = this.selectedNode.record;
434         if (!record || !record.callFrames || !record.callFrames.length) {
435             this._hidePopover();
436             return;
437         }
438
439         this._showPopoverForSelectedNodeSoon();
440     }
441
442     _windowResized(event)
443     {
444         if (this._popover && this._popover.visible)
445             this._updatePopoverForSelectedNode(false);
446     }
447
448     _showPopoverForSelectedNodeSoon()
449     {
450         if (this._showPopoverTimeout)
451             return;
452
453         function delayedWork()
454         {
455             if (!this._popover)
456                 this._popover = new WebInspector.Popover;
457
458             this._updatePopoverForSelectedNode(true);
459         }
460
461         this._showPopoverTimeout = setTimeout(delayedWork.bind(this), WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout);
462     }
463
464     _hidePopover()
465     {
466         if (this._showPopoverTimeout) {
467             clearTimeout(this._showPopoverTimeout);
468             this._showPopoverTimeout = undefined;
469         }
470
471         if (this._popover)
472             this._popover.dismiss();
473
474         function delayedWork()
475         {
476             if (this._popoverCallStackTreeOutline)
477                 this._popoverCallStackTreeOutline.removeChildren();
478         }
479
480         if (this._hidePopoverContentClearTimeout)
481             clearTimeout(this._hidePopoverContentClearTimeout);
482         this._hidePopoverContentClearTimeout = setTimeout(delayedWork.bind(this), WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout);
483     }
484
485     _updatePopoverForSelectedNode(updateContent)
486     {
487         if (!this._popover || !this.selectedNode)
488             return;
489
490         var targetPopoverElement = this.callFramePopoverAnchorElement();
491         console.assert(targetPopoverElement, "TimelineDataGrid subclass should always return a valid element from callFramePopoverAnchorElement.");
492         if (!targetPopoverElement)
493             return;
494
495         var targetFrame = WebInspector.Rect.rectFromClientRect(targetPopoverElement.getBoundingClientRect());
496
497         // The element might be hidden if it does not have a width and height.
498         if (!targetFrame.size.width && !targetFrame.size.height)
499             return;
500
501         if (this._hidePopoverContentClearTimeout) {
502             clearTimeout(this._hidePopoverContentClearTimeout);
503             this._hidePopoverContentClearTimeout = undefined;
504         }
505
506         if (updateContent)
507             this._popover.content = this._createPopoverContent();
508
509         this._popover.present(targetFrame.pad(2), [WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MAX_X]);
510     }
511
512     _createPopoverContent()
513     {
514         if (!this._popoverCallStackTreeOutline) {
515             this._popoverCallStackTreeOutline = new WebInspector.TreeOutline;
516             this._popoverCallStackTreeOutline.disclosureButtons = false;
517             this._popoverCallStackTreeOutline.element.classList.add("timeline-data-grid");
518             this._popoverCallStackTreeOutline.addEventListener(WebInspector.TreeOutline.Event.SelectionDidChange, this._popoverCallStackTreeSelectionDidChange, this);
519         } else
520             this._popoverCallStackTreeOutline.removeChildren();
521
522         var callFrames = this.selectedNode.record.callFrames;
523         for (var i = 0; i < callFrames.length; ++i) {
524             var callFrameTreeElement = new WebInspector.CallFrameTreeElement(callFrames[i]);
525             this._popoverCallStackTreeOutline.appendChild(callFrameTreeElement);
526         }
527
528         var content = document.createElement("div");
529         content.className = "timeline-data-grid-popover";
530         content.appendChild(this._popoverCallStackTreeOutline.element);
531         return content;
532     }
533
534     _popoverCallStackTreeSelectionDidChange(event)
535     {
536         let treeElement = event.data.selectedElement;
537         if (!treeElement)
538             return;
539
540         this._popover.dismiss();
541
542         console.assert(treeElement instanceof WebInspector.CallFrameTreeElement, "TreeElements in TimelineDataGrid popover should always be CallFrameTreeElements");
543         var callFrame = treeElement.callFrame;
544         if (!callFrame.sourceCodeLocation)
545             return;
546
547         WebInspector.showSourceCodeLocation(callFrame.sourceCodeLocation);
548     }
549 };
550
551 WebInspector.TimelineDataGrid.WasExpandedDuringFilteringSymbol = Symbol("was-expanded-during-filtering");
552
553 WebInspector.TimelineDataGrid.HasNonDefaultFilterStyleClassName = "has-non-default-filter";
554 WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout = 250;
555 WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout = 500;