Home | History | Annotate | Download | only in front_end
      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.View}
     33  * @constructor
     34  */
     35 WebInspector.TabbedPane = function()
     36 {
     37     WebInspector.View.call(this);
     38     this.registerRequiredCSS("tabbedPane.css");
     39     this.element.addStyleClass("tabbed-pane");
     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     this._tabs = [];
     45     this._tabsHistory = [];
     46     this._tabsById = {};
     47     this.element.addEventListener("click", this.focus.bind(this), true);
     48     this.element.addEventListener("mouseup", this.onMouseUp.bind(this), false);
     49 
     50     this._dropDownButton = this._createDropDownButton();
     51 }
     52 
     53 WebInspector.TabbedPane.EventTypes = {
     54     TabSelected: "TabSelected",
     55     TabClosed: "TabClosed"
     56 }
     57 
     58 WebInspector.TabbedPane.prototype = {
     59     /**
     60      * @return {WebInspector.View}
     61      */
     62     get visibleView()
     63     {
     64         return this._currentTab ? this._currentTab.view : null;
     65     },
     66 
     67     /**
     68      * @return {string}
     69      */
     70     get selectedTabId()
     71     {
     72         return this._currentTab ? this._currentTab.id : null;
     73     },
     74 
     75     /**
     76      * @type {boolean} shrinkableTabs
     77      */
     78     set shrinkableTabs(shrinkableTabs)
     79     {
     80         this._shrinkableTabs = shrinkableTabs;
     81     },
     82 
     83     /**
     84      * @type {boolean} verticalTabLayout
     85      */
     86     set verticalTabLayout(verticalTabLayout)
     87     {
     88         this._verticalTabLayout = verticalTabLayout;
     89     },
     90 
     91     /**
     92      * @type {boolean} closeableTabs
     93      */
     94     set closeableTabs(closeableTabs)
     95     {
     96         this._closeableTabs = closeableTabs;
     97     },
     98 
     99     /**
    100      * @param {boolean} retainTabsOrder
    101      */
    102     setRetainTabsOrder: function(retainTabsOrder)
    103     {
    104         this._retainTabsOrder = retainTabsOrder;
    105     },
    106 
    107     defaultFocusedElement: function()
    108     {
    109         return this.visibleView ? this.visibleView.defaultFocusedElement() : null;
    110     },
    111 
    112     /**
    113      * @param {WebInspector.TabbedPaneTabDelegate} delegate
    114      */
    115     setTabDelegate: function(delegate)
    116     {
    117         var tabs = this._tabs.slice();
    118         for (var i = 0; i < tabs.length; ++i)
    119             tabs[i].setDelegate(delegate);
    120         this._delegate = delegate;
    121     },
    122 
    123     /**
    124      * @param {Event} event
    125      */
    126     onMouseUp: function(event)
    127     {
    128         // This is needed to prevent middle-click pasting on linux when tabs are clicked.
    129         if (event.button === 1)
    130             event.consume(true);
    131     },
    132 
    133     /**
    134      * @param {string} id
    135      * @param {string} tabTitle
    136      * @param {WebInspector.View} view
    137      * @param {string=} tabTooltip
    138      * @param {boolean=} userGesture
    139      */
    140     appendTab: function(id, tabTitle, view, tabTooltip, userGesture)
    141     {
    142         var tab = new WebInspector.TabbedPaneTab(this, id, tabTitle, this._closeableTabs, view, tabTooltip);
    143         tab.setDelegate(this._delegate);
    144         this._tabsById[id] = tab;
    145 
    146         this._tabs.push(tab);
    147         this._tabsHistory.push(tab);
    148 
    149         if (this._tabsHistory[0] === tab)
    150             this.selectTab(tab.id, userGesture);
    151 
    152         this._updateTabElements();
    153     },
    154 
    155     /**
    156      * @param {string} id
    157      * @param {boolean=} userGesture
    158      */
    159     closeTab: function(id, userGesture)
    160     {
    161         this.closeTabs([id], userGesture);
    162     },
    163 
    164      /**
    165       * @param {Array.<string>} ids
    166       * @param {boolean=} userGesture
    167       */
    168      closeTabs: function(ids, userGesture)
    169      {
    170          for (var i = 0; i < ids.length; ++i)
    171              this._innerCloseTab(ids[i], userGesture);
    172          this._updateTabElements();
    173          if (this._tabsHistory.length)
    174              this.selectTab(this._tabsHistory[0].id, userGesture);
    175      },
    176 
    177     /**
    178      * @param {string} id
    179      * @param {boolean=} userGesture
    180      */
    181     _innerCloseTab: function(id, userGesture)
    182     {
    183         if (this._currentTab && this._currentTab.id === id)
    184             this._hideCurrentTab();
    185 
    186         var tab = this._tabsById[id];
    187         delete this._tabsById[id];
    188 
    189         this._tabsHistory.splice(this._tabsHistory.indexOf(tab), 1);
    190         this._tabs.splice(this._tabs.indexOf(tab), 1);
    191         if (tab._shown)
    192             this._hideTabElement(tab);
    193 
    194         var eventData = { tabId: id, view: tab.view, isUserGesture: userGesture };
    195         this.dispatchEventToListeners(WebInspector.TabbedPane.EventTypes.TabClosed, eventData);
    196         return true;
    197     },
    198 
    199     /**
    200      * @return {Array.<string>}
    201      */
    202     allTabs: function()
    203     {
    204         var result = [];
    205         var tabs = this._tabs.slice();
    206         for (var i = 0; i < tabs.length; ++i)
    207             result.push(tabs[i].id);
    208         return result;
    209     },
    210 
    211     /**
    212      * @param {string} id
    213      * @return {Array.<string>}
    214      */
    215     otherTabs: function(id)
    216     {
    217         var result = [];
    218         var tabs = this._tabs.slice();
    219         for (var i = 0; i < tabs.length; ++i) {
    220             if (tabs[i].id !== id)
    221                 result.push(tabs[i].id);
    222         }
    223         return result;
    224     },
    225 
    226     /**
    227      * @param {string} id
    228      * @param {boolean=} userGesture
    229      */
    230     selectTab: function(id, userGesture)
    231     {
    232         var tab = this._tabsById[id];
    233         if (!tab)
    234             return;
    235         if (this._currentTab && this._currentTab.id === id)
    236             return;
    237 
    238         this._hideCurrentTab();
    239         this._showTab(tab);
    240         this._currentTab = tab;
    241 
    242         this._tabsHistory.splice(this._tabsHistory.indexOf(tab), 1);
    243         this._tabsHistory.splice(0, 0, tab);
    244 
    245         this._updateTabElements();
    246 
    247         var eventData = { tabId: id, view: tab.view, isUserGesture: userGesture };
    248         this.dispatchEventToListeners(WebInspector.TabbedPane.EventTypes.TabSelected, eventData);
    249         return true;
    250     },
    251 
    252     /**
    253      * @param {number} tabsCount
    254      * @return {Array.<string>}
    255      */
    256     lastOpenedTabIds: function(tabsCount)
    257     {
    258         function tabToTabId(tab) {
    259             return tab.id;
    260         }
    261 
    262         return this._tabsHistory.slice(0, tabsCount).map(tabToTabId);
    263     },
    264 
    265     /**
    266      * @param {string} id
    267      * @param {string} iconClass
    268      * @param {string=} iconTooltip
    269      */
    270     setTabIcon: function(id, iconClass, iconTooltip)
    271     {
    272         var tab = this._tabsById[id];
    273         tab._setIconClass(iconClass, iconTooltip);
    274         this._updateTabElements();
    275     },
    276 
    277     /**
    278      * @param {string} id
    279      * @param {string} tabTitle
    280      */
    281     changeTabTitle: function(id, tabTitle)
    282     {
    283         var tab = this._tabsById[id];
    284         tab.title = tabTitle;
    285         this._updateTabElements();
    286     },
    287 
    288     /**
    289      * @param {string} id
    290      * @param {WebInspector.View} view
    291      */
    292     changeTabView: function(id, view)
    293     {
    294         var tab = this._tabsById[id];
    295         if (this._currentTab && this._currentTab.id === tab.id) {
    296             this._hideTab(tab);
    297             tab.view = view;
    298             this._showTab(tab);
    299         } else
    300             tab.view = view;
    301     },
    302 
    303     /**
    304      * @param {string} id
    305      * @param {string=} tabTooltip
    306      */
    307     changeTabTooltip: function(id, tabTooltip)
    308     {
    309         var tab = this._tabsById[id];
    310         tab.tooltip = tabTooltip;
    311     },
    312 
    313     onResize: function()
    314     {
    315         this._updateTabElements();
    316     },
    317 
    318     _updateTabElements: function()
    319     {
    320         WebInspector.invokeOnceAfterBatchUpdate(this, this._innerUpdateTabElements);
    321     },
    322 
    323     /**
    324      * @param {string} text
    325      */
    326     setPlaceholderText: function(text)
    327     {
    328         this._noTabsMessage = text;
    329     },
    330 
    331     _innerUpdateTabElements: function()
    332     {
    333         if (!this.isShowing())
    334             return;
    335 
    336         if (!this._tabs.length) {
    337             this._contentElement.addStyleClass("has-no-tabs");
    338             if (this._noTabsMessage && !this._noTabsMessageElement) {
    339                 this._noTabsMessageElement = this._contentElement.createChild("div", "tabbed-pane-placeholder fill");
    340                 this._noTabsMessageElement.textContent = this._noTabsMessage;
    341             }
    342         } else {
    343             this._contentElement.removeStyleClass("has-no-tabs");
    344             if (this._noTabsMessageElement) {
    345                 this._noTabsMessageElement.remove();
    346                 delete this._noTabsMessageElement;
    347             }
    348         }
    349 
    350         if (!this._measuredDropDownButtonWidth)
    351             this._measureDropDownButton();
    352 
    353         this._updateWidths();
    354         this._updateTabsDropDown();
    355     },
    356 
    357     /**
    358      * @param {number} index
    359      * @param {WebInspector.TabbedPaneTab} tab
    360      */
    361     _showTabElement: function(index, tab)
    362     {
    363         if (index >= this._tabsElement.children.length)
    364             this._tabsElement.appendChild(tab.tabElement);
    365         else
    366             this._tabsElement.insertBefore(tab.tabElement, this._tabsElement.children[index]);
    367         tab._shown = true;
    368     },
    369 
    370     /**
    371      * @param {WebInspector.TabbedPaneTab} tab
    372      */
    373     _hideTabElement: function(tab)
    374     {
    375         this._tabsElement.removeChild(tab.tabElement);
    376         tab._shown = false;
    377     },
    378 
    379     _createDropDownButton: function()
    380     {
    381         var dropDownContainer = document.createElement("div");
    382         dropDownContainer.addStyleClass("tabbed-pane-header-tabs-drop-down-container");
    383         var dropDownButton = dropDownContainer.createChild("div", "tabbed-pane-header-tabs-drop-down");
    384         dropDownButton.appendChild(document.createTextNode("\u00bb"));
    385         this._tabsSelect = dropDownButton.createChild("select", "tabbed-pane-header-tabs-drop-down-select");
    386         this._tabsSelect.addEventListener("change", this._tabsSelectChanged.bind(this), false);
    387         return dropDownContainer;
    388     },
    389 
    390     _totalWidth: function()
    391     {
    392         return this._headerContentsElement.getBoundingClientRect().width;
    393     },
    394 
    395     _updateTabsDropDown: function()
    396     {
    397         var tabsToShowIndexes = this._tabsToShowIndexes(this._tabs, this._tabsHistory, this._totalWidth(), this._measuredDropDownButtonWidth);
    398 
    399         for (var i = 0; i < this._tabs.length; ++i) {
    400             if (this._tabs[i]._shown && tabsToShowIndexes.indexOf(i) === -1)
    401                 this._hideTabElement(this._tabs[i]);
    402         }
    403         for (var i = 0; i < tabsToShowIndexes.length; ++i) {
    404             var tab = this._tabs[tabsToShowIndexes[i]];
    405             if (!tab._shown)
    406                 this._showTabElement(i, tab);
    407         }
    408 
    409         this._populateDropDownFromIndex();
    410     },
    411 
    412     _populateDropDownFromIndex: function()
    413     {
    414         if (this._dropDownButton.parentElement)
    415             this._headerContentsElement.removeChild(this._dropDownButton);
    416 
    417         this._tabsSelect.removeChildren();
    418         var tabsToShow = [];
    419         for (var i = 0; i < this._tabs.length; ++i) {
    420             if (!this._tabs[i]._shown)
    421                 tabsToShow.push(this._tabs[i]);
    422                 continue;
    423         }
    424 
    425         function compareFunction(tab1, tab2)
    426         {
    427             return tab1.title.localeCompare(tab2.title);
    428         }
    429         tabsToShow.sort(compareFunction);
    430 
    431         var selectedIndex = -1;
    432         for (var i = 0; i < tabsToShow.length; ++i) {
    433             var option = new Option(tabsToShow[i].title);
    434             option.tab = tabsToShow[i];
    435             this._tabsSelect.appendChild(option);
    436             if (this._tabsHistory[0] === tabsToShow[i])
    437                 selectedIndex = i;
    438         }
    439         if (this._tabsSelect.options.length) {
    440             this._headerContentsElement.appendChild(this._dropDownButton);
    441             this._tabsSelect.selectedIndex = selectedIndex;
    442         }
    443     },
    444 
    445     _tabsSelectChanged: function()
    446     {
    447         var options = this._tabsSelect.options;
    448         var selectedOption = options[this._tabsSelect.selectedIndex];
    449         this.selectTab(selectedOption.tab.id, true);
    450     },
    451 
    452     _measureDropDownButton: function()
    453     {
    454         this._dropDownButton.addStyleClass("measuring");
    455         this._headerContentsElement.appendChild(this._dropDownButton);
    456         this._measuredDropDownButtonWidth = this._dropDownButton.getBoundingClientRect().width;
    457         this._headerContentsElement.removeChild(this._dropDownButton);
    458         this._dropDownButton.removeStyleClass("measuring");
    459     },
    460 
    461     _updateWidths: function()
    462     {
    463         var measuredWidths = this._measureWidths();
    464         var maxWidth = this._shrinkableTabs ? this._calculateMaxWidth(measuredWidths.slice(), this._totalWidth()) : Number.MAX_VALUE;
    465 
    466         var i = 0;
    467         for (var tabId in this._tabs) {
    468             var tab = this._tabs[tabId];
    469             tab.setWidth(this._verticalTabLayout ? -1 : Math.min(maxWidth, measuredWidths[i++]));
    470         }
    471     },
    472 
    473     _measureWidths: function()
    474     {
    475         // Add all elements to measure into this._tabsElement
    476         var measuringTabElements = [];
    477         for (var tabId in this._tabs) {
    478             var tab = this._tabs[tabId];
    479             if (typeof tab._measuredWidth === "number")
    480                 continue;
    481             var measuringTabElement = tab._createTabElement(true);
    482             measuringTabElement.__tab = tab;
    483             measuringTabElements.push(measuringTabElement);
    484             this._tabsElement.appendChild(measuringTabElement);
    485         }
    486 
    487         // Perform measurement
    488         for (var i = 0; i < measuringTabElements.length; ++i)
    489             measuringTabElements[i].__tab._measuredWidth = measuringTabElements[i].getBoundingClientRect().width;
    490 
    491         // Nuke elements from the UI
    492         for (var i = 0; i < measuringTabElements.length; ++i)
    493             measuringTabElements[i].remove();
    494 
    495         // Combine the results.
    496         var measuredWidths = [];
    497         for (var tabId in this._tabs)
    498             measuredWidths.push(this._tabs[tabId]._measuredWidth);
    499 
    500         return measuredWidths;
    501     },
    502 
    503     /**
    504      * @param {Array.<number>} measuredWidths
    505      * @param {number} totalWidth
    506      */
    507     _calculateMaxWidth: function(measuredWidths, totalWidth)
    508     {
    509         if (!measuredWidths.length)
    510             return 0;
    511 
    512         measuredWidths.sort(function(x, y) { return x - y });
    513 
    514         var totalMeasuredWidth = 0;
    515         for (var i = 0; i < measuredWidths.length; ++i)
    516             totalMeasuredWidth += measuredWidths[i];
    517 
    518         if (totalWidth >= totalMeasuredWidth)
    519             return measuredWidths[measuredWidths.length - 1];
    520 
    521         var totalExtraWidth = 0;
    522         for (var i = measuredWidths.length - 1; i > 0; --i) {
    523             var extraWidth = measuredWidths[i] - measuredWidths[i - 1];
    524             totalExtraWidth += (measuredWidths.length - i) * extraWidth;
    525 
    526             if (totalWidth + totalExtraWidth >= totalMeasuredWidth)
    527                 return measuredWidths[i - 1] + (totalWidth + totalExtraWidth - totalMeasuredWidth) / (measuredWidths.length - i);
    528         }
    529 
    530         return totalWidth / measuredWidths.length;
    531     },
    532 
    533     /**
    534      * @param {Array.<WebInspector.TabbedPaneTab>} tabsOrdered
    535      * @param {Array.<WebInspector.TabbedPaneTab>} tabsHistory
    536      * @param {number} totalWidth
    537      * @param {number} measuredDropDownButtonWidth
    538      * @return {Array.<number>}
    539      */
    540     _tabsToShowIndexes: function(tabsOrdered, tabsHistory, totalWidth, measuredDropDownButtonWidth)
    541     {
    542         var tabsToShowIndexes = [];
    543 
    544         var totalTabsWidth = 0;
    545         var tabCount = tabsOrdered.length;
    546         for (var i = 0; i < tabCount; ++i) {
    547             var tab = this._retainTabsOrder ? tabsOrdered[i] : tabsHistory[i];
    548             totalTabsWidth += tab.width();
    549             var minimalRequiredWidth = totalTabsWidth;
    550             if (i !== tabCount - 1)
    551                 minimalRequiredWidth += measuredDropDownButtonWidth;
    552             if (!this._verticalTabLayout && minimalRequiredWidth > totalWidth)
    553                 break;
    554             tabsToShowIndexes.push(tabsOrdered.indexOf(tab));
    555         }
    556 
    557         tabsToShowIndexes.sort(function(x, y) { return x - y });
    558 
    559         return tabsToShowIndexes;
    560     },
    561 
    562     _hideCurrentTab: function()
    563     {
    564         if (!this._currentTab)
    565             return;
    566 
    567         this._hideTab(this._currentTab);
    568         delete this._currentTab;
    569     },
    570 
    571     /**
    572      * @param {WebInspector.TabbedPaneTab} tab
    573      */
    574     _showTab: function(tab)
    575     {
    576         tab.tabElement.addStyleClass("selected");
    577         tab.view.show(this._contentElement);
    578     },
    579 
    580     /**
    581      * @param {WebInspector.TabbedPaneTab} tab
    582      */
    583     _hideTab: function(tab)
    584     {
    585         tab.tabElement.removeStyleClass("selected");
    586         tab.view.detach();
    587     },
    588 
    589     /**
    590      * @override
    591      */
    592     canHighlightPosition: function()
    593     {
    594         return this._currentTab && this._currentTab.view && this._currentTab.view.canHighlightPosition();
    595     },
    596 
    597     /**
    598      * @override
    599      */
    600     highlightPosition: function(line, column)
    601     {
    602         if (this.canHighlightPosition())
    603             this._currentTab.view.highlightPosition(line, column);
    604     },
    605 
    606     /**
    607      * @return {Array.<Element>}
    608      */
    609     elementsToRestoreScrollPositionsFor: function()
    610     {
    611         return [ this._contentElement ];
    612     },
    613 
    614     /**
    615      * @param {WebInspector.TabbedPaneTab} tab
    616      * @param {number} index
    617      */
    618     _insertBefore: function(tab, index)
    619     {
    620         this._tabsElement.insertBefore(tab._tabElement, this._tabsElement.childNodes[index]);
    621         var oldIndex = this._tabs.indexOf(tab);
    622         this._tabs.splice(oldIndex, 1);
    623         if (oldIndex < index)
    624             --index;
    625         this._tabs.splice(index, 0, tab);
    626     },
    627 
    628     __proto__: WebInspector.View.prototype
    629 }
    630 
    631 
    632 /**
    633  * @constructor
    634  * @param {WebInspector.TabbedPane} tabbedPane
    635  * @param {string} id
    636  * @param {string} title
    637  * @param {boolean} closeable
    638  * @param {WebInspector.View} view
    639  * @param {string=} tooltip
    640  */
    641 WebInspector.TabbedPaneTab = function(tabbedPane, id, title, closeable, view, tooltip)
    642 {
    643     this._closeable = closeable;
    644     this._tabbedPane = tabbedPane;
    645     this._id = id;
    646     this._title = title;
    647     this._tooltip = tooltip;
    648     this._view = view;
    649     this._shown = false;
    650     /** @type {number} */ this._measuredWidth;
    651     /** @type {Element} */ this._tabElement;
    652 }
    653 
    654 WebInspector.TabbedPaneTab.prototype = {
    655     /**
    656      * @return {string}
    657      */
    658     get id()
    659     {
    660         return this._id;
    661     },
    662 
    663     /**
    664      * @return {string}
    665      */
    666     get title()
    667     {
    668         return this._title;
    669     },
    670 
    671     set title(title)
    672     {
    673         if (title === this._title)
    674             return;
    675         this._title = title;
    676         if (this._titleElement)
    677             this._titleElement.textContent = title;
    678         delete this._measuredWidth;
    679     },
    680 
    681     /**
    682      * @return {string}
    683      */
    684     iconClass: function()
    685     {
    686         return this._iconClass;
    687     },
    688 
    689     /**
    690      * @param {string} iconClass
    691      * @param {string} iconTooltip
    692      */
    693     _setIconClass: function(iconClass, iconTooltip)
    694     {
    695         if (iconClass === this._iconClass && iconTooltip === this._iconTooltip)
    696             return;
    697         this._iconClass = iconClass;
    698         this._iconTooltip = iconTooltip;
    699         if (this._iconElement)
    700             this._iconElement.remove();
    701         if (this._iconClass)
    702             this._iconElement = this._createIconElement(this._tabElement, this._titleElement);
    703         delete this._measuredWidth;
    704     },
    705 
    706     /**
    707      * @return {WebInspector.View}
    708      */
    709     get view()
    710     {
    711         return this._view;
    712     },
    713 
    714     set view(view)
    715     {
    716         this._view = view;
    717     },
    718 
    719     /**
    720      * @return {string|undefined}
    721      */
    722     get tooltip()
    723     {
    724         return this._tooltip;
    725     },
    726 
    727     set tooltip(tooltip)
    728     {
    729         this._tooltip = tooltip;
    730         if (this._titleElement)
    731             this._titleElement.title = tooltip || "";
    732     },
    733 
    734     /**
    735      * @return {Element}
    736      */
    737     get tabElement()
    738     {
    739         if (typeof(this._tabElement) !== "undefined")
    740             return this._tabElement;
    741 
    742         this._createTabElement(false);
    743         return this._tabElement;
    744     },
    745 
    746     /**
    747      * @return {number}
    748      */
    749     width: function()
    750     {
    751         return this._width;
    752     },
    753 
    754     /**
    755      * @param {number} width
    756      */
    757     setWidth: function(width)
    758     {
    759         this.tabElement.style.width = width === -1 ? "" : (width + "px");
    760         this._width = width;
    761     },
    762 
    763     /**
    764      * @param {WebInspector.TabbedPaneTabDelegate} delegate
    765      */
    766     setDelegate: function(delegate)
    767     {
    768         this._delegate = delegate;
    769     },
    770 
    771     _createIconElement: function(tabElement, titleElement)
    772     {
    773         var iconElement = document.createElement("span");
    774         iconElement.className = "tabbed-pane-header-tab-icon " + this._iconClass;
    775         if (this._iconTooltip)
    776             iconElement.title = this._iconTooltip;
    777         tabElement.insertBefore(iconElement, titleElement);
    778         return iconElement;
    779     },
    780 
    781     /**
    782      * @param {boolean} measuring
    783      */
    784     _createTabElement: function(measuring)
    785     {
    786         var tabElement = document.createElement("div");
    787         tabElement.addStyleClass("tabbed-pane-header-tab");
    788         tabElement.id = "tab-" + this._id;
    789         tabElement.tabIndex = -1;
    790 
    791         var titleElement = tabElement.createChild("span", "tabbed-pane-header-tab-title");
    792         titleElement.textContent = this.title;
    793         titleElement.title = this.tooltip || "";
    794         if (this._iconClass)
    795             this._createIconElement(tabElement, titleElement);
    796         if (!measuring)
    797             this._titleElement = titleElement;
    798 
    799         if (this._closeable)
    800             tabElement.createChild("div", "close-button-gray");
    801 
    802         if (measuring)
    803             tabElement.addStyleClass("measuring");
    804         else {
    805             this._tabElement = tabElement;
    806             tabElement.addEventListener("click", this._tabClicked.bind(this), false);
    807             tabElement.addEventListener("mousedown", this._tabMouseDown.bind(this), false);
    808             if (this._closeable) {
    809                 tabElement.addEventListener("contextmenu", this._tabContextMenu.bind(this), false);
    810                 WebInspector.installDragHandle(tabElement, this._startTabDragging.bind(this), this._tabDragging.bind(this), this._endTabDragging.bind(this), "pointer");
    811             }
    812         }
    813 
    814         return tabElement;
    815     },
    816 
    817     /**
    818      * @param {Event} event
    819      */
    820     _tabClicked: function(event)
    821     {
    822         if (this._closeable && (event.button === 1 || event.target.hasStyleClass("close-button-gray")))
    823             this._closeTabs([this.id]);
    824     },
    825 
    826     /**
    827      * @param {Event} event
    828      */
    829     _tabMouseDown: function(event)
    830     {
    831         if (event.target.hasStyleClass("close-button-gray") || event.button === 1)
    832             return;
    833         this._tabbedPane.selectTab(this.id, true);
    834     },
    835 
    836     /**
    837      * @param {Array.<string>} ids
    838      */
    839     _closeTabs: function(ids)
    840     {
    841         if (this._delegate) {
    842             this._delegate.closeTabs(this._tabbedPane, ids);
    843             return;
    844         }
    845         this._tabbedPane.closeTabs(ids, true);
    846     },
    847 
    848     _tabContextMenu: function(event)
    849     {
    850         function close()
    851         {
    852             this._closeTabs([this.id]);
    853         }
    854 
    855         function closeOthers()
    856         {
    857             this._closeTabs(this._tabbedPane.otherTabs(this.id));
    858         }
    859 
    860         function closeAll()
    861         {
    862             this._closeTabs(this._tabbedPane.allTabs(this.id));
    863         }
    864 
    865         var contextMenu = new WebInspector.ContextMenu(event);
    866         contextMenu.appendItem(WebInspector.UIString("Close"), close.bind(this));
    867         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Close others" : "Close Others"), closeOthers.bind(this));
    868         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Close all" : "Close All"), closeAll.bind(this));
    869         contextMenu.show();
    870     },
    871 
    872     /**
    873      * @param {Event} event
    874      * @return {boolean}
    875      */
    876     _startTabDragging: function(event)
    877     {
    878         if (event.target.hasStyleClass("close-button-gray"))
    879             return false;
    880         this._dragStartX = event.pageX;
    881         return true;
    882     },
    883 
    884     /**
    885      * @param {Event} event
    886      */
    887     _tabDragging: function(event)
    888     {
    889         var tabElements = this._tabbedPane._tabsElement.childNodes;
    890         for (var i = 0; i < tabElements.length; ++i) {
    891             var tabElement = tabElements[i];
    892             if (tabElement === this._tabElement)
    893                 continue;
    894 
    895             var intersects = tabElement.offsetLeft + tabElement.clientWidth > this._tabElement.offsetLeft &&
    896                 this._tabElement.offsetLeft + this._tabElement.clientWidth > tabElement.offsetLeft;
    897             if (!intersects)
    898                 continue;
    899 
    900             if (Math.abs(event.pageX - this._dragStartX) < tabElement.clientWidth / 2 + 5)
    901                 break;
    902 
    903             if (event.pageX - this._dragStartX > 0) {
    904                 tabElement = tabElement.nextSibling;
    905                 ++i;
    906             }
    907 
    908             var oldOffsetLeft = this._tabElement.offsetLeft;
    909             this._tabbedPane._insertBefore(this, i);
    910             this._dragStartX += this._tabElement.offsetLeft - oldOffsetLeft;
    911             break;
    912         }
    913 
    914         if (!this._tabElement.previousSibling && event.pageX - this._dragStartX < 0) {
    915             this._tabElement.style.setProperty("left", "0px");
    916             return;
    917         }
    918         if (!this._tabElement.nextSibling && event.pageX - this._dragStartX > 0) {
    919             this._tabElement.style.setProperty("left", "0px");
    920             return;
    921         }
    922 
    923         this._tabElement.style.setProperty("position", "relative");
    924         this._tabElement.style.setProperty("left", (event.pageX - this._dragStartX) + "px");
    925     },
    926 
    927     /**
    928      * @param {Event} event
    929      */
    930     _endTabDragging: function(event)
    931     {
    932         this._tabElement.style.removeProperty("position");
    933         this._tabElement.style.removeProperty("left");
    934         delete this._dragStartX;
    935     }
    936 }
    937 
    938 /**
    939  * @interface
    940  */
    941 WebInspector.TabbedPaneTabDelegate = function()
    942 {
    943 }
    944 
    945 WebInspector.TabbedPaneTabDelegate.prototype = {
    946     /**
    947      * @param {WebInspector.TabbedPane} tabbedPane
    948      * @param {Array.<string>} ids
    949      */
    950     closeTabs: function(tabbedPane, ids) { }
    951 }
    952