blob: a65f677f3549fa697672cbb929c31a0bfa462e48 [file] [log] [blame]
/*
* Copyright (C) 2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
WI.TabBrowser = class TabBrowser extends WI.View
{
constructor(element, tabBar, navigationSidebar, detailsSidebar)
{
console.assert(tabBar, "Must provide a TabBar.");
super(element);
this.element.classList.add("tab-browser");
this._tabBar = tabBar;
this._navigationSidebar = navigationSidebar || null;
this._detailsSidebar = detailsSidebar || null;
if (this._navigationSidebar) {
this._navigationSidebar.addEventListener(WI.Sidebar.Event.CollapsedStateDidChange, this._handleSidebarCollapsedStateDidChange, this);
this._navigationSidebar.addEventListener(WI.Sidebar.Event.WidthDidChange, this._handleSidebarWidthDidChange, this);
}
if (this._detailsSidebar) {
this._detailsSidebar.addEventListener(WI.Sidebar.Event.CollapsedStateDidChange, this._handleSidebarCollapsedStateDidChange, this);
this._detailsSidebar.addEventListener(WI.Sidebar.Event.WidthDidChange, this._handleSidebarWidthDidChange, this);
this._detailsSidebar.addEventListener(WI.Sidebar.Event.SidebarPanelSelected, this._handleSidebarPanelSelected, this);
this._detailsSidebar.addEventListener(WI.MultiSidebar.Event.SidebarAdded, this._handleMultiSidebarSidebarAdded, this);
}
this._contentViewContainer = new WI.ContentViewContainer;
this.addSubview(this._contentViewContainer);
let showNextTab = () => { this._showNextTab(); };
let showPreviousTab = () => { this._showPreviousTab(); };
let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
let nextKey1 = isRTL ? WI.KeyboardShortcut.Key.LeftCurlyBrace : WI.KeyboardShortcut.Key.RightCurlyBrace;
let previousKey1 = isRTL ? WI.KeyboardShortcut.Key.RightCurlyBrace : WI.KeyboardShortcut.Key.LeftCurlyBrace;
this._showNextTabKeyboardShortcut1 = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, nextKey1, showNextTab);
this._showPreviousTabKeyboardShortcut1 = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, previousKey1, showPreviousTab);
let nextModifier2 = isRTL ? WI.KeyboardShortcut.Modifier.Shift : 0;
let previousModifier2 = isRTL ? 0 : WI.KeyboardShortcut.Modifier.Shift;
this._showNextTabKeyboardShortcut2 = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control | nextModifier2, WI.KeyboardShortcut.Key.Tab, showNextTab);
this._showPreviousTabKeyboardShortcut2 = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control | previousModifier2, WI.KeyboardShortcut.Key.Tab, showPreviousTab);
let previousTabKey = isRTL ? WI.KeyboardShortcut.Key.Right : WI.KeyboardShortcut.Key.Left;
let nextTabKey = isRTL ? WI.KeyboardShortcut.Key.Left : WI.KeyboardShortcut.Key.Right;
this._previousTabKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, previousTabKey, this._showPreviousTabCheckingForEditableField.bind(this));
this._previousTabKeyboardShortcut.implicitlyPreventsDefault = false;
this._nextTabKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, nextTabKey, this._showNextTabCheckingForEditableField.bind(this));
this._nextTabKeyboardShortcut.implicitlyPreventsDefault = false;
this._tabBar.addEventListener(WI.TabBar.Event.TabBarItemSelected, this._tabBarItemSelected, this);
this._tabBar.addEventListener(WI.TabBar.Event.TabBarItemAdded, this._tabBarItemAdded, this);
this._tabBar.addEventListener(WI.TabBar.Event.TabBarItemRemoved, this._tabBarItemRemoved, this);
this._recentTabContentViews = [];
this._closedTabClasses = new Set;
}
// Public
get tabBar()
{
return this._tabBar;
}
get navigationSidebar()
{
return this._navigationSidebar;
}
get detailsSidebar()
{
return this._detailsSidebar;
}
get selectedTabContentView()
{
return this._contentViewContainer.currentContentView;
}
bestTabContentViewForClass(constructor)
{
console.assert(!this.selectedTabContentView || this.selectedTabContentView === this._recentTabContentViews[0]);
for (var tabContentView of this._recentTabContentViews) {
if (tabContentView instanceof constructor)
return tabContentView;
}
return null;
}
bestTabContentViewForRepresentedObject(representedObject, options = {})
{
let shouldSaveTab = this.selectedTabContentView?.constructor.shouldSaveTab() || this.selectedTabContentView?.constructor.shouldPinTab();
console.assert(!this.selectedTabContentView || this.selectedTabContentView === this._recentTabContentViews[0] || !shouldSaveTab);
let tabContentView = this._recentTabContentViews.find((tabContentView) => tabContentView.type === options.preferredTabType);
if (tabContentView && tabContentView.canShowRepresentedObject(representedObject))
return tabContentView;
for (let tabContentView of this._recentTabContentViews) {
if (options.ignoreSearchTab && tabContentView instanceof WI.SearchTabContentView)
continue;
if (options.ignoreNetworkTab && tabContentView instanceof WI.NetworkTabContentView)
continue;
if (tabContentView.canShowRepresentedObject(representedObject))
return tabContentView;
}
return null;
}
addTabForContentView(tabContentView, options = {})
{
console.assert(tabContentView instanceof WI.TabContentView);
if (!(tabContentView instanceof WI.TabContentView))
return false;
let tabBarItem = tabContentView.tabBarItem;
console.assert(tabBarItem instanceof WI.TabBarItem);
if (!(tabBarItem instanceof WI.TabBarItem))
return false;
if (tabBarItem.representedObject !== tabContentView)
tabBarItem.representedObject = tabContentView;
if (tabBarItem.parentTabBar === this._tabBar)
return true;
// Add the tab after the first tab content view, since the first
// tab content view is the currently selected one.
if (this._recentTabContentViews.length && this.selectedTabContentView)
this._recentTabContentViews.splice(1, 0, tabContentView);
else
this._recentTabContentViews.push(tabContentView);
if (typeof options.insertionIndex === "number")
this._tabBar.insertTabBarItem(tabBarItem, options.insertionIndex, options);
else
this._tabBar.addTabBarItem(tabBarItem, options);
console.assert(this._recentTabContentViews.length === this._tabBar.tabCount);
let shouldSaveTab = this.selectedTabContentView?.constructor.shouldSaveTab() || this.selectedTabContentView?.constructor.shouldPinTab();
console.assert(!this.selectedTabContentView || this.selectedTabContentView === this._recentTabContentViews[0] || !shouldSaveTab);
return true;
}
showTabForContentView(tabContentView, options = {})
{
if (!this.addTabForContentView(tabContentView, options))
return false;
this._tabBar.selectTabBarItem(tabContentView.tabBarItem, options);
// FIXME: this is a workaround for <https://webkit.org/b/151876>.
// Without this extra call, we might never lay out the child tab
// if it has already marked itself as dirty in the same run loop
// as it is attached. It will schedule a layout, but when the rAF
// fires the parent will abort the layout because the counter is
// out of sync.
this.needsLayout();
return true;
}
closeTabForContentView(tabContentView, options = {})
{
console.assert(tabContentView instanceof WI.TabContentView);
if (!(tabContentView instanceof WI.TabContentView))
return false;
console.assert(tabContentView.tabBarItem instanceof WI.TabBarItem);
if (!(tabContentView.tabBarItem instanceof WI.TabBarItem))
return false;
if (tabContentView.tabBarItem.parentTabBar !== this._tabBar)
return false;
this._tabBar.removeTabBarItem(tabContentView.tabBarItem, options);
let shouldSaveTab = this.selectedTabContentView?.constructor.shouldSaveTab() || this.selectedTabContentView?.constructor.shouldPinTab();
console.assert(this._recentTabContentViews.length === this._tabBar.tabCount);
console.assert(!this.selectedTabContentView || this.selectedTabContentView === this._recentTabContentViews[0] || !shouldSaveTab);
return true;
}
// Protected
sizeDidChange()
{
super.sizeDidChange();
for (let tabContentView of this._recentTabContentViews)
tabContentView[WI.TabBrowser.NeedsResizeLayoutSymbol] = tabContentView !== this.selectedTabContentView;
}
// Private
_tabBarItemSelected(event)
{
this._saveFocusedNodeForTabContentView(event.data.previousTabBarItem ? event.data.previousTabBarItem.representedObject : null);
let tabContentView = this._tabBar.selectedTabBarItem ? this._tabBar.selectedTabBarItem.representedObject : null;
if (tabContentView) {
let tabClass = tabContentView.constructor;
let shouldSaveTab = tabClass.shouldSaveTab() || tabClass.shouldPinTab();
if (shouldSaveTab) {
this._recentTabContentViews.remove(tabContentView);
this._recentTabContentViews.unshift(tabContentView);
}
this._contentViewContainer.showContentView(tabContentView);
console.assert(this.selectedTabContentView);
console.assert(this._recentTabContentViews.length === this._tabBar.tabCount);
console.assert(this.selectedTabContentView === this._recentTabContentViews[0] || !shouldSaveTab);
} else {
this._contentViewContainer.closeAllContentViews();
console.assert(!this.selectedTabContentView);
}
this._showNavigationSidebarPanelForTabContentView(tabContentView);
this._showDetailsSidebarPanelsForTabContentView(tabContentView);
// If the tab browser was resized prior to showing the tab, the new tab needs to perform a resize layout.
if (tabContentView && tabContentView[WI.TabBrowser.NeedsResizeLayoutSymbol]) {
tabContentView[WI.TabBrowser.NeedsResizeLayoutSymbol] = false;
tabContentView.updateLayout(WI.View.LayoutReason.Resize);
}
let outgoingTab = event.data.previousTabBarItem ? event.data.previousTabBarItem.representedObject : null;
let incomingTab = tabContentView;
let initiator = event.data.initiatorHint || WI.TabBrowser.TabNavigationInitiator.Unknown;
this.dispatchEventToListeners(WI.TabBrowser.Event.SelectedTabContentViewDidChange, {outgoingTab, incomingTab, initiator});
this._restoreFocusedNodeForTabContentView(tabContentView);
}
_tabBarItemAdded(event)
{
let tabContentView = event.data.tabBarItem.representedObject;
console.assert(tabContentView);
if (!tabContentView)
return;
this._closedTabClasses.delete(tabContentView.constructor);
}
_tabBarItemRemoved(event)
{
let tabContentView = event.data.tabBarItem.representedObject;
console.assert(tabContentView);
if (!tabContentView)
return;
this._recentTabContentViews.remove(tabContentView);
if (tabContentView.constructor.shouldSaveTab())
this._closedTabClasses.add(tabContentView.constructor);
this._contentViewContainer.closeContentView(tabContentView);
let shouldSaveTab = this.selectedTabContentView?.constructor.shouldSaveTab() || this.selectedTabContentView?.constructor.shouldPinTab();
console.assert(this._recentTabContentViews.length === this._tabBar.tabCount);
console.assert(!this.selectedTabContentView || this.selectedTabContentView === this._recentTabContentViews[0] || !shouldSaveTab);
}
_handleSidebarPanelSelected(event)
{
if (this._ignoreSidebarEvents)
return;
var tabContentView = this.selectedTabContentView;
if (!tabContentView)
return;
console.assert(event.target === this._detailsSidebar);
if (tabContentView.managesDetailsSidebarPanels)
return;
var selectedSidebarPanel = this._detailsSidebar.selectedSidebarPanel;
tabContentView.detailsSidebarSelectedPanelSetting.value = selectedSidebarPanel?.identifier ?? null;
}
_handleSidebarCollapsedStateDidChange(event)
{
if (this._ignoreSidebarEvents)
return;
var tabContentView = this.selectedTabContentView;
if (!tabContentView)
return;
if (event.target === this._navigationSidebar && !tabContentView.managesNavigationSidebarPanel)
tabContentView.navigationSidebarCollapsedSetting.value = this._navigationSidebar.collapsed;
else if (event.target === this._detailsSidebar && !tabContentView.managesDetailsSidebarPanels)
tabContentView.detailsSidebarCollapsedSetting.value = this._detailsSidebar.collapsed;
}
_handleSidebarWidthDidChange(event)
{
if (this._ignoreSidebarEvents || !event.data)
return;
let tabContentView = this.selectedTabContentView;
if (!tabContentView)
return;
switch (event.target) {
case this._navigationSidebar:
tabContentView.navigationSidebarWidthSetting.value = event.data.newWidth;
break;
case this._detailsSidebar:
if (event.data.sidebar && event.data.newWidth) {
let identifier = event.data.sidebar === this._detailsSidebar.primarySidebar ? WI.TabBrowser.SidebarWidthSettingPrimarySidebarIdentifier : (event.data.sidebar.sidebarPanels[0]?.identifier || null);
if (identifier) {
tabContentView.detailsSidebarWidthSetting.value[identifier] = event.data.newWidth;
tabContentView.detailsSidebarWidthSetting.save();
}
}
break;
}
}
_handleMultiSidebarSidebarAdded(event)
{
let tabContentView = this.selectedTabContentView;
if (!tabContentView)
return;
if (event.target !== this._detailsSidebar)
return;
let sidebar = event.data.sidebar;
let identifier = event.data.sidebar === this._detailsSidebar.primarySidebar ? WI.TabBrowser.SidebarWidthSettingPrimarySidebarIdentifier : (event.data.sidebar.sidebarPanels[0]?.identifier || null);
sidebar.width = tabContentView.detailsSidebarWidthSetting.value[identifier] || WI.TabContentView.DefaultSidebarWidth;
}
_saveFocusedNodeForTabContentView(tabContentView)
{
if (!tabContentView)
return;
if (!WI.isContentAreaFocused())
return;
tabContentView[WI.TabBrowser.FocusedNodeSymbol] = document.activeElement;
}
_restoreFocusedNodeForTabContentView(tabContentView)
{
if (!tabContentView)
return;
let node = tabContentView[WI.TabBrowser.FocusedNodeSymbol];
if (node && !WI.isContentAreaFocused())
node.focus();
tabContentView[WI.TabBrowser.FocusedNodeSymbol] = null;
}
_showNavigationSidebarPanelForTabContentView(tabContentView)
{
if (!this._navigationSidebar)
return;
this._ignoreSidebarEvents = true;
this._navigationSidebar.removeSidebarPanel(0);
console.assert(!this._navigationSidebar.sidebarPanels.length);
if (!tabContentView) {
this._ignoreSidebarEvents = false;
return;
}
if (tabContentView.navigationSidebarWidthSetting.value)
this._navigationSidebar.width = tabContentView.navigationSidebarWidthSetting.value;
var navigationSidebarPanel = tabContentView.navigationSidebarPanel;
if (!navigationSidebarPanel) {
this._navigationSidebar.collapsed = true;
this._ignoreSidebarEvents = false;
return;
}
if (tabContentView.managesNavigationSidebarPanel) {
tabContentView.showNavigationSidebarPanel();
this._ignoreSidebarEvents = false;
return;
}
this._navigationSidebar.addSidebarPanel(navigationSidebarPanel);
this._navigationSidebar.selectedSidebarPanel = navigationSidebarPanel;
this._navigationSidebar.collapsed = tabContentView.navigationSidebarCollapsedSetting.value;
this._ignoreSidebarEvents = false;
}
_showDetailsSidebarPanelsForTabContentView(tabContentView)
{
if (!this._detailsSidebar)
return;
this._ignoreSidebarEvents = true;
for (var i = this._detailsSidebar.sidebarPanels.length - 1; i >= 0; --i)
this._detailsSidebar.removeSidebarPanel(i);
console.assert(!this._detailsSidebar.sidebarPanels.length);
if (!tabContentView) {
this._ignoreSidebarEvents = false;
return;
}
for (let sidebar of this._detailsSidebar.sidebars) {
let identifier = sidebar === this._detailsSidebar.primarySidebar ? WI.TabBrowser.SidebarWidthSettingPrimarySidebarIdentifier : (sidebar.sidebarPanels[0]?.identifier || null);
sidebar.width = tabContentView.detailsSidebarWidthSetting.value[identifier] || WI.TabContentView.DefaultSidebarWidth;
}
this._detailsSidebar.allowMultipleSidebars = tabContentView.allowMultipleDetailSidebars;
if (tabContentView.managesDetailsSidebarPanels) {
tabContentView.showDetailsSidebarPanels();
this._ignoreSidebarEvents = false;
return;
}
var detailsSidebarPanels = tabContentView.detailsSidebarPanels;
if (!detailsSidebarPanels) {
this._detailsSidebar.collapsed = true;
this._ignoreSidebarEvents = false;
return;
}
for (var detailsSidebarPanel of detailsSidebarPanels)
this._detailsSidebar.addSidebarPanel(detailsSidebarPanel);
this._detailsSidebar.selectedSidebarPanel = tabContentView.detailsSidebarSelectedPanelSetting.value || detailsSidebarPanels[0];
this._detailsSidebar.collapsed = tabContentView.detailsSidebarCollapsedSetting.value || !detailsSidebarPanels.length;
this._ignoreSidebarEvents = false;
}
_showPreviousTab(event)
{
this._tabBar.selectPreviousTab();
}
_showNextTab(event)
{
this._tabBar.selectNextTab();
}
_showNextTabCheckingForEditableField(event)
{
if (WI.isEventTargetAnEditableField(event))
return;
this._showNextTab(event);
event.preventDefault();
}
_showPreviousTabCheckingForEditableField(event)
{
if (WI.isEventTargetAnEditableField(event))
return;
this._showPreviousTab(event);
event.preventDefault();
}
};
WI.TabBrowser.NeedsResizeLayoutSymbol = Symbol("needs-resize-layout");
WI.TabBrowser.FocusedNodeSymbol = Symbol("focused-node");
WI.TabBrowser.SidebarWidthSettingPrimarySidebarIdentifier = "primary-sidebar";
WI.TabBrowser.TabNavigationInitiator = {
// Initiated by clicking on the TabBar UI (switching, opening, closing).
TabClick: "tab-browser-tab-navigation-initiator-tab-click",
// Initiated by clicking a URL, symbol, go-to-arrow, or other link to a resource/source code location.
LinkClick: "tab-browser-tab-navigation-initiator-link-click",
// Initiated by clicking miscellaneous UI (i.e., Quick Console's chevron, New Tab Tab's buttons).
ButtonClick: "tab-browser-tab-navigation-initiator-button-click",
// Initiated by selecting a context menu item in Web Inspector (i.e., "Reveal in Network Tab").
ContextMenu: "tab-browser-tab-navigation-initiator-context-menu",
// Initiated by clicking a dashboard element.
Dashboard: "tab-browser-tab-navigation-initiator-dashboard",
// Initiated by automatically switching tabs when a breakpoint is hit.
Breakpoint: "tab-browser-tab-navigation-initiator-breakpoint",
// Initiated by inspecting a DOM element, database, or other object via Console API's inspect() or live node selection.
Inspect: "tab-browser-tab-navigation-initiator-inspect",
// Initiated by keyboard shortcut (tab switching, new tab, search bar).
KeyboardShortcut: "tab-browser-tab-navigation-initiator-keyboard-shortcut",
// Initiated from outside of Web Inspector (Develop Menu, _WKInspector SPI).
FrontendAPI: "tab-browser-tab-navigation-initiator-frontend-api",
// Uncategorized; these should be investigated and categorized as one of the above.
Unknown: "tab-browser-tab-navigation-initiator-unknown"
}
WI.TabBrowser.Event = {
SelectedTabContentViewDidChange: "tab-browser-selected-tab-content-view-did-change"
};