2009-11-22 Pavel Feldman <pfeldman@chromium.org>
[WebKit-https.git] / WebCore / inspector / front-end / AbstractTimelinePanel.js
1 /*
2  * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3  * Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
4  * Copyright (C) 2009 Google Inc. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *
10  * 1.  Redistributions of source code must retain the above copyright
11  *     notice, this list of conditions and the following disclaimer.
12  * 2.  Redistributions in binary form must reproduce the above copyright
13  *     notice, this list of conditions and the following disclaimer in the
14  *     documentation and/or other materials provided with the distribution.
15  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16  *     its contributors may be used to endorse or promote products derived
17  *     from this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 WebInspector.AbstractTimelinePanel = function()
32 {
33     WebInspector.Panel.call(this);
34     this._items = [];
35     this._staleItems = [];
36 }
37
38 WebInspector.AbstractTimelinePanel.prototype = {
39     get categories()
40     {
41         // Should be implemented by the concrete subclasses.
42         return {};
43     },
44
45     populateSidebar: function()
46     {
47         // Should be implemented by the concrete subclasses.
48     },
49
50     createItemTreeElement: function(item)
51     {
52         // Should be implemented by the concrete subclasses.
53     },
54
55     createItemGraph: function(item)
56     {
57         // Should be implemented by the concrete subclasses.
58     },
59
60     get items()
61     {
62         return this._items;
63     },
64
65     createInterface: function()
66     {
67         this.containerElement = document.createElement("div");
68         this.containerElement.id = "resources-container";
69         this.containerElement.addEventListener("scroll", this._updateDividersLabelBarPosition.bind(this), false);
70         this.element.appendChild(this.containerElement);
71
72         this.createSidebar(this.containerElement, this.element);
73         this.sidebarElement.id = "resources-sidebar";
74         this.populateSidebar();
75
76         this._containerContentElement = document.createElement("div");
77         this._containerContentElement.id = "resources-container-content";
78         this.containerElement.appendChild(this._containerContentElement);
79
80         this.summaryBar = new WebInspector.SummaryBar(this.categories);
81         this.summaryBar.element.id = "resources-summary";
82         this._containerContentElement.appendChild(this.summaryBar.element);
83
84         this._timelineGrid = new WebInspector.TimelineGrid();
85         this._containerContentElement.appendChild(this._timelineGrid.element);
86         this.itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
87     },
88
89     createFilterPanel: function()
90     {
91         this.filterBarElement = document.createElement("div");
92         this.filterBarElement.id = "resources-filter";
93         this.filterBarElement.className = "scope-bar";
94         this.element.appendChild(this.filterBarElement);
95
96         function createFilterElement(category)
97         {
98             if (category === "all")
99                 var label = WebInspector.UIString("All");
100             else if (this.categories[category])
101                 var label = this.categories[category].title;
102
103             var categoryElement = document.createElement("li");
104             categoryElement.category = category;
105             categoryElement.addStyleClass(category);
106             categoryElement.appendChild(document.createTextNode(label));
107             categoryElement.addEventListener("click", this._updateFilter.bind(this), false);
108             this.filterBarElement.appendChild(categoryElement);
109
110             return categoryElement;
111         }
112
113         this.filterAllElement = createFilterElement.call(this, "all");
114
115         // Add a divider
116         var dividerElement = document.createElement("div");
117         dividerElement.addStyleClass("divider");
118         this.filterBarElement.appendChild(dividerElement);
119
120         for (var category in this.categories)
121             createFilterElement.call(this, category);
122     },
123
124     showCategory: function(category)
125     {
126         var filterClass = "filter-" + category.toLowerCase();
127         this.itemsGraphsElement.addStyleClass(filterClass);
128         this.itemsTreeElement.childrenListElement.addStyleClass(filterClass);
129     },
130
131     hideCategory: function(category)
132     {
133         var filterClass = "filter-" + category.toLowerCase();
134         this.itemsGraphsElement.removeStyleClass(filterClass);
135         this.itemsTreeElement.childrenListElement.removeStyleClass(filterClass);
136     },
137
138     filter: function(target, selectMultiple)
139     {
140         function unselectAll()
141         {
142             for (var i = 0; i < this.filterBarElement.childNodes.length; ++i) {
143                 var child = this.filterBarElement.childNodes[i];
144                 if (!child.category)
145                     continue;
146
147                 child.removeStyleClass("selected");
148                 this.hideCategory(child.category);
149             }
150         }
151
152         if (target === this.filterAllElement) {
153             if (target.hasStyleClass("selected")) {
154                 // We can't unselect All, so we break early here
155                 return;
156             }
157
158             // If All wasn't selected, and now is, unselect everything else.
159             unselectAll.call(this);
160         } else {
161             // Something other than All is being selected, so we want to unselect All.
162             if (this.filterAllElement.hasStyleClass("selected")) {
163                 this.filterAllElement.removeStyleClass("selected");
164                 this.hideCategory("all");
165             }
166         }
167
168         if (!selectMultiple) {
169             // If multiple selection is off, we want to unselect everything else
170             // and just select ourselves.
171             unselectAll.call(this);
172
173             target.addStyleClass("selected");
174             this.showCategory(target.category);
175             return;
176         }
177
178         if (target.hasStyleClass("selected")) {
179             // If selectMultiple is turned on, and we were selected, we just
180             // want to unselect ourselves.
181             target.removeStyleClass("selected");
182             this.hideCategory(target.category);
183         } else {
184             // If selectMultiple is turned on, and we weren't selected, we just
185             // want to select ourselves.
186             target.addStyleClass("selected");
187             this.showCategory(target.category);
188         }
189     },
190
191     _updateFilter: function(e)
192     {
193         var isMac = WebInspector.isMac();
194         var selectMultiple = false;
195         if (isMac && e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey)
196             selectMultiple = true;
197         if (!isMac && e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey)
198             selectMultiple = true;
199
200         this.filter(e.target, selectMultiple);
201
202         // When we are updating our filtering, scroll to the top so we don't end up
203         // in blank graph under all the resources.
204         this.containerElement.scrollTop = 0;
205     },
206
207     updateGraphDividersIfNeeded: function(force)
208     {
209         if (!this.visible) {
210             this.needsRefresh = true;
211             return false;
212         }
213         return this._timelineGrid.updateDividers(force, this.calculator);
214     },
215
216     _updateDividersLabelBarPosition: function()
217     {
218         var scrollTop = this.containerElement.scrollTop;
219         var dividersTop = (scrollTop < this.summaryBar.element.offsetHeight ? this.summaryBar.element.offsetHeight : scrollTop);
220         this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop);
221     },
222
223     get needsRefresh()
224     {
225         return this._needsRefresh;
226     },
227
228     set needsRefresh(x)
229     {
230         if (this._needsRefresh === x)
231             return;
232
233         this._needsRefresh = x;
234
235         if (x) {
236             if (this.visible && !("_refreshTimeout" in this))
237                 this._refreshTimeout = setTimeout(this.refresh.bind(this), 500);
238         } else {
239             if ("_refreshTimeout" in this) {
240                 clearTimeout(this._refreshTimeout);
241                 delete this._refreshTimeout;
242             }
243         }
244     },
245
246     refreshIfNeeded: function()
247     {
248         if (this.needsRefresh)
249             this.refresh();
250     },
251
252     show: function()
253     {
254         WebInspector.Panel.prototype.show.call(this);
255
256         this._updateDividersLabelBarPosition();
257         this.refreshIfNeeded();
258     },
259
260     resize: function()
261     {
262         this.updateGraphDividersIfNeeded();
263     },
264
265     updateMainViewWidth: function(width)
266     {
267         this._containerContentElement.style.left = width + "px";
268         this.updateGraphDividersIfNeeded();
269     },
270
271     invalidateAllItems: function()
272     {
273         this._staleItems = this._items.slice();
274     },
275
276     refresh: function()
277     {
278         this.needsRefresh = false;
279
280         var staleItemsLength = this._staleItems.length;
281
282         var boundariesChanged = false;
283
284         for (var i = 0; i < staleItemsLength; ++i) {
285             var item = this._staleItems[i];
286             if (!item._itemsTreeElement) {
287                 // Create the timeline tree element and graph.
288                 item._itemsTreeElement = this.createItemTreeElement(item);
289                 item._itemsTreeElement._itemGraph = this.createItemGraph(item);
290
291                 this.itemsTreeElement.appendChild(item._itemsTreeElement);
292                 this.itemsGraphsElement.appendChild(item._itemsTreeElement._itemGraph.graphElement);
293             }
294
295             if (item._itemsTreeElement.refresh)
296                 item._itemsTreeElement.refresh();
297
298             if (this.calculator.updateBoundaries(item))
299                 boundariesChanged = true;
300         }
301
302         if (boundariesChanged) {
303             // The boundaries changed, so all item graphs are stale.
304             this._staleItems = this._items.slice();
305             staleItemsLength = this._staleItems.length;
306         }
307
308         for (var i = 0; i < staleItemsLength; ++i)
309             this._staleItems[i]._itemsTreeElement._itemGraph.refresh(this.calculator);
310
311         this._staleItems = [];
312
313         this.updateGraphDividersIfNeeded();
314     },
315
316     reset: function()
317     {
318         this.containerElement.scrollTop = 0;
319
320         if (this._calculator)
321             this._calculator.reset();
322
323         if (this._items) {
324             var itemsLength = this._items.length;
325             for (var i = 0; i < itemsLength; ++i) {
326                 var item = this._items[i];
327                 delete item._itemsTreeElement;
328             }
329         }
330
331         this._items = [];
332         this._staleItems = [];
333
334         this.itemsTreeElement.removeChildren();
335         this.itemsGraphsElement.removeChildren();
336
337         this.updateGraphDividersIfNeeded(true);
338     },
339
340     get calculator()
341     {
342         return this._calculator;
343     },
344
345     set calculator(x)
346     {
347         if (!x || this._calculator === x)
348             return;
349
350         this._calculator = x;
351         this._calculator.reset();
352
353         this._staleItems = this._items.slice();
354         this.refresh();
355     },
356
357     addItem: function(item)
358     {
359         this._items.push(item);
360         this.refreshItem(item);
361     },
362
363     removeItem: function(item)
364     {
365         this._items.remove(item, true);
366
367         if (item._itemsTreeElement) {
368             this.itemsTreeElement.removeChild(item._itemsTreeElement);
369             this.itemsGraphsElement.removeChild(item._itemsTreeElement._itemGraph.graphElement);
370         }
371
372         delete item._itemsTreeElement;
373         this.adjustScrollPosition();
374     },
375
376     refreshItem: function(item)
377     {
378         this._staleItems.push(item);
379         this.needsRefresh = true;
380     },
381
382     revealAndSelectItem: function(item)
383     {
384         if (item._itemsTreeElement) {
385             item._itemsTreeElement.reveal();
386             item._itemsTreeElement.select(true);
387         }
388     },
389
390     sortItems: function(sortingFunction)
391     {
392         var sortedElements = [].concat(this.itemsTreeElement.children);
393         sortedElements.sort(sortingFunction);
394
395         var sortedElementsLength = sortedElements.length;
396         for (var i = 0; i < sortedElementsLength; ++i) {
397             var treeElement = sortedElements[i];
398             if (treeElement === this.itemsTreeElement.children[i])
399                 continue;
400
401             var wasSelected = treeElement.selected;
402             this.itemsTreeElement.removeChild(treeElement);
403             this.itemsTreeElement.insertChild(treeElement, i);
404             if (wasSelected)
405                 treeElement.select(true);
406
407             var graphElement = treeElement._itemGraph.graphElement;
408             this.itemsGraphsElement.insertBefore(graphElement, this.itemsGraphsElement.children[i]);
409         }
410     },
411
412     adjustScrollPosition: function()
413     {
414         // Prevent the container from being scrolled off the end.
415         if ((this.containerElement.scrollTop + this.containerElement.offsetHeight) > this.sidebarElement.offsetHeight)
416             this.containerElement.scrollTop = (this.sidebarElement.offsetHeight - this.containerElement.offsetHeight);
417     },
418
419     addEventDivider: function(divider)
420     {
421         this._timelineGrid.addEventDivider(divider);
422     }
423 }
424
425 WebInspector.AbstractTimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
426
427 WebInspector.AbstractTimelineCalculator = function()
428 {
429 }
430
431 WebInspector.AbstractTimelineCalculator.prototype = {
432     computeSummaryValues: function(items)
433     {
434         var total = 0;
435         var categoryValues = {};
436
437         var itemsLength = items.length;
438         for (var i = 0; i < itemsLength; ++i) {
439             var item = items[i];
440             var value = this._value(item);
441             if (typeof value === "undefined")
442                 continue;
443             if (!(item.category.name in categoryValues))
444                 categoryValues[item.category.name] = 0;
445             categoryValues[item.category.name] += value;
446             total += value;
447         }
448
449         return {categoryValues: categoryValues, total: total};
450     },
451
452     computeBarGraphPercentages: function(item)
453     {
454         return {start: 0, middle: 0, end: (this._value(item) / this.boundarySpan) * 100};
455     },
456
457     computeBarGraphLabels: function(item)
458     {
459         const label = this.formatValue(this._value(item));
460         return {left: label, right: label, tooltip: label};
461     },
462
463     get boundarySpan()
464     {
465         return this.maximumBoundary - this.minimumBoundary;
466     },
467
468     updateBoundaries: function(item)
469     {
470         this.minimumBoundary = 0;
471
472         var value = this._value(item);
473         if (typeof this.maximumBoundary === "undefined" || value > this.maximumBoundary) {
474             this.maximumBoundary = value;
475             return true;
476         }
477         return false;
478     },
479
480     reset: function()
481     {
482         delete this.minimumBoundary;
483         delete this.maximumBoundary;
484     },
485
486     _value: function(item)
487     {
488         return 0;
489     },
490
491     formatValue: function(value)
492     {
493         return value.toString();
494     }
495 }
496
497 WebInspector.AbstractTimelineCategory = function(name, title, color)
498 {
499     this.name = name;
500     this.title = title;
501     this.color = color;
502 }
503
504 WebInspector.AbstractTimelineCategory.prototype = {
505     toString: function()
506     {
507         return this.title;
508     }
509 }