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