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._retainTabOrder && 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 = document.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 = document.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 = document.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 1147 this._tabbedPane.setRetainTabOrder(true, self.runtime.orderComparator(extensionPoint, "name", "order")); 1148 this._tabbedPane.addEventListener(WebInspector.TabbedPane.EventTypes.TabSelected, this._tabSelected, this); 1149 /** @type {!StringMap.<?WebInspector.View>} */ 1150 this._views = new StringMap(); 1151 this._initialize(); 1152 } 1153 1154 WebInspector.ExtensibleTabbedPaneController.prototype = { 1155 _initialize: function() 1156 { 1157 /** @type {!StringMap.<!Runtime.Extension>} */ 1158 this._extensions = new StringMap(); 1159 var extensions = self.runtime.extensions(this._extensionPoint); 1160 1161 for (var i = 0; i < extensions.length; ++i) { 1162 var descriptor = extensions[i].descriptor(); 1163 var id = descriptor["name"]; 1164 var title = WebInspector.UIString(descriptor["title"]); 1165 var settingName = descriptor["setting"]; 1166 var setting = settingName ? /** @type {!WebInspector.Setting|undefined} */ (WebInspector.settings[settingName]) : null; 1167 1168 this._extensions.set(id, extensions[i]); 1169 1170 if (setting) { 1171 setting.addChangeListener(this._toggleSettingBasedView.bind(this, id, title, setting)); 1172 if (setting.get()) 1173 this._tabbedPane.appendTab(id, title, new WebInspector.View()); 1174 } else { 1175 this._tabbedPane.appendTab(id, title, new WebInspector.View()); 1176 } 1177 } 1178 }, 1179 1180 /** 1181 * @param {string} id 1182 * @param {string} title 1183 * @param {!WebInspector.Setting} setting 1184 */ 1185 _toggleSettingBasedView: function(id, title, setting) 1186 { 1187 this._tabbedPane.closeTab(id); 1188 if (setting.get()) 1189 this._tabbedPane.appendTab(id, title, new WebInspector.View()); 1190 }, 1191 1192 /** 1193 * @param {!WebInspector.Event} event 1194 */ 1195 _tabSelected: function(event) 1196 { 1197 var tabId = this._tabbedPane.selectedTabId; 1198 if (!tabId) 1199 return; 1200 var view = this.viewForId(tabId); 1201 if (view) 1202 this._tabbedPane.changeTabView(tabId, view); 1203 }, 1204 1205 /** 1206 * @return {!Array.<string>} 1207 */ 1208 viewIds: function() 1209 { 1210 return this._extensions.keys(); 1211 }, 1212 1213 /** 1214 * @param {string} id 1215 * @return {?WebInspector.View} 1216 */ 1217 viewForId: function(id) 1218 { 1219 if (this._views.has(id)) 1220 return /** @type {!WebInspector.View} */ (this._views.get(id)); 1221 var view = this._extensions.has(id) ? /** @type {!WebInspector.View} */ (this._extensions.get(id).instance()) : null; 1222 this._views.set(id, view); 1223 if (this._viewCallback && view) 1224 this._viewCallback(id, view); 1225 return view; 1226 } 1227 } 1228