display:inline on the tbody is causing the width of the iframe to be shrunk to the...
[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
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)
230             return;
231
232         let selectedNode = this.selectedNode;
233         let sortComparator = this._sortComparator.bind(this);
234
235         if (this._treeOutlineDataGridSynchronizer)
236             this._treeOutlineDataGridSynchronizer.enabled = false;
237
238         for (let 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             console.assert(!dataGridNode.parent || dataGridNode.parent === this);
248             if (dataGridNode.parent === this)
249                 this.removeChild(dataGridNode);
250
251             let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, this.children, sortComparator);
252             this.insertChild(dataGridNode, insertionIndex);
253
254             if (dataGridNode === selectedNode) {
255                 selectedNode.revealAndSelect();
256                 this._ignoreSelectionEvent = false;
257             }
258
259             if (!this._treeOutlineDataGridSynchronizer)
260                 continue;
261
262             let treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
263             let treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
264             console.assert(treeElement);
265
266             console.assert(!treeElement.parent || treeElement.parent === treeOutline);
267             if (treeElement.parent === treeOutline)
268                 treeOutline.removeChild(treeElement);
269
270             treeOutline.insertChild(treeElement, insertionIndex);
271
272             // Adding the tree element back to the tree outline subjects it to filters.
273             // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
274             dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
275         }
276
277         if (this._treeOutlineDataGridSynchronizer)
278             this._treeOutlineDataGridSynchronizer.enabled = true;
279
280         this._dirtyDataGridNodes = null;
281     }
282
283     _sort()
284     {
285         let sortColumnIdentifier = this.sortColumnIdentifier;
286         if (!sortColumnIdentifier)
287             return;
288
289         let selectedNode = this.selectedNode;
290         this._ignoreSelectionEvent = true;
291
292         let treeOutline;
293         if (this._treeOutlineDataGridSynchronizer) {
294             this._treeOutlineDataGridSynchronizer.enabled = false;
295
296             treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
297             if (treeOutline.selectedTreeElement)
298                 treeOutline.selectedTreeElement.deselect(true);
299         }
300
301         // Collect parent nodes that need their children sorted. So this in two phases since
302         // traverseNextNode would get confused if we sort the tree while traversing it.
303         let parentDataGridNodes = [this];
304         let currentDataGridNode = this.children[0];
305         while (currentDataGridNode) {
306             if (currentDataGridNode.children.length)
307                 parentDataGridNodes.push(currentDataGridNode);
308             currentDataGridNode = currentDataGridNode.traverseNextNode(false, null, true);
309         }
310
311         // Sort the children of collected parent nodes.
312         for (let parentDataGridNode of parentDataGridNodes) {
313             let childDataGridNodes = parentDataGridNode.children.slice();
314             parentDataGridNode.removeChildren();
315
316             let parentTreeElement;
317             if (this._treeOutlineDataGridSynchronizer) {
318                 parentTreeElement = parentDataGridNode === this ? treeOutline : this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(parentDataGridNode);
319                 console.assert(parentTreeElement);
320
321                 parentTreeElement.removeChildren();
322             }
323
324             childDataGridNodes.sort(this._sortComparator.bind(this));
325
326             for (let dataGridNode of childDataGridNodes) {
327                 if (this._treeOutlineDataGridSynchronizer) {
328                     let treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
329                     console.assert(treeElement);
330
331                     if (parentTreeElement)
332                         parentTreeElement.appendChild(treeElement);
333
334                     // Adding the tree element back to the tree outline subjects it to filters.
335                     // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
336                     dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
337                 }
338
339                 parentDataGridNode.appendChild(dataGridNode);
340             }
341         }
342
343         if (this._treeOutlineDataGridSynchronizer)
344             this._treeOutlineDataGridSynchronizer.enabled = true;
345
346         if (selectedNode)
347             selectedNode.revealAndSelect();
348
349         this._ignoreSelectionEvent = false;
350     }
351
352     _sortComparator(node1, node2)
353     {
354         var sortColumnIdentifier = this.sortColumnIdentifier;
355         if (!sortColumnIdentifier)
356             return 0;
357
358         var sortDirection = this.sortOrder === WebInspector.DataGrid.SortOrder.Ascending ? 1 : -1;
359
360         var value1 = node1.data[sortColumnIdentifier];
361         var value2 = node2.data[sortColumnIdentifier];
362
363         if (typeof value1 === "number" && typeof value2 === "number") {
364             if (isNaN(value1) && isNaN(value2))
365                 return 0;
366             if (isNaN(value1))
367                 return -sortDirection;
368             if (isNaN(value2))
369                 return sortDirection;
370             return (value1 - value2) * sortDirection;
371         }
372
373         if (typeof value1 === "string" && typeof value2 === "string")
374             return value1.localeCompare(value2) * sortDirection;
375
376         if (value1 instanceof WebInspector.CallFrame || value2 instanceof WebInspector.CallFrame) {
377             // Sort by function name if available, then fall back to the source code object.
378             value1 = value1 && value1.functionName ? value1.functionName : (value1 && value1.sourceCodeLocation ? value1.sourceCodeLocation.sourceCode : "");
379             value2 = value2 && value2.functionName ? value2.functionName : (value2 && value2.sourceCodeLocation ? value2.sourceCodeLocation.sourceCode : "");
380         }
381
382         if (value1 instanceof WebInspector.SourceCode || value2 instanceof WebInspector.SourceCode) {
383             value1 = value1 ? value1.displayName || "" : "";
384             value2 = value2 ? value2.displayName || "" : "";
385         }
386
387         // For everything else (mostly booleans).
388         return (value1 < value2 ? -1 : (value1 > value2 ? 1 : 0)) * sortDirection;
389     }
390
391     _updateScopeBarForcedVisibility()
392     {
393         for (var identifier of this._filterableColumns) {
394             var scopeBar = this.columns.get(identifier).scopeBar;
395             if (scopeBar) {
396                 this.element.classList.toggle(WebInspector.TimelineDataGrid.HasNonDefaultFilterStyleClassName, scopeBar.hasNonDefaultItemSelected());
397                 break;
398             }
399         }
400     }
401
402     _scopeBarSelectedItemsDidChange(event)
403     {
404         this._updateScopeBarForcedVisibility();
405
406         var columnIdentifier = event.target.columnIdentifier;
407         this.dispatchEventToListeners(WebInspector.TimelineDataGrid.Event.FiltersDidChange, {columnIdentifier});
408     }
409
410     _dataGridSelectedNodeChanged(event)
411     {
412         if (!this.selectedNode) {
413             this._hidePopover();
414             return;
415         }
416
417         var record = this.selectedNode.record;
418         if (!record || !record.callFrames || !record.callFrames.length) {
419             this._hidePopover();
420             return;
421         }
422
423         this._showPopoverForSelectedNodeSoon();
424     }
425
426     _windowResized(event)
427     {
428         if (this._popover && this._popover.visible)
429             this._updatePopoverForSelectedNode(false);
430     }
431
432     _showPopoverForSelectedNodeSoon()
433     {
434         if (this._showPopoverTimeout)
435             return;
436
437         function delayedWork()
438         {
439             if (!this._popover)
440                 this._popover = new WebInspector.Popover;
441
442             this._updatePopoverForSelectedNode(true);
443         }
444
445         this._showPopoverTimeout = setTimeout(delayedWork.bind(this), WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout);
446     }
447
448     _hidePopover()
449     {
450         if (this._showPopoverTimeout) {
451             clearTimeout(this._showPopoverTimeout);
452             this._showPopoverTimeout = undefined;
453         }
454
455         if (this._popover)
456             this._popover.dismiss();
457
458         function delayedWork()
459         {
460             if (this._popoverCallStackTreeOutline)
461                 this._popoverCallStackTreeOutline.removeChildren();
462         }
463
464         if (this._hidePopoverContentClearTimeout)
465             clearTimeout(this._hidePopoverContentClearTimeout);
466         this._hidePopoverContentClearTimeout = setTimeout(delayedWork.bind(this), WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout);
467     }
468
469     _updatePopoverForSelectedNode(updateContent)
470     {
471         if (!this._popover || !this.selectedNode)
472             return;
473
474         var targetPopoverElement = this.callFramePopoverAnchorElement();
475         console.assert(targetPopoverElement, "TimelineDataGrid subclass should always return a valid element from callFramePopoverAnchorElement.");
476         if (!targetPopoverElement)
477             return;
478
479         var targetFrame = WebInspector.Rect.rectFromClientRect(targetPopoverElement.getBoundingClientRect());
480
481         // The element might be hidden if it does not have a width and height.
482         if (!targetFrame.size.width && !targetFrame.size.height)
483             return;
484
485         if (this._hidePopoverContentClearTimeout) {
486             clearTimeout(this._hidePopoverContentClearTimeout);
487             this._hidePopoverContentClearTimeout = undefined;
488         }
489
490         if (updateContent)
491             this._popover.content = this._createPopoverContent();
492
493         this._popover.present(targetFrame.pad(2), [WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MAX_X]);
494     }
495
496     _createPopoverContent()
497     {
498         if (!this._popoverCallStackTreeOutline) {
499             this._popoverCallStackTreeOutline = new WebInspector.TreeOutline;
500             this._popoverCallStackTreeOutline.disclosureButtons = false;
501             this._popoverCallStackTreeOutline.element.classList.add("timeline-data-grid");
502             this._popoverCallStackTreeOutline.addEventListener(WebInspector.TreeOutline.Event.SelectionDidChange, this._popoverCallStackTreeSelectionDidChange, this);
503         } else
504             this._popoverCallStackTreeOutline.removeChildren();
505
506         var callFrames = this.selectedNode.record.callFrames;
507         for (var i = 0; i < callFrames.length; ++i) {
508             var callFrameTreeElement = new WebInspector.CallFrameTreeElement(callFrames[i]);
509             this._popoverCallStackTreeOutline.appendChild(callFrameTreeElement);
510         }
511
512         var content = document.createElement("div");
513         content.className = "timeline-data-grid-popover";
514         content.appendChild(this._popoverCallStackTreeOutline.element);
515         return content;
516     }
517
518     _popoverCallStackTreeSelectionDidChange(event)
519     {
520         let treeElement = event.data.selectedElement;
521         if (!treeElement)
522             return;
523
524         this._popover.dismiss();
525
526         console.assert(treeElement instanceof WebInspector.CallFrameTreeElement, "TreeElements in TimelineDataGrid popover should always be CallFrameTreeElements");
527         var callFrame = treeElement.callFrame;
528         if (!callFrame.sourceCodeLocation)
529             return;
530
531         WebInspector.showSourceCodeLocation(callFrame.sourceCodeLocation);
532     }
533 };
534
535 WebInspector.TimelineDataGrid.HasNonDefaultFilterStyleClassName = "has-non-default-filter";
536 WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout = 250;
537 WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout = 500;
538
539 WebInspector.TimelineDataGrid.Event = {
540     FiltersDidChange: "timelinedatagrid-filters-did-change"
541 };
542