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