Web Inspector: Timelines UI redesign: use DataGridNode for TimelineView selection...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TimelineDataGridNode.js
1 /*
2  * Copyright (C) 2014, 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.TimelineDataGridNode = class TimelineDataGridNode extends WebInspector.DataGridNode
27 {
28     constructor(includesGraph, graphDataSource, hasChildren)
29     {
30         super({}, hasChildren);
31
32         this.copyable = false;
33
34         this._includesGraph = includesGraph || false;
35         this._graphDataSource = graphDataSource || null;
36
37         if (graphDataSource) {
38             this._graphContainerElement = document.createElement("div");
39             this._timelineRecordBars = [];
40         }
41     }
42
43     // Public
44
45     get record()
46     {
47         return this.records && this.records.length ? this.records[0] : null;;
48     }
49
50     get records()
51     {
52         // Implemented by subclasses.
53         return [];
54     }
55
56     get graphDataSource()
57     {
58         return this._graphDataSource;
59     }
60
61     get data()
62     {
63         if (!this._graphDataSource)
64             return {};
65
66         var records = this.records || [];
67         return {graph: records.length ? records[0].startTime : 0};
68     }
69
70     collapse()
71     {
72         super.collapse();
73
74         if (!this._graphDataSource || !this.revealed)
75             return;
76
77         // Refresh to show child bars in our graph now that we collapsed.
78         this.refreshGraph();
79     }
80
81     expand()
82     {
83         super.expand();
84
85         if (!this._graphDataSource || !this.revealed)
86             return;
87
88         // Refresh to remove child bars from our graph now that we expanded.
89         this.refreshGraph();
90
91         // Refresh child graphs since they haven't been updating while we were collapsed.
92         var childNode = this.children[0];
93         while (childNode) {
94             if (childNode instanceof WebInspector.TimelineDataGridNode)
95                 childNode.refreshGraph();
96             childNode = childNode.traverseNextNode(true, this);
97         }
98     }
99
100     createCellContent(columnIdentifier, cell)
101     {
102         if (columnIdentifier === "graph" && this._graphDataSource) {
103             this.needsGraphRefresh();
104             return this._graphContainerElement;
105         }
106
107         var value = this.data[columnIdentifier];
108         if (!value)
109             return emDash;
110
111         if (value instanceof WebInspector.SourceCodeLocation) {
112             if (value.sourceCode instanceof WebInspector.Resource) {
113                 cell.classList.add(WebInspector.ResourceTreeElement.ResourceIconStyleClassName);
114                 cell.classList.add(value.sourceCode.type);
115             } else if (value.sourceCode instanceof WebInspector.Script) {
116                 if (value.sourceCode.url) {
117                     cell.classList.add(WebInspector.ResourceTreeElement.ResourceIconStyleClassName);
118                     cell.classList.add(WebInspector.Resource.Type.Script);
119                 } else
120                     cell.classList.add(WebInspector.ScriptTreeElement.AnonymousScriptIconStyleClassName);
121             } else
122                 console.error("Unknown SourceCode subclass.");
123
124             // Give the whole cell a tooltip and keep it up to date.
125             value.populateLiveDisplayLocationTooltip(cell);
126
127             var fragment = document.createDocumentFragment();
128
129             var goToArrowButtonLink = WebInspector.createSourceCodeLocationLink(value, false, true);
130             fragment.appendChild(goToArrowButtonLink);
131
132             var titleElement = document.createElement("span");
133             value.populateLiveDisplayLocationString(titleElement, "textContent");
134             fragment.appendChild(titleElement);
135
136             return fragment;
137         }
138
139         if (value instanceof WebInspector.CallFrame) {
140             var callFrame = value;
141
142             var isAnonymousFunction = false;
143             var functionName = callFrame.functionName;
144             if (!functionName) {
145                 functionName = WebInspector.UIString("(anonymous function)");
146                 isAnonymousFunction = true;
147             }
148
149             cell.classList.add(WebInspector.CallFrameView.FunctionIconStyleClassName);
150
151             var fragment = document.createDocumentFragment();
152
153             if (callFrame.sourceCodeLocation && callFrame.sourceCodeLocation.sourceCode) {
154                 // Give the whole cell a tooltip and keep it up to date.
155                 callFrame.sourceCodeLocation.populateLiveDisplayLocationTooltip(cell);
156
157                 var goToArrowButtonLink = WebInspector.createSourceCodeLocationLink(callFrame.sourceCodeLocation, false, true);
158                 fragment.appendChild(goToArrowButtonLink);
159
160                 if (isAnonymousFunction) {
161                     // For anonymous functions we show the resource or script icon and name.
162                     if (callFrame.sourceCodeLocation.sourceCode instanceof WebInspector.Resource) {
163                         cell.classList.add(WebInspector.ResourceTreeElement.ResourceIconStyleClassName);
164                         cell.classList.add(callFrame.sourceCodeLocation.sourceCode.type);
165                     } else if (callFrame.sourceCodeLocation.sourceCode instanceof WebInspector.Script) {
166                         if (callFrame.sourceCodeLocation.sourceCode.url) {
167                             cell.classList.add(WebInspector.ResourceTreeElement.ResourceIconStyleClassName);
168                             cell.classList.add(WebInspector.Resource.Type.Script);
169                         } else
170                             cell.classList.add(WebInspector.ScriptTreeElement.AnonymousScriptIconStyleClassName);
171                     } else
172                         console.error("Unknown SourceCode subclass.");
173
174                     var titleElement = document.createElement("span");
175                     callFrame.sourceCodeLocation.populateLiveDisplayLocationString(titleElement, "textContent");
176
177                     fragment.appendChild(titleElement);
178                 } else {
179                     // Show the function name and icon.
180                     cell.classList.add(WebInspector.CallFrameView.FunctionIconStyleClassName);
181
182                     fragment.append(functionName);
183
184                     var subtitleElement = document.createElement("span");
185                     subtitleElement.classList.add("subtitle");
186                     callFrame.sourceCodeLocation.populateLiveDisplayLocationString(subtitleElement, "textContent");
187
188                     fragment.appendChild(subtitleElement);
189                 }
190
191                 return fragment;
192             }
193
194             var icon = document.createElement("div");
195             icon.classList.add("icon");
196
197             fragment.append(icon, functionName);
198
199             return fragment;
200         }
201
202         return super.createCellContent(columnIdentifier, cell);
203     }
204
205     refresh()
206     {
207         if (this._graphDataSource && this._includesGraph)
208             this.needsGraphRefresh();
209
210         super.refresh();
211     }
212
213     refreshGraph()
214     {
215         if (!this._graphDataSource)
216             return;
217
218         if (this._scheduledGraphRefreshIdentifier) {
219             cancelAnimationFrame(this._scheduledGraphRefreshIdentifier);
220             this._scheduledGraphRefreshIdentifier = undefined;
221         }
222
223         // We are not visible, but an ancestor will draw our graph.
224         // They need notified by using our needsGraphRefresh.
225         console.assert(this.revealed);
226         if (!this.revealed)
227             return;
228
229         var secondsPerPixel = this._graphDataSource.secondsPerPixel;
230         console.assert(isFinite(secondsPerPixel) && secondsPerPixel > 0);
231
232         var recordBarIndex = 0;
233
234         function createBar(records, renderMode)
235         {
236             var timelineRecordBar = this._timelineRecordBars[recordBarIndex];
237             if (!timelineRecordBar)
238                 timelineRecordBar = this._timelineRecordBars[recordBarIndex] = new WebInspector.TimelineRecordBar(records, renderMode);
239             else {
240                 timelineRecordBar.renderMode = renderMode;
241                 timelineRecordBar.records = records;
242             }
243             timelineRecordBar.refresh(this._graphDataSource);
244             if (!timelineRecordBar.element.parentNode)
245                 this._graphContainerElement.appendChild(timelineRecordBar.element);
246             ++recordBarIndex;
247         }
248
249         function collectRecordsByType(records, recordsByTypeMap)
250         {
251             for (var record of records) {
252                 var typedRecords = recordsByTypeMap.get(record.type);
253                 if (!typedRecords) {
254                     typedRecords = [];
255                     recordsByTypeMap.set(record.type, typedRecords);
256                 }
257
258                 typedRecords.push(record);
259             }
260         }
261
262         var boundCreateBar = createBar.bind(this);
263
264         if (this.expanded) {
265             // When expanded just use the records for this node.
266             WebInspector.TimelineRecordBar.createCombinedBars(this.records, secondsPerPixel, this._graphDataSource, boundCreateBar);
267         } else {
268             // When collapsed use the records for this node and its descendants.
269             // To share bars better, group records by type.
270
271             var recordTypeMap = new Map;
272             collectRecordsByType(this.records, recordTypeMap);
273
274             var childNode = this.children[0];
275             while (childNode) {
276                 if (childNode instanceof WebInspector.TimelineDataGridNode)
277                     collectRecordsByType(childNode.records, recordTypeMap);
278                 childNode = childNode.traverseNextNode(false, this);
279             }
280
281             for (var records of recordTypeMap.values())
282                 WebInspector.TimelineRecordBar.createCombinedBars(records, secondsPerPixel, this._graphDataSource, boundCreateBar);
283         }
284
285         // Remove the remaining unused TimelineRecordBars.
286         for (; recordBarIndex < this._timelineRecordBars.length; ++recordBarIndex) {
287             this._timelineRecordBars[recordBarIndex].records = null;
288             this._timelineRecordBars[recordBarIndex].element.remove();
289         }
290     }
291
292     needsGraphRefresh()
293     {
294         if (!this.revealed) {
295             // We are not visible, but an ancestor will be drawing our graph.
296             // Notify the next visible ancestor that their graph needs to refresh.
297             var ancestor = this;
298             while (ancestor && !ancestor.root) {
299                 if (ancestor.revealed && ancestor instanceof WebInspector.TimelineDataGridNode) {
300                     ancestor.needsGraphRefresh();
301                     return;
302                 }
303
304                 ancestor = ancestor.parent;
305             }
306
307             return;
308         }
309
310         if (!this._graphDataSource || this._scheduledGraphRefreshIdentifier)
311             return;
312
313         this._scheduledGraphRefreshIdentifier = requestAnimationFrame(this.refreshGraph.bind(this));
314     }
315
316     displayName()
317     {
318         // Can be overridden by subclasses.
319         return WebInspector.TimelineTabContentView.displayNameForRecord(this.record);
320     }
321
322     iconClassNames()
323     {
324         // Can be overridden by subclasses.
325         return [WebInspector.TimelineTabContentView.iconClassNameForRecord(this.record)];
326     }
327
328     // Protected
329
330     createGoToArrowButton(cellElement, callback)
331     {
332         function buttonClicked(event)
333         {
334             if (this.hidden || !this.revealed)
335                 return;
336
337             event.stopPropagation();
338
339             callback(this, cellElement.__columnIdentifier);
340         }
341
342         let button = WebInspector.createGoToArrowButton();
343         button.addEventListener("click", buttonClicked.bind(this));
344
345         let contentElement = cellElement.firstChild;
346         contentElement.appendChild(button);
347     }
348
349     isRecordVisible(record)
350     {
351         if (!this._graphDataSource)
352             return false;
353
354         if (isNaN(record.startTime))
355             return false;
356
357         // If this bar is completely before the bounds of the graph, not visible.
358         if (record.endTime < this.graphDataSource.startTime)
359             return false;
360
361         // If this record is completely after the current time or end time, not visible.
362         if (record.startTime > this.graphDataSource.currentTime || record.startTime > this.graphDataSource.endTime)
363             return false;
364
365         return true;
366     }
367 };