Home | History | Annotate | Download | only in front-end
      1 /*
      2  * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
      3  * Copyright (C) 2008, 2009 Anthony Ricaud <rik (at) 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         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 
    428 WebInspector.AbstractTimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
    429 
    430 WebInspector.AbstractTimelineCalculator = function()
    431 {
    432 }
    433 
    434 WebInspector.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 
    500 WebInspector.AbstractTimelineCategory = function(name, title, color)
    501 {
    502     this.name = name;
    503     this.title = title;
    504     this.color = color;
    505 }
    506 
    507 WebInspector.AbstractTimelineCategory.prototype = {
    508     toString: function()
    509     {
    510         return this.title;
    511     }
    512 }
    513