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