Web Inspector: DOM: provide a way to disable/breakpoint all event listeners for a...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TimelineDataGrid.js
1 /*
2  * Copyright (C) 2013-2018 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 WI.TimelineDataGrid = class TimelineDataGrid extends WI.DataGrid
27 {
28     constructor(columns)
29     {
30         super(columns);
31
32         this.element.classList.add("timeline");
33
34         this._sortDelegate = null;
35         this._scopeBarColumns = [];
36
37         // Check if any of the cells can be filtered.
38         for (var [identifier, column] of this.columns) {
39             var scopeBar = column.scopeBar;
40
41             if (!scopeBar)
42                 continue;
43
44             this._scopeBarColumns.push(identifier);
45             scopeBar.columnIdentifier = identifier;
46             scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectedItemsDidChange, this);
47         }
48
49         if (this._scopeBarColumns.length > 1) {
50             console.error("Creating a TimelineDataGrid with more than one filterable column is not yet supported.");
51             return;
52         }
53
54         this.addEventListener(WI.DataGrid.Event.SelectedNodeChanged, this._dataGridSelectedNodeChanged, this);
55         this.addEventListener(WI.DataGrid.Event.SortChanged, this._sort, this);
56
57         this.columnChooserEnabled = true;
58     }
59
60     static createColumnScopeBar(prefix, map)
61     {
62         prefix = prefix + "-timeline-data-grid-";
63
64         var scopeBarItems = [];
65         for (var [key, value] of map) {
66             var id = prefix + key;
67             var item = new WI.ScopeBarItem(id, value);
68             item.value = key;
69             scopeBarItems.push(item);
70         }
71
72         var allItem = new WI.ScopeBarItem(prefix + "type-all", WI.UIString("All"));
73         scopeBarItems.unshift(allItem);
74
75         return new WI.ScopeBar(prefix + "scope-bar", scopeBarItems, allItem, true);
76     }
77
78     // Public
79
80     get sortDelegate()
81     {
82         return this._sortDelegate;
83     }
84
85     set sortDelegate(delegate)
86     {
87         delegate = delegate || null;
88         if (this._sortDelegate === delegate)
89             return;
90
91         this._sortDelegate = delegate;
92
93         if (this.sortOrder !== WI.DataGrid.SortOrder.Indeterminate)
94             this.dispatchEventToListeners(WI.DataGrid.Event.SortChanged);
95     }
96
97     reset()
98     {
99         // May be overridden by subclasses. If so, they should call the superclass.
100
101         this.removeChildren();
102
103         this._hidePopover();
104     }
105
106     shown()
107     {
108         // May be overridden by subclasses. If so, they should call the superclass.
109     }
110
111     hidden()
112     {
113         // May be overridden by subclasses. If so, they should call the superclass.
114
115         this._hidePopover();
116     }
117
118     callFramePopoverAnchorElement()
119     {
120         // Implemented by subclasses.
121         return null;
122     }
123
124     shouldShowCallFramePopover()
125     {
126         // Implemented by subclasses.
127         return false;
128     }
129
130     addRowInSortOrder(dataGridNode, parentDataGridNode)
131     {
132         parentDataGridNode = parentDataGridNode || this;
133
134         if (this.sortColumnIdentifier) {
135             let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, parentDataGridNode.children, this._sortComparator.bind(this));
136             parentDataGridNode.insertChild(dataGridNode, insertionIndex);
137         } else
138             parentDataGridNode.appendChild(dataGridNode);
139     }
140
141     shouldIgnoreSelectionEvent()
142     {
143         return this._ignoreSelectionEvent || false;
144     }
145
146     // Protected
147
148     dataGridNodeNeedsRefresh(dataGridNode)
149     {
150         if (!this._dirtyDataGridNodes)
151             this._dirtyDataGridNodes = new Set;
152         this._dirtyDataGridNodes.add(dataGridNode);
153
154         if (this._scheduledDataGridNodeRefreshIdentifier)
155             return;
156
157         this._scheduledDataGridNodeRefreshIdentifier = requestAnimationFrame(this._refreshDirtyDataGridNodes.bind(this));
158     }
159
160     hasCustomFilters()
161     {
162         return true;
163     }
164
165     matchNodeAgainstCustomFilters(node)
166     {
167         if (!super.matchNodeAgainstCustomFilters(node))
168             return false;
169
170         for (let identifier of this._scopeBarColumns) {
171             let scopeBar = this.columns.get(identifier).scopeBar;
172             if (!scopeBar || scopeBar.defaultItem.selected)
173                 continue;
174
175             let value = node.data[identifier];
176             if (!scopeBar.selectedItems.some((scopeBarItem) => scopeBarItem.value === value))
177                 return false;
178         }
179
180         return true;
181     }
182
183     // Private
184
185     _refreshDirtyDataGridNodes()
186     {
187         if (this._scheduledDataGridNodeRefreshIdentifier) {
188             cancelAnimationFrame(this._scheduledDataGridNodeRefreshIdentifier);
189             this._scheduledDataGridNodeRefreshIdentifier = undefined;
190         }
191
192         if (!this._dirtyDataGridNodes)
193             return;
194
195         let selectedNode = this.selectedNode;
196         let sortComparator = this._sortComparator.bind(this);
197
198         for (let dataGridNode of this._dirtyDataGridNodes) {
199             dataGridNode.refresh();
200
201             if (!this.sortColumnIdentifier)
202                 continue;
203
204             if (dataGridNode === selectedNode)
205                 this._ignoreSelectionEvent = true;
206
207             console.assert(!dataGridNode.parent || dataGridNode.parent === this);
208             if (dataGridNode.parent === this)
209                 this.removeChild(dataGridNode);
210
211             let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, this.children, sortComparator);
212             this.insertChild(dataGridNode, insertionIndex);
213
214             if (dataGridNode === selectedNode) {
215                 selectedNode.revealAndSelect();
216                 this._ignoreSelectionEvent = false;
217             }
218         }
219
220         this._dirtyDataGridNodes = null;
221     }
222
223     _sort()
224     {
225         if (!this.children.length)
226             return;
227
228         let sortColumnIdentifier = this.sortColumnIdentifier;
229         if (!sortColumnIdentifier)
230             return;
231
232         let selectedNode = this.selectedNode;
233         this._ignoreSelectionEvent = true;
234
235         // Collect parent nodes that need their children sorted. So this in two phases since
236         // traverseNextNode would get confused if we sort the tree while traversing it.
237         let parentDataGridNodes = [this];
238         let currentDataGridNode = this.children[0];
239         while (currentDataGridNode) {
240             if (currentDataGridNode.children.length)
241                 parentDataGridNodes.push(currentDataGridNode);
242             currentDataGridNode = currentDataGridNode.traverseNextNode(false, null, true);
243         }
244
245         // Sort the children of collected parent nodes.
246         for (let parentDataGridNode of parentDataGridNodes) {
247             let childDataGridNodes = parentDataGridNode.children.slice();
248             parentDataGridNode.removeChildren();
249             childDataGridNodes.sort(this._sortComparator.bind(this));
250
251             for (let dataGridNode of childDataGridNodes)
252                 parentDataGridNode.appendChild(dataGridNode);
253         }
254
255         if (selectedNode)
256             selectedNode.revealAndSelect();
257
258         this._ignoreSelectionEvent = false;
259     }
260
261     _sortComparator(node1, node2)
262     {
263         var sortColumnIdentifier = this.sortColumnIdentifier;
264         if (!sortColumnIdentifier)
265             return 0;
266
267         var sortDirection = this.sortOrder === WI.DataGrid.SortOrder.Ascending ? 1 : -1;
268
269         if (this._sortDelegate && typeof this._sortDelegate.dataGridSortComparator === "function") {
270             let result = this._sortDelegate.dataGridSortComparator(sortColumnIdentifier, sortDirection, node1, node2);
271             if (typeof result === "number")
272                 return result;
273         }
274
275         var value1 = node1.data[sortColumnIdentifier];
276         var value2 = node2.data[sortColumnIdentifier];
277
278         if (typeof value1 === "number" && typeof value2 === "number") {
279             if (isNaN(value1) && isNaN(value2))
280                 return 0;
281             if (isNaN(value1))
282                 return -sortDirection;
283             if (isNaN(value2))
284                 return sortDirection;
285             return (value1 - value2) * sortDirection;
286         }
287
288         if (typeof value1 === "string" && typeof value2 === "string")
289             return value1.extendedLocaleCompare(value2) * sortDirection;
290
291         if (value1 instanceof WI.CallFrame || value2 instanceof WI.CallFrame) {
292             // Sort by function name if available, then fall back to the source code object.
293             value1 = value1 && value1.functionName ? value1.functionName : (value1 && value1.sourceCodeLocation ? value1.sourceCodeLocation.sourceCode : "");
294             value2 = value2 && value2.functionName ? value2.functionName : (value2 && value2.sourceCodeLocation ? value2.sourceCodeLocation.sourceCode : "");
295         }
296
297         if (value1 instanceof WI.SourceCode || value2 instanceof WI.SourceCode) {
298             value1 = value1 ? value1.displayName || "" : "";
299             value2 = value2 ? value2.displayName || "" : "";
300         }
301
302         if (value1 instanceof WI.SourceCodeLocation || value2 instanceof WI.SourceCodeLocation) {
303             value1 = value1 ? value1.displayLocationString() || "" : "";
304             value2 = value2 ? value2.displayLocationString() || "" : "";
305         }
306
307         // For everything else (mostly booleans).
308         return (value1 < value2 ? -1 : (value1 > value2 ? 1 : 0)) * sortDirection;
309     }
310
311     _scopeBarSelectedItemsDidChange(event)
312     {
313         this.filterDidChange();
314     }
315
316     _dataGridSelectedNodeChanged(event)
317     {
318         if (!this.selectedNode) {
319             this._hidePopover();
320             return;
321         }
322
323         var record = this.selectedNode.record;
324         if (!record || !record.callFrames || !record.callFrames.length) {
325             this._hidePopover();
326             return;
327         }
328
329         if (this.shouldShowCallFramePopover())
330             this._showPopoverForSelectedNodeSoon();
331     }
332
333     _showPopoverForSelectedNodeSoon()
334     {
335         if (this._showPopoverTimeout)
336             return;
337
338         this._showPopoverTimeout = setTimeout(() => {
339             if (!this._popover) {
340                 this._popover = new WI.Popover;
341                 this._popover.windowResizeHandler = () => { this._updatePopoverForSelectedNode(false); };
342             }
343             this._updatePopoverForSelectedNode(true);
344             this._showPopoverTimeout = undefined;
345         }, WI.TimelineDataGrid.DelayedPopoverShowTimeout);
346     }
347
348     _hidePopover()
349     {
350         if (this._showPopoverTimeout) {
351             clearTimeout(this._showPopoverTimeout);
352             this._showPopoverTimeout = undefined;
353         }
354
355         if (this._popover)
356             this._popover.dismiss();
357
358         if (this._hidePopoverContentClearTimeout)
359             clearTimeout(this._hidePopoverContentClearTimeout);
360
361         this._hidePopoverContentClearTimeout = setTimeout(() => {
362             if (this._popoverCallStackTreeOutline)
363                 this._popoverCallStackTreeOutline.removeChildren();
364         }, WI.TimelineDataGrid.DelayedPopoverHideContentClearTimeout);
365     }
366
367     _updatePopoverForSelectedNode(updateContent)
368     {
369         if (!this._popover || !this.selectedNode)
370             return;
371
372         let targetPopoverElement = this.callFramePopoverAnchorElement();
373         console.assert(targetPopoverElement, "TimelineDataGrid subclass should always return a valid element from callFramePopoverAnchorElement.");
374         if (!targetPopoverElement)
375             return;
376
377         // The element might be hidden if it does not have a width and height.
378         let rect = WI.Rect.rectFromClientRect(targetPopoverElement.getBoundingClientRect());
379         if (!rect.size.width && !rect.size.height)
380             return;
381
382         if (this._hidePopoverContentClearTimeout) {
383             clearTimeout(this._hidePopoverContentClearTimeout);
384             this._hidePopoverContentClearTimeout = undefined;
385         }
386
387         let targetFrame = rect.pad(2);
388         let preferredEdges = [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X];
389
390         if (updateContent)
391             this._popover.presentNewContentWithFrame(this._createPopoverContent(), targetFrame, preferredEdges);
392         else
393             this._popover.present(targetFrame, preferredEdges);
394     }
395
396     _createPopoverContent()
397     {
398         if (!this._popoverCallStackTreeOutline) {
399             this._popoverCallStackTreeOutline = new WI.TreeOutline;
400             this._popoverCallStackTreeOutline.disclosureButtons = false;
401             this._popoverCallStackTreeOutline.element.classList.add("timeline-data-grid");
402             this._popoverCallStackTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._popoverCallStackTreeSelectionDidChange, this);
403         } else
404             this._popoverCallStackTreeOutline.removeChildren();
405
406         var callFrames = this.selectedNode.record.callFrames;
407         for (var i = 0; i < callFrames.length; ++i) {
408             var callFrameTreeElement = new WI.CallFrameTreeElement(callFrames[i]);
409             this._popoverCallStackTreeOutline.appendChild(callFrameTreeElement);
410         }
411
412         let content = document.createElement("div");
413         content.appendChild(this._popoverCallStackTreeOutline.element);
414         return content;
415     }
416
417     _popoverCallStackTreeSelectionDidChange(event)
418     {
419         let treeElement = this._popoverCallStackTreeOutline.selectedTreeElement;
420         if (!treeElement)
421             return;
422
423         this._popover.dismiss();
424
425         console.assert(treeElement instanceof WI.CallFrameTreeElement, "TreeElements in TimelineDataGrid popover should always be CallFrameTreeElements");
426         var callFrame = treeElement.callFrame;
427         if (!callFrame.sourceCodeLocation)
428             return;
429
430         WI.showSourceCodeLocation(callFrame.sourceCodeLocation, {
431             ignoreNetworkTab: true,
432             ignoreSearchTab: true,
433         });
434     }
435 };
436
437 WI.TimelineDataGrid.HasNonDefaultFilterStyleClassName = "has-non-default-filter";
438 WI.TimelineDataGrid.DelayedPopoverShowTimeout = 250;
439 WI.TimelineDataGrid.DelayedPopoverHideContentClearTimeout = 500;