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