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