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