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 31WebInspector.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 96WebInspector.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 function forAllRecords(recordsArray, callback) 116 { 117 if (!recordsArray) 118 return; 119 for (var i = 0; i < recordsArray.length; ++i) { 120 callback(recordsArray[i]); 121 forAllRecords(recordsArray[i].children, callback); 122 } 123 } 124 125 // Create sparse arrays with 101 cells each to fill with chunks for a given category. 126 this._overviewCalculator.reset(); 127 forAllRecords(records, this._overviewCalculator.updateBoundaries.bind(this._overviewCalculator)); 128 129 function markTimeline(record) 130 { 131 var percentages = this._overviewCalculator.computeBarGraphPercentages(record); 132 133 var end = Math.round(percentages.end); 134 var categoryName = record.category.name; 135 for (var j = Math.round(percentages.start); j <= end; ++j) 136 timelines[categoryName][j] = true; 137 } 138 forAllRecords(records, markTimeline.bind(this)); 139 140 // Convert sparse arrays to continuous segments, render graphs for each. 141 for (var category in this._categories) { 142 var timeline = timelines[category]; 143 window.timelineSaved = timeline; 144 var chunkStart = -1; 145 for (var j = 0; j < 101; ++j) { 146 if (timeline[j]) { 147 if (chunkStart === -1) 148 chunkStart = j; 149 } else { 150 if (chunkStart !== -1) { 151 this._categoryGraphs[category].addChunk(chunkStart, j); 152 chunkStart = -1; 153 } 154 } 155 } 156 if (chunkStart !== -1) { 157 this._categoryGraphs[category].addChunk(chunkStart, 100); 158 chunkStart = -1; 159 } 160 } 161 this._overviewGrid.updateDividers(true, this._overviewCalculator); 162 }, 163 164 setSidebarWidth: function(width) 165 { 166 this._overviewSidebarElement.style.width = width + "px"; 167 }, 168 169 updateMainViewWidth: function(width) 170 { 171 this._overviewGrid.element.style.left = width + "px"; 172 }, 173 174 reset: function() 175 { 176 this.windowLeft = 0.0; 177 this.windowRight = 1.0; 178 this._setWindowPosition(0, this._overviewGrid.element.clientWidth); 179 this._overviewCalculator.reset(); 180 this._overviewGrid.updateDividers(true, this._overviewCalculator); 181 }, 182 183 _resizeWindow: function(resizeElement, event) 184 { 185 WebInspector.elementDragStart(resizeElement, this._windowResizeDragging.bind(this, resizeElement), this._endWindowDragging.bind(this), event, "col-resize"); 186 }, 187 188 _windowResizeDragging: function(resizeElement, event) 189 { 190 if (resizeElement === this._leftResizeElement) 191 this._resizeWindowLeft(event.pageX - this._overviewGrid.element.offsetLeft); 192 else 193 this._resizeWindowRight(event.pageX - this._overviewGrid.element.offsetLeft); 194 event.preventDefault(); 195 }, 196 197 _dragWindow: function(event) 198 { 199 WebInspector.elementDragStart(this._overviewWindowElement, this._windowDragging.bind(this, event.pageX, 200 this._leftResizeElement.offsetLeft, this._rightResizeElement.offsetLeft), this._endWindowDragging.bind(this), event, "ew-resize"); 201 }, 202 203 _windowDragging: function(startX, windowLeft, windowRight, event) 204 { 205 var delta = event.pageX - startX; 206 var start = windowLeft + delta; 207 var end = windowRight + delta; 208 var windowSize = windowRight - windowLeft; 209 210 if (start < 0) { 211 start = 0; 212 end = windowSize; 213 } 214 215 if (end > this._overviewGrid.element.clientWidth) { 216 end = this._overviewGrid.element.clientWidth; 217 start = end - windowSize; 218 } 219 this._setWindowPosition(start, end); 220 221 event.preventDefault(); 222 }, 223 224 _resizeWindowLeft: function(start) 225 { 226 // Glue to edge. 227 if (start < 10) 228 start = 0; 229 this._setWindowPosition(start, null); 230 }, 231 232 _resizeWindowRight: function(end) 233 { 234 // Glue to edge. 235 if (end > this._overviewGrid.element.clientWidth - 10) 236 end = this._overviewGrid.element.clientWidth; 237 this._setWindowPosition(null, end); 238 }, 239 240 _setWindowPosition: function(start, end) 241 { 242 if (typeof start === "number") { 243 if (start > this._rightResizeElement.offsetLeft - 4) 244 start = this._rightResizeElement.offsetLeft - 4; 245 246 this.windowLeft = start / this._overviewGrid.element.clientWidth; 247 this._leftResizeElement.style.left = this.windowLeft * 100 + "%"; 248 this._overviewWindowElement.style.left = this.windowLeft * 100 + "%"; 249 } 250 if (typeof end === "number") { 251 if (end < this._leftResizeElement.offsetLeft + 12) 252 end = this._leftResizeElement.offsetLeft + 12; 253 254 this.windowRight = end / this._overviewGrid.element.clientWidth; 255 this._rightResizeElement.style.left = this.windowRight * 100 + "%"; 256 } 257 this._overviewWindowElement.style.width = (this.windowRight - this.windowLeft) * 100 + "%"; 258 this.dispatchEventToListeners("window changed"); 259 }, 260 261 _endWindowDragging: function(event) 262 { 263 WebInspector.elementDragEnd(event); 264 } 265} 266 267WebInspector.TimelineOverviewPane.prototype.__proto__ = WebInspector.Object.prototype; 268 269 270WebInspector.TimelineOverviewCalculator = function() 271{ 272 this._uiString = WebInspector.UIString.bind(WebInspector); 273} 274 275WebInspector.TimelineOverviewCalculator.prototype = { 276 computeBarGraphPercentages: function(record) 277 { 278 var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100; 279 var end = (record.endTime - this.minimumBoundary) / this.boundarySpan * 100; 280 return {start: start, end: end}; 281 }, 282 283 reset: function() 284 { 285 delete this.minimumBoundary; 286 delete this.maximumBoundary; 287 }, 288 289 updateBoundaries: function(record) 290 { 291 if (typeof this.minimumBoundary === "undefined" || record.startTime < this.minimumBoundary) { 292 this.minimumBoundary = record.startTime; 293 return true; 294 } 295 if (typeof this.maximumBoundary === "undefined" || record.endTime > this.maximumBoundary) { 296 this.maximumBoundary = record.endTime; 297 return true; 298 } 299 return false; 300 }, 301 302 get boundarySpan() 303 { 304 return this.maximumBoundary - this.minimumBoundary; 305 }, 306 307 formatValue: function(value) 308 { 309 return Number.secondsToString(value, this._uiString); 310 } 311} 312 313 314WebInspector.TimelineCategoryTreeElement = function(category, onCheckboxClicked) 315{ 316 this._category = category; 317 this._onCheckboxClicked = onCheckboxClicked; 318 // Pass an empty title, the title gets made later in onattach. 319 TreeElement.call(this, "", null, false); 320} 321 322WebInspector.TimelineCategoryTreeElement.prototype = { 323 onattach: function() 324 { 325 this.listItemElement.removeChildren(); 326 this.listItemElement.addStyleClass("timeline-category-tree-item"); 327 this.listItemElement.addStyleClass("timeline-category-" + this._category.name); 328 329 var label = document.createElement("label"); 330 331 var checkElement = document.createElement("input"); 332 checkElement.type = "checkbox"; 333 checkElement.className = "timeline-category-checkbox"; 334 checkElement.checked = true; 335 checkElement.addEventListener("click", this._onCheckboxClicked); 336 label.appendChild(checkElement); 337 338 var typeElement = document.createElement("span"); 339 typeElement.className = "type"; 340 typeElement.textContent = this._category.title; 341 label.appendChild(typeElement); 342 343 this.listItemElement.appendChild(label); 344 } 345} 346 347WebInspector.TimelineCategoryTreeElement.prototype.__proto__ = TreeElement.prototype; 348 349WebInspector.TimelineCategoryGraph = function(category, isEven) 350{ 351 this._category = category; 352 353 this._graphElement = document.createElement("div"); 354 this._graphElement.className = "timeline-graph-side timeline-overview-graph-side" + (isEven ? " even" : ""); 355 356 this._barAreaElement = document.createElement("div"); 357 this._barAreaElement.className = "timeline-graph-bar-area timeline-category-" + category.name; 358 this._graphElement.appendChild(this._barAreaElement); 359} 360 361WebInspector.TimelineCategoryGraph.prototype = { 362 get graphElement() 363 { 364 return this._graphElement; 365 }, 366 367 addChunk: function(start, end) 368 { 369 var chunk = document.createElement("div"); 370 chunk.className = "timeline-graph-bar"; 371 this._barAreaElement.appendChild(chunk); 372 chunk.style.setProperty("left", start + "%"); 373 chunk.style.setProperty("width", (end - start) + "%"); 374 }, 375 376 clearChunks: function() 377 { 378 this._barAreaElement.removeChildren(); 379 }, 380 381 set dimmed(dimmed) 382 { 383 if (dimmed) 384 this._barAreaElement.removeStyleClass("timeline-category-" + this._category.name); 385 else 386 this._barAreaElement.addStyleClass("timeline-category-" + this._category.name); 387 } 388} 389