Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2010 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 /**
     32  * @extends {WebInspector.VBox}
     33  * @constructor
     34  */
     35 WebInspector.TabbedPane = function()
     36 {
     37     WebInspector.VBox.call(this);
     38     this.element.classList.add("tabbed-pane");
     39     this.element.tabIndex = -1;
     40     this._headerElement = this.element.createChild("div", "tabbed-pane-header");
     41     this._headerContentsElement = this._headerElement.createChild("div", "tabbed-pane-header-contents");
     42     this._tabsElement = this._headerContentsElement.createChild("div", "tabbed-pane-header-tabs");
     43     this._contentElement = this.element.createChild("div", "tabbed-pane-content");
     44     /** @type {!Array.<!WebInspector.TabbedPaneTab>} */
     45     this._tabs = [];
     46     /** @type {!Array.<!WebInspector.TabbedPaneTab>} */
     47     this._tabsHistory = [];
     48     /** @type {!Object.<string, !WebInspector.TabbedPaneTab>} */
     49     this._tabsById = {};
     50     this._currentTabLocked = false;
     51 
     52     this._dropDownButton = this._createDropDownButton();
     53     WebInspector.zoomManager.addEventListener(WebInspector.ZoomManager.Events.ZoomChanged, this._zoomChanged, this);
     54 }
     55 
     56 WebInspector.TabbedPane.EventTypes = {
     57     TabSelected: "TabSelected",
     58     TabClosed: "TabClosed"
     59 }
     60 
     61 WebInspector.TabbedPane.prototype = {
     62     /**
     63      * @param {boolean} locked
     64      */
     65     setCurrentTabLocked: function(locked)
     66     {
     67         this._currentTabLocked = locked;
     68         this._headerElement.classList.toggle("locked", this._currentTabLocked);
     69     },
     70 
     71     /**
     72      * @return {?WebInspector.View}
     73      */
     74     get visibleView()
     75     {
     76         return this._currentTab ? this._currentTab.view : null;
     77     },
     78 
     79     /**
     80      * @return {!Array.<!WebInspector.View>}
     81      */
     82     tabViews: function()
     83     {
     84         /**
     85          * @param {!WebInspector.TabbedPaneTab} tab
     86          * @return {!WebInspector.View}
     87          */
     88         function tabToView(tab)
     89         {
     90             return tab.view;
     91         }
     92         return this._tabs.map(tabToView);
     93     },
     94 
     95     /**
     96      * @return {?string}
     97      */
     98     get selectedTabId()
     99     {
    100         return this._currentTab ? this._currentTab.id : null;
    101     },
    102 
    103     /**
    104      * @type {boolean} shrinkableTabs
    105      */
    106     set shrinkableTabs(shrinkableTabs)
    107     {
    108         this._shrinkableTabs = shrinkableTabs;
    109     },
    110 
    111     /**
    112      * @type {boolean} verticalTabLayout
    113      */
    114     set verticalTabLayout(verticalTabLayout)
    115     {
    116         this._verticalTabLayout = verticalTabLayout;
    117         this.invalidateConstraints();
    118     },
    119 
    120     /**
    121      * @type {boolean} closeableTabs
    122      */
    123     set closeableTabs(closeableTabs)
    124     {
    125         this._closeableTabs = closeableTabs;
    126     },
    127 
    128     /**
    129      * @param {boolean} retainTabOrder
    130      * @param {function(string, string):number=} tabOrderComparator
    131      */
    132     setRetainTabOrder: function(retainTabOrder, tabOrderComparator)
    133     {
    134         this._retainTabOrder = retainTabOrder;
    135         this._tabOrderComparator = tabOrderComparator;
    136     },
    137 
    138     /**
    139      * @return {?Element}
    140      */
    141     defaultFocusedElement: function()
    142     {
    143         return this.visibleView ? this.visibleView.defaultFocusedElement() : null;
    144     },
    145 
    146     focus: function()
    147     {
    148         if (this.visibleView)
    149             this.visibleView.focus();
    150         else
    151             this.element.focus();
    152     },
    153 
    154     /**
    155      * @return {!Element}
    156      */
    157     headerElement: function()
    158     {
    159         return this._headerElement;
    160     },
    161 
    162     /**
    163      * @param {string} id
    164      * @return {boolean}
    165      */
    166     isTabCloseable: function(id)
    167     {
    168         var tab = this._tabsById[id];
    169         return tab ? tab.isCloseable() : false;
    170     },
    171 
    172     /**
    173      * @param {!WebInspector.TabbedPaneTabDelegate} delegate
    174      */
    175     setTabDelegate: function(delegate)
    176     {
    177         var tabs = this._tabs.slice();
    178         for (var i = 0; i < tabs.length; ++i)
    179             tabs[i].setDelegate(delegate);
    180         this._delegate = delegate;
    181     },
    182 
    183     /**
    184      * @param {string} id
    185      * @param {string} tabTitle
    186      * @param {!WebInspector.View} view
    187      * @param {string=} tabTooltip
    188      * @param {boolean=} userGesture
    189      * @param {boolean=} isCloseable
    190      */
    191     appendTab: function(id, tabTitle, view, tabTooltip, userGesture, isCloseable)
    192     {
    193         isCloseable = typeof isCloseable === "boolean" ? isCloseable : this._closeableTabs;
    194         var tab = new WebInspector.TabbedPaneTab(this, id, tabTitle, isCloseable, view, tabTooltip);
    195         tab.setDelegate(this._delegate);
    196         this._tabsById[id] = tab;
    197 
    198         /**
    199          * @param {!WebInspector.TabbedPaneTab} tab1
    200          * @param {!WebInspector.TabbedPaneTab} tab2
    201          * @this {WebInspector.TabbedPane}
    202          * @return {number}
    203          */
    204         function comparator(tab1, tab2)
    205         {
    206             return this._tabOrderComparator(tab1.id, tab2.id);
    207         }
    208 
    209         if (this._retainTabOrder && this._tabOrderComparator)
    210             this._tabs.splice(insertionIndexForObjectInListSortedByFunction(tab, this._tabs, comparator.bind(this)), 0, tab);
    211         else
    212             this._tabs.push(tab);
    213 
    214         this._tabsHistory.push(tab);
    215 
    216         if (this._tabsHistory[0] === tab && this.isShowing())
    217             this.selectTab(tab.id, userGesture);
    218 
    219         this._updateTabElements();
    220     },
    221 
    222     /**
    223      * @param {string} id
    224      * @param {boolean=} userGesture
    225      */
    226     closeTab: function(id, userGesture)
    227     {
    228         this.closeTabs([id], userGesture);
    229     },
    230 
    231     /**
    232      * @param {!Array.<string>} ids
    233      * @param {boolean=} userGesture
    234      */
    235     closeTabs: function(ids, userGesture)
    236     {
    237         var focused = this.hasFocus();
    238         for (var i = 0; i < ids.length; ++i)
    239             this._innerCloseTab(ids[i], userGesture);
    240         this._updateTabElements();
    241         if (this._tabsHistory.length)
    242             this.selectTab(this._tabsHistory[0].id, false);
    243         if (focused)
    244             this.focus();
    245     },
    246 
    247     /**
    248      * @param {string} id
    249      * @param {boolean=} userGesture
    250      */
    251     _innerCloseTab: function(id, userGesture)
    252     {
    253         if (!this._tabsById[id])
    254             return;
    255         if (userGesture && !this._tabsById[id]._closeable)
    256             return;
    257         if (this._currentTab && this._currentTab.id === id)
    258             this._hideCurrentTab();
    259 
    260         var tab = this._tabsById[id];
    261         delete this._tabsById[id];
    262 
    263         this._tabsHistory.splice(this._tabsHistory.indexOf(tab), 1);
    264         this._tabs.splice(this._tabs.indexOf(tab), 1);
    265         if (tab._shown)
    266             this._hideTabElement(tab);
    267 
    268         var eventData = { tabId: id, view: tab.view, isUserGesture: userGesture };
    269         this.dispatchEventToListeners(WebInspector.TabbedPane.EventTypes.TabClosed, eventData);
    270         return true;
    271     },
    272 
    273     /**
    274      * @param {string} tabId
    275      * @return {boolean}
    276      */
    277     hasTab: function(tabId)
    278     {
    279         return !!this._tabsById[tabId];
    280     },
    281 
    282     /**
    283      * @return {!Array.<string>}
    284      */
    285     allTabs: function()
    286     {
    287         var result = [];
    288         var tabs = this._tabs.slice();
    289         for (var i = 0; i < tabs.length; ++i)
    290             result.push(tabs[i].id);
    291         return result;
    292     },
    293 
    294     /**
    295      * @param {string} id
    296      * @return {!Array.<string>}
    297      */
    298     otherTabs: function(id)
    299     {
    300         var result = [];
    301         var tabs = this._tabs.slice();
    302         for (var i = 0; i < tabs.length; ++i) {
    303             if (tabs[i].id !== id)
    304                 result.push(tabs[i].id);
    305         }
    306         return result;
    307     },
    308 
    309     /**
    310      * @param {string} id
    311      * @param {boolean=} userGesture
    312      * @return {boolean}
    313      */
    314     selectTab: function(id, userGesture)
    315     {
    316         if (this._currentTabLocked)
    317             return false;
    318         var focused = this.hasFocus();
    319         var tab = this._tabsById[id];
    320         if (!tab)
    321             return false;
    322         if (this._currentTab && this._currentTab.id === id)
    323             return true;
    324 
    325         this._hideCurrentTab();
    326         this._showTab(tab);
    327         this._currentTab = tab;
    328 
    329         this._tabsHistory.splice(this._tabsHistory.indexOf(tab), 1);
    330         this._tabsHistory.splice(0, 0, tab);
    331 
    332         this._updateTabElements();
    333         if (focused)
    334             this.focus();
    335 
    336         var eventData = { tabId: id, view: tab.view, isUserGesture: userGesture };
    337         this.dispatchEventToListeners(WebInspector.TabbedPane.EventTypes.TabSelected, eventData);
    338         return true;
    339     },
    340 
    341     /**
    342      * @param {number} tabsCount
    343      * @return {!Array.<string>}
    344      */
    345     lastOpenedTabIds: function(tabsCount)
    346     {
    347         function tabToTabId(tab) {
    348             return tab.id;
    349         }
    350 
    351         return this._tabsHistory.slice(0, tabsCount).map(tabToTabId);
    352     },
    353 
    354     /**
    355      * @param {string} id
    356      * @param {string} iconClass
    357      * @param {string=} iconTooltip
    358      */
    359     setTabIcon: function(id, iconClass, iconTooltip)
    360     {
    361         var tab = this._tabsById[id];
    362         if (tab._setIconClass(iconClass, iconTooltip))
    363             this._updateTabElements();
    364     },
    365 
    366     /**
    367      * @param {string} id
    368      * @param {string} className
    369      * @param {boolean=} force
    370      */
    371     toggleTabClass: function(id, className, force)
    372     {
    373         var tab = this._tabsById[id];
    374         if (tab._toggleClass(className, force))
    375             this._updateTabElements();
    376     },
    377 
    378     /**
    379      * @param {!WebInspector.Event} event
    380      */
    381     _zoomChanged: function(event)
    382     {
    383         for (var i = 0; i < this._tabs.length; ++i)
    384             delete this._tabs[i]._measuredWidth;
    385         if (this.isShowing())
    386             this._updateTabElements();
    387     },
    388 
    389     /**
    390      * @param {string} id
    391      * @param {string} tabTitle
    392      */
    393     changeTabTitle: function(id, tabTitle)
    394     {
    395         var tab = this._tabsById[id];
    396         if (tab.title === tabTitle)
    397             return;
    398         tab.title = tabTitle;
    399         this._updateTabElements();
    400     },
    401 
    402     /**
    403      * @param {string} id
    404      * @param {!WebInspector.View} view
    405      */
    406     changeTabView: function(id, view)
    407     {
    408         var tab = this._tabsById[id];
    409         if (this._currentTab && this._currentTab.id === tab.id) {
    410             if (tab.view !== view)
    411                 this._hideTab(tab);
    412             tab.view = view;
    413             this._showTab(tab);
    414         } else
    415             tab.view = view;
    416     },
    417 
    418     /**
    419      * @param {string} id
    420      * @param {string=} tabTooltip
    421      */
    422     changeTabTooltip: function(id, tabTooltip)
    423     {
    424         var tab = this._tabsById[id];
    425         tab.tooltip = tabTooltip;
    426     },
    427 
    428     onResize: function()
    429     {
    430         this._updateTabElements();
    431     },
    432 
    433     headerResized: function()
    434     {
    435         this._updateTabElements();
    436     },
    437 
    438     wasShown: function()
    439     {
    440         var effectiveTab = this._currentTab || this._tabsHistory[0];
    441         if (effectiveTab)
    442             this.selectTab(effectiveTab.id);
    443     },
    444 
    445     /**
    446      * @return {!Constraints}
    447      */
    448     calculateConstraints: function()
    449     {
    450         var constraints = WebInspector.VBox.prototype.calculateConstraints.call(this);
    451         var minContentConstraints = new Constraints(new Size(0, 0), new Size(50, 50));
    452         constraints = constraints.widthToMax(minContentConstraints).heightToMax(minContentConstraints);
    453         if (this._verticalTabLayout)
    454             constraints = constraints.addWidth(new Constraints(new Size(this._headerElement.offsetWidth, 0)));
    455         else
    456             constraints = constraints.addHeight(new Constraints(new Size(0, this._headerElement.offsetHeight)));
    457         return constraints;
    458     },
    459 
    460     _updateTabElements: function()
    461     {
    462         WebInspector.invokeOnceAfterBatchUpdate(this, this._innerUpdateTabElements);
    463     },
    464 
    465     /**
    466      * @param {string} text
    467      */
    468     setPlaceholderText: function(text)
    469     {
    470         this._noTabsMessage = text;
    471     },
    472 
    473     _innerUpdateTabElements: function()
    474     {
    475         if (!this.isShowing())
    476             return;
    477 
    478         if (!this._tabs.length) {
    479             this._contentElement.classList.add("has-no-tabs");
    480             if (this._noTabsMessage && !this._noTabsMessageElement) {
    481                 this._noTabsMessageElement = this._contentElement.createChild("div", "tabbed-pane-placeholder fill");
    482                 this._noTabsMessageElement.textContent = this._noTabsMessage;
    483             }
    484         } else {
    485             this._contentElement.classList.remove("has-no-tabs");
    486             if (this._noTabsMessageElement) {
    487                 this._noTabsMessageElement.remove();
    488                 delete this._noTabsMessageElement;
    489             }
    490         }
    491 
    492         if (!this._measuredDropDownButtonWidth)
    493             this._measureDropDownButton();
    494 
    495         this._updateWidths();
    496         this._updateTabsDropDown();
    497     },
    498 
    499     /**
    500      * @param {number} index
    501      * @param {!WebInspector.TabbedPaneTab} tab
    502      */
    503     _showTabElement: function(index, tab)
    504     {
    505         if (index >= this._tabsElement.children.length)
    506             this._tabsElement.appendChild(tab.tabElement);
    507         else
    508             this._tabsElement.insertBefore(tab.tabElement, this._tabsElement.children[index]);
    509         tab._shown = true;
    510     },
    511 
    512     /**
    513      * @param {!WebInspector.TabbedPaneTab} tab
    514      */
    515     _hideTabElement: function(tab)
    516     {
    517         this._tabsElement.removeChild(tab.tabElement);
    518         tab._shown = false;
    519     },
    520 
    521     _createDropDownButton: function()
    522     {
    523         var dropDownContainer = document.createElementWithClass("div", "tabbed-pane-header-tabs-drop-down-container");
    524         var dropDownButton = dropDownContainer.createChild("div", "tabbed-pane-header-tabs-drop-down");
    525         dropDownButton.createTextChild("\u00bb");
    526 
    527         this._dropDownMenu = new WebInspector.DropDownMenu();
    528         this._dropDownMenu.addEventListener(WebInspector.DropDownMenu.Events.ItemSelected, this._dropDownMenuItemSelected, this);
    529         dropDownButton.appendChild(this._dropDownMenu.element);
    530 
    531         return dropDownContainer;
    532     },
    533 
    534     /**
    535      * @param {!WebInspector.Event} event
    536      */
    537     _dropDownMenuItemSelected: function(event)
    538     {
    539         var tabId = /** @type {string} */ (event.data);
    540         this.selectTab(tabId, true);
    541     },
    542 
    543     _totalWidth: function()
    544     {
    545         return this._headerContentsElement.getBoundingClientRect().width;
    546     },
    547 
    548     _updateTabsDropDown: function()
    549     {
    550         var tabsToShowIndexes = this._tabsToShowIndexes(this._tabs, this._tabsHistory, this._totalWidth(), this._measuredDropDownButtonWidth);
    551 
    552         for (var i = 0; i < this._tabs.length; ++i) {
    553             if (this._tabs[i]._shown && tabsToShowIndexes.indexOf(i) === -1)
    554                 this._hideTabElement(this._tabs[i]);
    555         }
    556         for (var i = 0; i < tabsToShowIndexes.length; ++i) {
    557             var tab = this._tabs[tabsToShowIndexes[i]];
    558             if (!tab._shown)
    559                 this._showTabElement(i, tab);
    560         }
    561 
    562         this._populateDropDownFromIndex();
    563     },
    564 
    565     _populateDropDownFromIndex: function()
    566     {
    567         if (this._dropDownButton.parentElement)
    568             this._headerContentsElement.removeChild(this._dropDownButton);
    569 
    570         this._dropDownMenu.clear();
    571 
    572         var tabsToShow = [];
    573         for (var i = 0; i < this._tabs.length; ++i) {
    574             if (!this._tabs[i]._shown)
    575                 tabsToShow.push(this._tabs[i]);
    576                 continue;
    577         }
    578 
    579         function compareFunction(tab1, tab2)
    580         {
    581             return tab1.title.localeCompare(tab2.title);
    582         }
    583         if (!this._retainTabOrder)
    584             tabsToShow.sort(compareFunction);
    585 
    586         var selectedId = null;
    587         for (var i = 0; i < tabsToShow.length; ++i) {
    588             var tab = tabsToShow[i];
    589             this._dropDownMenu.addItem(tab.id, tab.title);
    590             if (this._tabsHistory[0] === tab)
    591                 selectedId = tab.id;
    592         }
    593         if (tabsToShow.length) {
    594             this._headerContentsElement.appendChild(this._dropDownButton);
    595             this._dropDownMenu.selectItem(selectedId);
    596         }
    597     },
    598 
    599     _measureDropDownButton: function()
    600     {
    601         this._dropDownButton.classList.add("measuring");
    602         this._headerContentsElement.appendChild(this._dropDownButton);
    603         this._measuredDropDownButtonWidth = this._dropDownButton.getBoundingClientRect().width;
    604         this._headerContentsElement.removeChild(this._dropDownButton);
    605         this._dropDownButton.classList.remove("measuring");
    606     },
    607 
    608     _updateWidths: function()
    609     {
    610         var measuredWidths = this._measureWidths();
    611         var maxWidth = this._shrinkableTabs ? this._calculateMaxWidth(measuredWidths.slice(), this._totalWidth()) : Number.MAX_VALUE;
    612 
    613         var i = 0;
    614         for (var tabId in this._tabs) {
    615             var tab = this._tabs[tabId];
    616             tab.setWidth(this._verticalTabLayout ? -1 : Math.min(maxWidth, measuredWidths[i++]));
    617         }
    618     },
    619 
    620     _measureWidths: function()
    621     {
    622         // Add all elements to measure into this._tabsElement
    623         this._tabsElement.style.setProperty("width", "2000px");
    624         var measuringTabElements = [];
    625         for (var tabId in this._tabs) {
    626             var tab = this._tabs[tabId];
    627             if (typeof tab._measuredWidth === "number")
    628                 continue;
    629             var measuringTabElement = tab._createTabElement(true);
    630             measuringTabElement.__tab = tab;
    631             measuringTabElements.push(measuringTabElement);
    632             this._tabsElement.appendChild(measuringTabElement);
    633         }
    634 
    635         // Perform measurement
    636         for (var i = 0; i < measuringTabElements.length; ++i)
    637             measuringTabElements[i].__tab._measuredWidth = measuringTabElements[i].getBoundingClientRect().width;
    638 
    639         // Nuke elements from the UI
    640         for (var i = 0; i < measuringTabElements.length; ++i)
    641             measuringTabElements[i].remove();
    642 
    643         // Combine the results.
    644         var measuredWidths = [];
    645         for (var tabId in this._tabs)
    646             measuredWidths.push(this._tabs[tabId]._measuredWidth);
    647         this._tabsElement.style.removeProperty("width");
    648 
    649         return measuredWidths;
    650     },
    651 
    652     /**
    653      * @param {!Array.<number>} measuredWidths
    654      * @param {number} totalWidth
    655      */
    656     _calculateMaxWidth: function(measuredWidths, totalWidth)
    657     {
    658         if (!measuredWidths.length)
    659             return 0;
    660 
    661         measuredWidths.sort(function(x, y) { return x - y });
    662 
    663         var totalMeasuredWidth = 0;
    664         for (var i = 0; i < measuredWidths.length; ++i)
    665             totalMeasuredWidth += measuredWidths[i];
    666 
    667         if (totalWidth >= totalMeasuredWidth)
    668             return measuredWidths[measuredWidths.length - 1];
    669 
    670         var totalExtraWidth = 0;
    671         for (var i = measuredWidths.length - 1; i > 0; --i) {
    672             var extraWidth = measuredWidths[i] - measuredWidths[i - 1];
    673             totalExtraWidth += (measuredWidths.length - i) * extraWidth;
    674 
    675             if (totalWidth + totalExtraWidth >= totalMeasuredWidth)
    676                 return measuredWidths[i - 1] + (totalWidth + totalExtraWidth - totalMeasuredWidth) / (measuredWidths.length - i);
    677         }
    678 
    679         return totalWidth / measuredWidths.length;
    680     },
    681 
    682     /**
    683      * @param {!Array.<!WebInspector.TabbedPaneTab>} tabsOrdered
    684      * @param {!Array.<!WebInspector.TabbedPaneTab>} tabsHistory
    685      * @param {number} totalWidth
    686      * @param {number} measuredDropDownButtonWidth
    687      * @return {!Array.<number>}
    688      */
    689     _tabsToShowIndexes: function(tabsOrdered, tabsHistory, totalWidth, measuredDropDownButtonWidth)
    690     {
    691         var tabsToShowIndexes = [];
    692 
    693         var totalTabsWidth = 0;
    694         var tabCount = tabsOrdered.length;
    695         for (var i = 0; i < tabCount; ++i) {
    696             var tab = this._retainTabOrder ? tabsOrdered[i] : tabsHistory[i];
    697             totalTabsWidth += tab.width();
    698             var minimalRequiredWidth = totalTabsWidth;
    699             if (i !== tabCount - 1)
    700                 minimalRequiredWidth += measuredDropDownButtonWidth;
    701             if (!this._verticalTabLayout && minimalRequiredWidth > totalWidth)
    702                 break;
    703             tabsToShowIndexes.push(tabsOrdered.indexOf(tab));
    704         }
    705 
    706         tabsToShowIndexes.sort(function(x, y) { return x - y });
    707 
    708         return tabsToShowIndexes;
    709     },
    710 
    711     _hideCurrentTab: function()
    712     {
    713         if (!this._currentTab)
    714             return;
    715 
    716         this._hideTab(this._currentTab);
    717         delete this._currentTab;
    718     },
    719 
    720     /**
    721      * @param {!WebInspector.TabbedPaneTab} tab
    722      */
    723     _showTab: function(tab)
    724     {
    725         tab.tabElement.classList.add("selected");
    726         tab.view.show(this._contentElement);
    727     },
    728 
    729     /**
    730      * @param {!WebInspector.TabbedPaneTab} tab
    731      */
    732     _hideTab: function(tab)
    733     {
    734         tab.tabElement.classList.remove("selected");
    735         tab.view.detach();
    736     },
    737 
    738     /**
    739      * @return {!Array.<!Element>}
    740      */
    741     elementsToRestoreScrollPositionsFor: function()
    742     {
    743         return [ this._contentElement ];
    744     },
    745 
    746     /**
    747      * @param {!WebInspector.TabbedPaneTab} tab
    748      * @param {number} index
    749      */
    750     _insertBefore: function(tab, index)
    751     {
    752         this._tabsElement.insertBefore(tab._tabElement || null, this._tabsElement.childNodes[index]);
    753         var oldIndex = this._tabs.indexOf(tab);
    754         this._tabs.splice(oldIndex, 1);
    755         if (oldIndex < index)
    756             --index;
    757         this._tabs.splice(index, 0, tab);
    758     },
    759 
    760     __proto__: WebInspector.VBox.prototype
    761 }
    762 
    763 /**
    764  * @constructor
    765  * @param {!WebInspector.TabbedPane} tabbedPane
    766  * @param {string} id
    767  * @param {string} title
    768  * @param {boolean} closeable
    769  * @param {!WebInspector.View} view
    770  * @param {string=} tooltip
    771  */
    772 WebInspector.TabbedPaneTab = function(tabbedPane, id, title, closeable, view, tooltip)
    773 {
    774     this._closeable = closeable;
    775     this._tabbedPane = tabbedPane;
    776     this._id = id;
    777     this._title = title;
    778     this._tooltip = tooltip;
    779     this._view = view;
    780     this._shown = false;
    781     /** @type {number} */ this._measuredWidth;
    782     /** @type {!Element|undefined} */ this._tabElement;
    783 }
    784 
    785 WebInspector.TabbedPaneTab.prototype = {
    786     /**
    787      * @return {string}
    788      */
    789     get id()
    790     {
    791         return this._id;
    792     },
    793 
    794     /**
    795      * @return {string}
    796      */
    797     get title()
    798     {
    799         return this._title;
    800     },
    801 
    802     set title(title)
    803     {
    804         if (title === this._title)
    805             return;
    806         this._title = title;
    807         if (this._titleElement)
    808             this._titleElement.textContent = title;
    809         delete this._measuredWidth;
    810     },
    811 
    812     /**
    813      * @return {string}
    814      */
    815     iconClass: function()
    816     {
    817         return this._iconClass;
    818     },
    819 
    820     /**
    821      * @return {boolean}
    822      */
    823     isCloseable: function()
    824     {
    825         return this._closeable;
    826     },
    827 
    828     /**
    829      * @param {string} iconClass
    830      * @param {string=} iconTooltip
    831      * @return {boolean}
    832      */
    833     _setIconClass: function(iconClass, iconTooltip)
    834     {
    835         if (iconClass === this._iconClass && iconTooltip === this._iconTooltip)
    836             return false;
    837         this._iconClass = iconClass;
    838         this._iconTooltip = iconTooltip;
    839         if (this._iconElement)
    840             this._iconElement.remove();
    841         if (this._iconClass && this._tabElement)
    842             this._iconElement = this._createIconElement(this._tabElement, this._titleElement);
    843         delete this._measuredWidth;
    844         return true;
    845     },
    846 
    847     /**
    848      * @param {string} className
    849      * @param {boolean=} force
    850      * @return {boolean}
    851      */
    852     _toggleClass: function(className, force)
    853     {
    854         var element = this.tabElement;
    855         var hasClass = element.classList.contains(className);
    856         if (hasClass === force)
    857             return false;
    858         element.classList.toggle(className, force);
    859         delete this._measuredWidth;
    860         return true;
    861     },
    862 
    863     /**
    864      * @return {!WebInspector.View}
    865      */
    866     get view()
    867     {
    868         return this._view;
    869     },
    870 
    871     set view(view)
    872     {
    873         this._view = view;
    874     },
    875 
    876     /**
    877      * @return {string|undefined}
    878      */
    879     get tooltip()
    880     {
    881         return this._tooltip;
    882     },
    883 
    884     set tooltip(tooltip)
    885     {
    886         this._tooltip = tooltip;
    887         if (this._titleElement)
    888             this._titleElement.title = tooltip || "";
    889     },
    890 
    891     /**
    892      * @return {!Element}
    893      */
    894     get tabElement()
    895     {
    896         if (!this._tabElement)
    897             this._tabElement = this._createTabElement(false);
    898 
    899         return this._tabElement;
    900     },
    901 
    902     /**
    903      * @return {number}
    904      */
    905     width: function()
    906     {
    907         return this._width;
    908     },
    909 
    910     /**
    911      * @param {number} width
    912      */
    913     setWidth: function(width)
    914     {
    915         this.tabElement.style.width = width === -1 ? "" : (width + "px");
    916         this._width = width;
    917     },
    918 
    919     /**
    920      * @param {!WebInspector.TabbedPaneTabDelegate} delegate
    921      */
    922     setDelegate: function(delegate)
    923     {
    924         this._delegate = delegate;
    925     },
    926 
    927     _createIconElement: function(tabElement, titleElement)
    928     {
    929         var iconElement = document.createElementWithClass("span", "tabbed-pane-header-tab-icon " + this._iconClass);
    930         if (this._iconTooltip)
    931             iconElement.title = this._iconTooltip;
    932         tabElement.insertBefore(iconElement, titleElement);
    933         return iconElement;
    934     },
    935 
    936     /**
    937      * @param {boolean} measuring
    938      * @return {!Element}
    939      */
    940     _createTabElement: function(measuring)
    941     {
    942         var tabElement = document.createElementWithClass("div", "tabbed-pane-header-tab");
    943         tabElement.id = "tab-" + this._id;
    944         tabElement.tabIndex = -1;
    945         tabElement.selectTabForTest = this._tabbedPane.selectTab.bind(this._tabbedPane, this.id, true);
    946 
    947         var titleElement = tabElement.createChild("span", "tabbed-pane-header-tab-title");
    948         titleElement.textContent = this.title;
    949         titleElement.title = this.tooltip || "";
    950         if (this._iconClass)
    951             this._createIconElement(tabElement, titleElement);
    952         if (!measuring)
    953             this._titleElement = titleElement;
    954 
    955         if (this._closeable)
    956             tabElement.createChild("div", "close-button-gray");
    957 
    958         if (measuring) {
    959             tabElement.classList.add("measuring");
    960         } else {
    961             tabElement.addEventListener("click", this._tabClicked.bind(this), false);
    962             tabElement.addEventListener("mousedown", this._tabMouseDown.bind(this), false);
    963             tabElement.addEventListener("mouseup", this._tabMouseUp.bind(this), false);
    964 
    965             if (this._closeable) {
    966                 tabElement.addEventListener("contextmenu", this._tabContextMenu.bind(this), false);
    967                 WebInspector.installDragHandle(tabElement, this._startTabDragging.bind(this), this._tabDragging.bind(this), this._endTabDragging.bind(this), "pointer");
    968             }
    969         }
    970 
    971         return tabElement;
    972     },
    973 
    974     /**
    975      * @param {!Event} event
    976      */
    977     _tabClicked: function(event)
    978     {
    979         var middleButton = event.button === 1;
    980         var shouldClose = this._closeable && (middleButton || event.target.classList.contains("close-button-gray"));
    981         if (!shouldClose) {
    982             this._tabbedPane.focus();
    983             return;
    984         }
    985         this._closeTabs([this.id]);
    986         event.consume(true);
    987     },
    988 
    989     /**
    990      * @param {!Event} event
    991      */
    992     _tabMouseDown: function(event)
    993     {
    994         if (event.target.classList.contains("close-button-gray") || event.button === 1)
    995             return;
    996         this._tabbedPane.selectTab(this.id, true);
    997     },
    998 
    999     /**
   1000      * @param {!Event} event
   1001      */
   1002     _tabMouseUp: function(event)
   1003     {
   1004         // This is needed to prevent middle-click pasting on linux when tabs are clicked.
   1005         if (event.button === 1)
   1006             event.consume(true);
   1007     },
   1008 
   1009     /**
   1010      * @param {!Array.<string>} ids
   1011      */
   1012     _closeTabs: function(ids)
   1013     {
   1014         if (this._delegate) {
   1015             this._delegate.closeTabs(this._tabbedPane, ids);
   1016             return;
   1017         }
   1018         this._tabbedPane.closeTabs(ids, true);
   1019     },
   1020 
   1021     _tabContextMenu: function(event)
   1022     {
   1023         /**
   1024          * @this {WebInspector.TabbedPaneTab}
   1025          */
   1026         function close()
   1027         {
   1028             this._closeTabs([this.id]);
   1029         }
   1030 
   1031         /**
   1032          * @this {WebInspector.TabbedPaneTab}
   1033          */
   1034         function closeOthers()
   1035         {
   1036             this._closeTabs(this._tabbedPane.otherTabs(this.id));
   1037         }
   1038 
   1039         /**
   1040          * @this {WebInspector.TabbedPaneTab}
   1041          */
   1042         function closeAll()
   1043         {
   1044             this._closeTabs(this._tabbedPane.allTabs());
   1045         }
   1046 
   1047         var contextMenu = new WebInspector.ContextMenu(event);
   1048         contextMenu.appendItem(WebInspector.UIString("Close"), close.bind(this));
   1049         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Close others" : "Close Others"), closeOthers.bind(this));
   1050         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Close all" : "Close All"), closeAll.bind(this));
   1051         contextMenu.show();
   1052     },
   1053 
   1054     /**
   1055      * @param {!Event} event
   1056      * @return {boolean}
   1057      */
   1058     _startTabDragging: function(event)
   1059     {
   1060         if (event.target.classList.contains("close-button-gray"))
   1061             return false;
   1062         this._dragStartX = event.pageX;
   1063         return true;
   1064     },
   1065 
   1066     /**
   1067      * @param {!Event} event
   1068      */
   1069     _tabDragging: function(event)
   1070     {
   1071         var tabElements = this._tabbedPane._tabsElement.childNodes;
   1072         for (var i = 0; i < tabElements.length; ++i) {
   1073             var tabElement = tabElements[i];
   1074             if (tabElement === this._tabElement)
   1075                 continue;
   1076 
   1077             var intersects = tabElement.offsetLeft + tabElement.clientWidth > this._tabElement.offsetLeft &&
   1078                 this._tabElement.offsetLeft + this._tabElement.clientWidth > tabElement.offsetLeft;
   1079             if (!intersects)
   1080                 continue;
   1081 
   1082             if (Math.abs(event.pageX - this._dragStartX) < tabElement.clientWidth / 2 + 5)
   1083                 break;
   1084 
   1085             if (event.pageX - this._dragStartX > 0) {
   1086                 tabElement = tabElement.nextSibling;
   1087                 ++i;
   1088             }
   1089 
   1090             var oldOffsetLeft = this._tabElement.offsetLeft;
   1091             this._tabbedPane._insertBefore(this, i);
   1092             this._dragStartX += this._tabElement.offsetLeft - oldOffsetLeft;
   1093             break;
   1094         }
   1095 
   1096         if (!this._tabElement.previousSibling && event.pageX - this._dragStartX < 0) {
   1097             this._tabElement.style.setProperty("left", "0px");
   1098             return;
   1099         }
   1100         if (!this._tabElement.nextSibling && event.pageX - this._dragStartX > 0) {
   1101             this._tabElement.style.setProperty("left", "0px");
   1102             return;
   1103         }
   1104 
   1105         this._tabElement.style.setProperty("position", "relative");
   1106         this._tabElement.style.setProperty("left", (event.pageX - this._dragStartX) + "px");
   1107     },
   1108 
   1109     /**
   1110      * @param {!Event} event
   1111      */
   1112     _endTabDragging: function(event)
   1113     {
   1114         this._tabElement.style.removeProperty("position");
   1115         this._tabElement.style.removeProperty("left");
   1116         delete this._dragStartX;
   1117     }
   1118 }
   1119 
   1120 /**
   1121  * @interface
   1122  */
   1123 WebInspector.TabbedPaneTabDelegate = function()
   1124 {
   1125 }
   1126 
   1127 WebInspector.TabbedPaneTabDelegate.prototype = {
   1128     /**
   1129      * @param {!WebInspector.TabbedPane} tabbedPane
   1130      * @param {!Array.<string>} ids
   1131      */
   1132     closeTabs: function(tabbedPane, ids) { }
   1133 }
   1134 
   1135 /**
   1136  * @constructor
   1137  * @param {!WebInspector.TabbedPane} tabbedPane
   1138  * @param {string} extensionPoint
   1139  * @param {function(string, !WebInspector.View)=} viewCallback
   1140  */
   1141 WebInspector.ExtensibleTabbedPaneController = function(tabbedPane, extensionPoint, viewCallback)
   1142 {
   1143     this._tabbedPane = tabbedPane;
   1144     this._extensionPoint = extensionPoint;
   1145     this._viewCallback = viewCallback;
   1146 
   1147     this._tabbedPane.setRetainTabOrder(true, self.runtime.orderComparator(extensionPoint, "name", "order"));
   1148     this._tabbedPane.addEventListener(WebInspector.TabbedPane.EventTypes.TabSelected, this._tabSelected, this);
   1149     /** @type {!StringMap.<?WebInspector.View>} */
   1150     this._views = new StringMap();
   1151     this._initialize();
   1152 }
   1153 
   1154 WebInspector.ExtensibleTabbedPaneController.prototype = {
   1155     _initialize: function()
   1156     {
   1157         /** @type {!StringMap.<!Runtime.Extension>} */
   1158         this._extensions = new StringMap();
   1159         var extensions = self.runtime.extensions(this._extensionPoint);
   1160 
   1161         for (var i = 0; i < extensions.length; ++i) {
   1162             var descriptor = extensions[i].descriptor();
   1163             var id = descriptor["name"];
   1164             var title = WebInspector.UIString(descriptor["title"]);
   1165             var settingName = descriptor["setting"];
   1166             var setting = settingName ? /** @type {!WebInspector.Setting|undefined} */ (WebInspector.settings[settingName]) : null;
   1167 
   1168             this._extensions.set(id, extensions[i]);
   1169 
   1170             if (setting) {
   1171                 setting.addChangeListener(this._toggleSettingBasedView.bind(this, id, title, setting));
   1172                 if (setting.get())
   1173                     this._tabbedPane.appendTab(id, title, new WebInspector.View());
   1174             } else {
   1175                 this._tabbedPane.appendTab(id, title, new WebInspector.View());
   1176             }
   1177         }
   1178     },
   1179 
   1180     /**
   1181      * @param {string} id
   1182      * @param {string} title
   1183      * @param {!WebInspector.Setting} setting
   1184      */
   1185     _toggleSettingBasedView: function(id, title, setting)
   1186     {
   1187         this._tabbedPane.closeTab(id);
   1188         if (setting.get())
   1189             this._tabbedPane.appendTab(id, title, new WebInspector.View());
   1190     },
   1191 
   1192     /**
   1193      * @param {!WebInspector.Event} event
   1194      */
   1195     _tabSelected: function(event)
   1196     {
   1197         var tabId = this._tabbedPane.selectedTabId;
   1198         if (!tabId)
   1199             return;
   1200         var view = this.viewForId(tabId);
   1201         if (view)
   1202             this._tabbedPane.changeTabView(tabId, view);
   1203     },
   1204 
   1205     /**
   1206      * @return {!Array.<string>}
   1207      */
   1208     viewIds: function()
   1209     {
   1210         return this._extensions.keys();
   1211     },
   1212 
   1213     /**
   1214      * @param {string} id
   1215      * @return {?WebInspector.View}
   1216      */
   1217     viewForId: function(id)
   1218     {
   1219         if (this._views.has(id))
   1220             return /** @type {!WebInspector.View} */ (this._views.get(id));
   1221         var view = this._extensions.has(id) ? /** @type {!WebInspector.View} */ (this._extensions.get(id).instance()) : null;
   1222         this._views.set(id, view);
   1223         if (this._viewCallback && view)
   1224             this._viewCallback(id, view);
   1225         return view;
   1226     }
   1227 }
   1228