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 31WebInspector.AbstractTimelinePanel = function() 32{ 33 WebInspector.Panel.call(this); 34 this._items = []; 35 this._staleItems = []; 36} 37 38WebInspector.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 const scrollTop = this.containerElement.scrollTop; 219 const offsetHeight = this.summaryBar.element.offsetHeight; 220 const dividersTop = (scrollTop < offsetHeight ? offsetHeight : scrollTop); 221 this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop); 222 }, 223 224 get needsRefresh() 225 { 226 return this._needsRefresh; 227 }, 228 229 set needsRefresh(x) 230 { 231 if (this._needsRefresh === x) 232 return; 233 234 this._needsRefresh = x; 235 236 if (x) { 237 if (this.visible && !("_refreshTimeout" in this)) 238 this._refreshTimeout = setTimeout(this.refresh.bind(this), 500); 239 } else { 240 if ("_refreshTimeout" in this) { 241 clearTimeout(this._refreshTimeout); 242 delete this._refreshTimeout; 243 } 244 } 245 }, 246 247 refreshIfNeeded: function() 248 { 249 if (this.needsRefresh) 250 this.refresh(); 251 }, 252 253 show: function() 254 { 255 WebInspector.Panel.prototype.show.call(this); 256 257 this._updateDividersLabelBarPosition(); 258 this.refreshIfNeeded(); 259 }, 260 261 resize: function() 262 { 263 WebInspector.Panel.prototype.resize.call(this); 264 265 this.updateGraphDividersIfNeeded(); 266 }, 267 268 updateMainViewWidth: function(width) 269 { 270 this._containerContentElement.style.left = width + "px"; 271 this.resize(); 272 }, 273 274 invalidateAllItems: function() 275 { 276 this._staleItems = this._items.slice(); 277 }, 278 279 refresh: function() 280 { 281 this.needsRefresh = false; 282 283 var staleItemsLength = this._staleItems.length; 284 285 var boundariesChanged = false; 286 287 for (var i = 0; i < staleItemsLength; ++i) { 288 var item = this._staleItems[i]; 289 if (!item._itemsTreeElement) { 290 // Create the timeline tree element and graph. 291 item._itemsTreeElement = this.createItemTreeElement(item); 292 item._itemsTreeElement._itemGraph = this.createItemGraph(item); 293 294 this.itemsTreeElement.appendChild(item._itemsTreeElement); 295 this.itemsGraphsElement.appendChild(item._itemsTreeElement._itemGraph.graphElement); 296 } 297 298 if (item._itemsTreeElement.refresh) 299 item._itemsTreeElement.refresh(); 300 301 if (this.calculator.updateBoundaries(item)) 302 boundariesChanged = true; 303 } 304 305 if (boundariesChanged) { 306 // The boundaries changed, so all item graphs are stale. 307 this._staleItems = this._items.slice(); 308 staleItemsLength = this._staleItems.length; 309 } 310 311 for (var i = 0; i < staleItemsLength; ++i) 312 this._staleItems[i]._itemsTreeElement._itemGraph.refresh(this.calculator); 313 314 this._staleItems = []; 315 316 this.updateGraphDividersIfNeeded(); 317 }, 318 319 reset: function() 320 { 321 this.containerElement.scrollTop = 0; 322 323 if (this._calculator) 324 this._calculator.reset(); 325 326 if (this._items) { 327 var itemsLength = this._items.length; 328 for (var i = 0; i < itemsLength; ++i) { 329 var item = this._items[i]; 330 delete item._itemsTreeElement; 331 } 332 } 333 334 this._items = []; 335 this._staleItems = []; 336 337 this.itemsTreeElement.removeChildren(); 338 this.itemsGraphsElement.removeChildren(); 339 340 this.updateGraphDividersIfNeeded(true); 341 }, 342 343 get calculator() 344 { 345 return this._calculator; 346 }, 347 348 set calculator(x) 349 { 350 if (!x || this._calculator === x) 351 return; 352 353 this._calculator = x; 354 this._calculator.reset(); 355 356 this._staleItems = this._items.slice(); 357 this.refresh(); 358 }, 359 360 addItem: function(item) 361 { 362 this._items.push(item); 363 this.refreshItem(item); 364 }, 365 366 removeItem: function(item) 367 { 368 this._items.remove(item, true); 369 370 if (item._itemsTreeElement) { 371 this.itemsTreeElement.removeChild(item._itemsTreeElement); 372 this.itemsGraphsElement.removeChild(item._itemsTreeElement._itemGraph.graphElement); 373 } 374 375 delete item._itemsTreeElement; 376 this.adjustScrollPosition(); 377 }, 378 379 refreshItem: function(item) 380 { 381 this._staleItems.push(item); 382 this.needsRefresh = true; 383 }, 384 385 revealAndSelectItem: function(item) 386 { 387 if (item._itemsTreeElement) { 388 item._itemsTreeElement.reveal(); 389 item._itemsTreeElement.select(true); 390 } 391 }, 392 393 sortItems: function(sortingFunction) 394 { 395 var sortedElements = [].concat(this.itemsTreeElement.children); 396 sortedElements.sort(sortingFunction); 397 398 var sortedElementsLength = sortedElements.length; 399 for (var i = 0; i < sortedElementsLength; ++i) { 400 var treeElement = sortedElements[i]; 401 if (treeElement === this.itemsTreeElement.children[i]) 402 continue; 403 404 var wasSelected = treeElement.selected; 405 this.itemsTreeElement.removeChild(treeElement); 406 this.itemsTreeElement.insertChild(treeElement, i); 407 if (wasSelected) 408 treeElement.select(true); 409 410 var graphElement = treeElement._itemGraph.graphElement; 411 this.itemsGraphsElement.insertBefore(graphElement, this.itemsGraphsElement.children[i]); 412 } 413 }, 414 415 adjustScrollPosition: function() 416 { 417 // Prevent the container from being scrolled off the end. 418 if ((this.containerElement.scrollTop + this.containerElement.offsetHeight) > this.sidebarElement.offsetHeight) 419 this.containerElement.scrollTop = (this.sidebarElement.offsetHeight - this.containerElement.offsetHeight); 420 }, 421 422 addEventDivider: function(divider) 423 { 424 this._timelineGrid.addEventDivider(divider); 425 } 426} 427 428WebInspector.AbstractTimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype; 429 430WebInspector.AbstractTimelineCalculator = function() 431{ 432} 433 434WebInspector.AbstractTimelineCalculator.prototype = { 435 computeSummaryValues: function(items) 436 { 437 var total = 0; 438 var categoryValues = {}; 439 440 var itemsLength = items.length; 441 for (var i = 0; i < itemsLength; ++i) { 442 var item = items[i]; 443 var value = this._value(item); 444 if (typeof value === "undefined") 445 continue; 446 if (!(item.category.name in categoryValues)) 447 categoryValues[item.category.name] = 0; 448 categoryValues[item.category.name] += value; 449 total += value; 450 } 451 452 return {categoryValues: categoryValues, total: total}; 453 }, 454 455 computeBarGraphPercentages: function(item) 456 { 457 return {start: 0, middle: 0, end: (this._value(item) / this.boundarySpan) * 100}; 458 }, 459 460 computeBarGraphLabels: function(item) 461 { 462 const label = this.formatValue(this._value(item)); 463 return {left: label, right: label, tooltip: label}; 464 }, 465 466 get boundarySpan() 467 { 468 return this.maximumBoundary - this.minimumBoundary; 469 }, 470 471 updateBoundaries: function(item) 472 { 473 this.minimumBoundary = 0; 474 475 var value = this._value(item); 476 if (typeof this.maximumBoundary === "undefined" || value > this.maximumBoundary) { 477 this.maximumBoundary = value; 478 return true; 479 } 480 return false; 481 }, 482 483 reset: function() 484 { 485 delete this.minimumBoundary; 486 delete this.maximumBoundary; 487 }, 488 489 _value: function(item) 490 { 491 return 0; 492 }, 493 494 formatValue: function(value) 495 { 496 return value.toString(); 497 } 498} 499 500WebInspector.AbstractTimelineCategory = function(name, title, color) 501{ 502 this.name = name; 503 this.title = title; 504 this.color = color; 505} 506 507WebInspector.AbstractTimelineCategory.prototype = { 508 toString: function() 509 { 510 return this.title; 511 } 512} 513