2009-11-22 Pavel Feldman <pfeldman@chromium.org>
[WebKit-https.git] / WebCore / inspector / front-end / TimelineOverviewPane.js
1 /*
2  * Copyright (C) 2009 Google 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 are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google Inc. nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 WebInspector.TimelineOverviewPane = function(categories)
32 {
33     this.element = document.createElement("div");
34     this.element.id = "timeline-overview-panel";
35
36     this._categories = categories;
37     this._overviewSidebarElement = document.createElement("div");
38     this._overviewSidebarElement.id = "timeline-overview-sidebar";
39     this.element.appendChild(this._overviewSidebarElement);
40
41     var overviewTreeElement = document.createElement("ol");
42     overviewTreeElement.className = "sidebar-tree";
43     this._overviewSidebarElement.appendChild(overviewTreeElement);
44     var sidebarTree = new TreeOutline(overviewTreeElement);
45
46     var categoriesTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("TIMELINES"), {}, true);
47     categoriesTreeElement.expanded = true;
48     sidebarTree.appendChild(categoriesTreeElement);
49     for (var categoryName in this._categories) {
50         var category = this._categories[categoryName];
51         categoriesTreeElement.appendChild(new WebInspector.TimelineCategoryTreeElement(category, this._onCheckboxClicked.bind(this, category)));
52     }
53
54     this._overviewGrid = new WebInspector.TimelineGrid();
55     this._overviewGrid.element.id = "timeline-overview-grid";
56     this._overviewGrid.itemsGraphsElement.id = "timeline-overview-graphs";
57     this.element.appendChild(this._overviewGrid.element);
58
59     this._categoryGraphs = {};
60     var i = 0;
61     for (var category in this._categories) {
62         var categoryGraph = new WebInspector.TimelineCategoryGraph(this._categories[category], i++ % 2);
63         this._categoryGraphs[category] = categoryGraph;
64         this._overviewGrid.itemsGraphsElement.appendChild(categoryGraph.graphElement);
65     }
66     this._overviewGrid.setScrollAndDividerTop(0, 0);
67
68     this._overviewWindowElement = document.createElement("div");
69     this._overviewWindowElement.id = "timeline-overview-window";
70     this._overviewWindowElement.addEventListener("mousedown", this._dragWindow.bind(this), false);
71     this._overviewGrid.element.appendChild(this._overviewWindowElement);
72
73     this._leftResizeElement = document.createElement("div");
74     this._leftResizeElement.className = "timeline-window-resizer";
75     this._leftResizeElement.style.left = 0;
76     this._overviewGrid.element.appendChild(this._leftResizeElement);
77     this._leftResizeElement.addEventListener("mousedown", this._resizeWindow.bind(this, this._leftResizeElement), false);
78
79     this._rightResizeElement = document.createElement("div");
80     this._rightResizeElement.className = "timeline-window-resizer timeline-window-resizer-right";
81     this._rightResizeElement.style.right = 0;
82     this._overviewGrid.element.appendChild(this._rightResizeElement);
83     this._rightResizeElement.addEventListener("mousedown", this._resizeWindow.bind(this, this._rightResizeElement), false);
84
85     this._overviewCalculator = new WebInspector.TimelineOverviewCalculator();
86
87     var separatorElement = document.createElement("div");
88     separatorElement.id = "timeline-overview-separator";
89     this.element.appendChild(separatorElement);
90
91     this.windowLeft = 0.0;
92     this.windowRight = 1.0;
93 }
94
95
96 WebInspector.TimelineOverviewPane.prototype = {
97     _onCheckboxClicked: function (category, event) {
98         if (event.target.checked)
99             category.hidden = false;
100         else
101             category.hidden = true;
102         this._categoryGraphs[category.name].dimmed = !event.target.checked;
103         this.dispatchEventToListeners("filter changed");
104     },
105
106     update: function(records)
107     {
108         // Clear summary bars.
109         var timelines = {};
110         for (var category in this._categories) {
111             timelines[category] = [];
112             this._categoryGraphs[category].clearChunks();
113         }
114
115         // Create sparse arrays with 101 cells each to fill with chunks for a given category.
116         this._overviewCalculator.reset();
117
118         for (var i = 1; i < records.length; ++i)
119             this._overviewCalculator.updateBoundaries(records[i]);
120
121         for (var i = 0; i < records.length; ++i) {
122             var record = records[i];
123             var percentages = this._overviewCalculator.computeBarGraphPercentages(record);
124             
125             var end = Math.round(percentages.end);
126             var categoryName = record.category.name;
127             for (var j = Math.round(percentages.start); j <= end; ++j)
128                 timelines[categoryName][j] = true;
129         }
130
131         // Convert sparse arrays to continuous segments, render graphs for each.
132         for (var category in this._categories) {
133             var timeline = timelines[category];
134             window.timelineSaved = timeline;
135             var chunkStart = -1;
136             for (var j = 0; j < 101; ++j) {
137                 if (timeline[j]) {
138                     if (chunkStart === -1)
139                         chunkStart = j;
140                 } else {
141                     if (chunkStart !== -1) {
142                         this._categoryGraphs[category].addChunk(chunkStart, j);
143                         chunkStart = -1;
144                     }
145                 }
146             }
147             if (chunkStart !== -1) {
148                 this._categoryGraphs[category].addChunk(chunkStart, 100);
149                 chunkStart = -1;
150             }
151         }
152         this._overviewGrid.updateDividers(true, this._overviewCalculator);
153     },
154
155     setSidebarWidth: function(width)
156     {
157         this._overviewSidebarElement.style.width = width + "px";
158     },
159
160     updateMainViewWidth: function(width)
161     {
162         this._overviewGrid.element.style.left = width + "px";
163     },
164
165     reset: function()
166     {
167         this._overviewCalculator.reset();
168         this._overviewGrid.updateDividers(true, this._overviewCalculator);
169         this.windowLeft = 0.0;
170         this.windowRight = 1.0;
171     },
172
173     _resizeWindow: function(resizeElement, event)
174     {
175         WebInspector.elementDragStart(resizeElement, this._windowResizeDragging.bind(this, resizeElement), this._endWindowDragging.bind(this), event, "col-resize");
176     },
177
178     _windowResizeDragging: function(resizeElement, event)
179     {
180         if (resizeElement === this._leftResizeElement)
181             this._resizeWindowLeft(event.pageX - this._overviewGrid.element.offsetLeft);
182         else
183             this._resizeWindowRight(event.pageX - this._overviewGrid.element.offsetLeft);
184         event.preventDefault();
185     },
186
187     _dragWindow: function(event)
188     {
189         WebInspector.elementDragStart(this._overviewWindowElement, this._windowDragging.bind(this, event.pageX,
190             this._leftResizeElement.offsetLeft, this._rightResizeElement.offsetLeft), this._endWindowDragging.bind(this), event, "ew-resize");
191     },
192
193     _windowDragging: function(startX, windowLeft, windowRight, event)
194     {
195         var delta = event.pageX - startX;
196         var start = windowLeft + delta;
197         var end = windowRight + delta;
198         var windowSize = windowRight - windowLeft;
199
200         if (start < 0) {
201             start = 0;
202             end = windowSize;
203         }
204
205         if (end > this._overviewGrid.element.clientWidth) {
206             end = this._overviewGrid.element.clientWidth;
207             start = end - windowSize;
208         }
209         this._setWindowPosition(start, end);
210
211         event.preventDefault();
212     },
213
214     _resizeWindowLeft: function(start)
215     {
216         // Glue to edge.
217         if (start < 10)
218             start = 0;
219         this._setWindowPosition(start, null);
220     },
221
222     _resizeWindowRight: function(end)
223     {
224         // Glue to edge.
225         if (end > this._overviewGrid.element.clientWidth - 10)
226             end = this._overviewGrid.element.clientWidth;
227         this._setWindowPosition(null, end);
228     },
229
230     _setWindowPosition: function(start, end)
231     {
232         if (typeof start === "number") {
233             if (start > this._rightResizeElement.offsetLeft - 25)
234                 start = this._rightResizeElement.offsetLeft - 25;
235
236             this.windowLeft = start / this._overviewGrid.element.clientWidth;
237             this._leftResizeElement.style.left = this.windowLeft * 100 + "%";
238             this._overviewWindowElement.style.left = this.windowLeft * 100 + "%";
239         }
240         if (typeof end === "number") {
241             if (end < this._leftResizeElement.offsetLeft + 30)
242                 end = this._leftResizeElement.offsetLeft + 30;
243
244             this.windowRight = end / this._overviewGrid.element.clientWidth;
245             this._rightResizeElement.style.left = this.windowRight * 100 + "%";
246         }
247         this._overviewWindowElement.style.width = (this.windowRight - this.windowLeft) * 100 + "%";
248         this.dispatchEventToListeners("window changed");
249     },
250
251     _endWindowDragging: function(event)
252     {
253         WebInspector.elementDragEnd(event);
254     }
255 }
256
257 WebInspector.TimelineOverviewPane.prototype.__proto__ = WebInspector.Object.prototype;
258
259
260 WebInspector.TimelineOverviewCalculator = function()
261 {
262     this._uiString = WebInspector.UIString.bind(WebInspector);
263 }
264
265 WebInspector.TimelineOverviewCalculator.prototype = {
266     computeBarGraphPercentages: function(record)
267     {
268         var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100;
269         var end = (record.endTime - this.minimumBoundary) / this.boundarySpan * 100;
270         return {start: start, end: end};
271     },
272
273     reset: function()
274     {
275         delete this.minimumBoundary;
276         delete this.maximumBoundary;
277     },
278
279     updateBoundaries: function(record)
280     {
281         if (typeof this.minimumBoundary === "undefined" || record.startTime < this.minimumBoundary) {
282             this.minimumBoundary = record.startTime;
283             return true;
284         }
285         if (typeof this.maximumBoundary === "undefined" || record.endTime > this.maximumBoundary) {
286             this.maximumBoundary = record.endTime;
287             return true;
288         }
289         return false;
290     },
291
292     get boundarySpan()
293     {
294         return this.maximumBoundary - this.minimumBoundary;
295     },
296
297     formatValue: function(value)
298     {
299         return Number.secondsToString(value, this._uiString);
300     }
301 }
302
303
304 WebInspector.TimelineCategoryTreeElement = function(category, onCheckboxClicked)
305 {
306     this._category = category;
307     this._onCheckboxClicked = onCheckboxClicked;
308     // Pass an empty title, the title gets made later in onattach.
309     TreeElement.call(this, "", null, false);
310 }
311
312 WebInspector.TimelineCategoryTreeElement.prototype = {
313     onattach: function()
314     {
315         this.listItemElement.removeChildren();
316         this.listItemElement.addStyleClass("timeline-category-tree-item");
317         this.listItemElement.addStyleClass("timeline-category-" + this._category.name);
318
319         var label = document.createElement("label");
320
321         var checkElement = document.createElement("input");
322         checkElement.type = "checkbox";
323         checkElement.className = "timeline-category-checkbox";
324         checkElement.checked = true;
325         checkElement.addEventListener("click", this._onCheckboxClicked);
326         label.appendChild(checkElement);
327
328         var typeElement = document.createElement("span");
329         typeElement.className = "type";
330         typeElement.textContent = this._category.title;
331         label.appendChild(typeElement);
332
333         this.listItemElement.appendChild(label);
334     }
335 }
336
337 WebInspector.TimelineCategoryTreeElement.prototype.__proto__ = TreeElement.prototype;
338
339 WebInspector.TimelineCategoryGraph = function(category, isEven)
340 {
341     this._category = category;
342
343     this._graphElement = document.createElement("div");
344     this._graphElement.className = "timeline-graph-side timeline-overview-graph-side" + (isEven ? " even" : "");
345
346     this._barAreaElement = document.createElement("div");
347     this._barAreaElement.className = "timeline-graph-bar-area timeline-category-" + category.name;
348     this._graphElement.appendChild(this._barAreaElement);
349 }
350
351 WebInspector.TimelineCategoryGraph.prototype = {
352     get graphElement()
353     {
354         return this._graphElement;
355     },
356
357     addChunk: function(start, end)
358     {
359         var chunk = document.createElement("div");
360         chunk.className = "timeline-graph-bar";
361         this._barAreaElement.appendChild(chunk);
362         chunk.style.setProperty("left", start + "%");
363         chunk.style.setProperty("width", (end - start) + "%");
364     },
365
366     clearChunks: function()
367     {
368         this._barAreaElement.removeChildren();
369     },
370
371     set dimmed(dimmed)
372     {
373         if (dimmed)
374             this._barAreaElement.removeStyleClass("timeline-category-" + this._category.name);
375         else
376             this._barAreaElement.addStyleClass("timeline-category-" + this._category.name);
377     }
378 }