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 */
35WebInspector.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
55WebInspector.TabbedPane.EventTypes = {
56    TabSelected: "TabSelected",
57    TabClosed: "TabClosed"
58}
59
60WebInspector.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 */
749WebInspector.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
762WebInspector.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 */
1086WebInspector.TabbedPaneTabDelegate = function()
1087{
1088}
1089
1090WebInspector.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 */
1104WebInspector.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
1117WebInspector.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