Files
react-native-devtools-frontend/front_end/ui/ViewManager.js
T
Tim van der Lippe e4d64e7ae0 Remove unrestricted from comments that include other annotations
For all comments that contained both the unrestricted annotation
and any other potential annotation, remove only the unrestricted
annotation.

R=szuend@chromium.org

No-Presubmit: True
Bug: 1011811
Change-Id: I27dd67f657cdb1a9233d768eea81cabe2591f5f7
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2569759
Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
2020-12-02 15:31:52 +00:00

1077 lines
29 KiB
JavaScript

// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../common/common.js';
import * as Host from '../host/host.js';
import * as Root from '../root/root.js';
import * as ARIAUtils from './ARIAUtils.js';
import {ContextMenu} from './ContextMenu.js'; // eslint-disable-line no-unused-vars
import {Icon} from './Icon.js';
import {Events as TabbedPaneEvents, TabbedPane} from './TabbedPane.js';
import {ItemsProvider, Toolbar, ToolbarItem, ToolbarMenuButton} from './Toolbar.js'; // eslint-disable-line no-unused-vars
import {createTextChild} from './UIUtils.js';
import {ProvidedView, TabbedViewLocation, View, ViewLocation, ViewLocationResolver} from './View.js'; // eslint-disable-line no-unused-vars
import {VBox, Widget} from './Widget.js'; // eslint-disable-line no-unused-vars
/** @type {!Array<!PreRegisteredView>} */
const registeredViewExtensions = [];
/** @enum {string} */
export const ViewPersistence = {
CLOSEABLE: 'closeable',
PERMANENT: 'permanent',
TRANSIENT: 'transient',
};
/** @enum {string} */
export const ViewLocationValues = {
PANEL: 'panel',
SETTINGS_VIEW: 'settings-view',
ELEMENTS_SIDEBAR: 'elements-sidebar',
SOURCES_SIDEBAR_BOTTOM: 'sources.sidebar-bottom'
};
/**
* @typedef {{
* experiment: (!Root.Runtime.ExperimentName|undefined),
* condition: (!Root.Runtime.ConditionName|undefined),
* title: string,
* persistence: (!ViewPersistence|undefined),
* id: string,
* location: (!ViewLocationValues|undefined),
* hasToolbar: (boolean|undefined),
* loadView: function():!Promise<!Widget>,
* order: (number|undefined),
* settings: (!Array<string>|undefined),
* tags: (string|undefined),
* }}
*/
// @ts-ignore typedef
export let ViewRegistration;
/**
* @implements {View}
*/
class PreRegisteredView {
/**
* @param {!ViewRegistration} viewRegistration
*/
constructor(viewRegistration) {
/** @type {!ViewRegistration} */
this._viewRegistration = viewRegistration;
this._widgetRequested = false;
}
/**
* @override
*/
title() {
return this._viewRegistration.title;
}
/**
* @override
*/
isCloseable() {
return this._viewRegistration.persistence === ViewPersistence.CLOSEABLE;
}
/**
* @override
*/
isTransient() {
return this._viewRegistration.persistence === ViewPersistence.TRANSIENT;
}
/**
* @override
*/
viewId() {
return this._viewRegistration.id;
}
location() {
return this._viewRegistration.location;
}
order() {
return this._viewRegistration.order;
}
settings() {
return this._viewRegistration.settings;
}
tags() {
return this._viewRegistration.tags;
}
persistence() {
return this._viewRegistration.persistence;
}
/**
* @override
*/
async toolbarItems() {
if (this._viewRegistration.hasToolbar) {
return this.widget().then(widget => /** @type {!ItemsProvider} */ (/** @type {*} */ (widget)).toolbarItems());
}
return [];
}
/**
* @override
*/
async widget() {
this._widgetRequested = true;
return this._viewRegistration.loadView();
}
/**
* @override
*/
async disposeView() {
if (!this._widgetRequested) {
return;
}
const widget = await this.widget();
await widget.ownerViewDisposed();
}
experiment() {
return this._viewRegistration.experiment;
}
condition() {
return this._viewRegistration.condition;
}
}
/**
* @param {!ViewRegistration} registration
*/
export function registerViewExtension(registration) {
registeredViewExtensions.push(new PreRegisteredView(registration));
}
/**
* @return {!Array<!PreRegisteredView>}
*/
export function getRegisteredViewExtensions() {
return registeredViewExtensions.filter(
view => Root.Runtime.Runtime.isDescriptorEnabled({experiment: view.experiment(), condition: view.condition()}));
}
/**
* @type {!ViewManager}
*/
let viewManagerInstance;
export class ViewManager {
/**
* @private
*/
constructor() {
/** @type {!Map<string, !View>} */
this._views = new Map();
/** @type {!Map<string, string>} */
this._locationNameByViewId = new Map();
// Read override setting for location
this._locationOverrideSetting = Common.Settings.Settings.instance().createSetting('viewsLocationOverride', {});
const preferredExtensionLocations = this._locationOverrideSetting.get();
/** @type {!Array<{viewId: string, view: (!ProvidedView|!PreRegisteredView), location: (string|null)}>} */
const unionOfViewExtensions = [
// TODO(crbug.com/1134103): Remove this call when all views are migrated
...Root.Runtime.Runtime.instance().extensions('view').map(extension => {
return {
viewId: extension.descriptor().id,
location: extension.descriptor()['location'],
view: new ProvidedView(extension),
};
}),
...getRegisteredViewExtensions().map(registeredView => {
return {
viewId: registeredView.viewId(),
location: registeredView.location() || null,
view: registeredView,
};
}),
];
// All views define their initial ordering. When the user has not reordered, we use the
// default ordering as defined by the views themselves.
unionOfViewExtensions.sort((firstView, secondView) => {
const firstViewOrder = firstView.view.order();
const secondViewOrder = secondView.view.order();
if (firstViewOrder && secondViewOrder) {
return firstViewOrder - secondViewOrder;
}
return 0;
});
for (const {viewId, view, location} of unionOfViewExtensions) {
this._views.set(viewId, view);
// Use the preferred user location if available
const locationName = preferredExtensionLocations[viewId] || location;
this._locationNameByViewId.set(viewId, locationName);
}
}
/**
* @param {{forceNew: ?boolean}} opts
*/
static instance(opts = {forceNew: null}) {
const {forceNew} = opts;
if (!viewManagerInstance || forceNew) {
viewManagerInstance = new ViewManager();
}
return viewManagerInstance;
}
/**
* @param {!Array<!ToolbarItem>} toolbarItems
* @return {?Element}
*/
static _createToolbar(toolbarItems) {
if (!toolbarItems.length) {
return null;
}
const toolbar = new Toolbar('');
for (const item of toolbarItems) {
toolbar.appendToolbarItem(item);
}
return toolbar.element;
}
/**
* @param {string} viewId
* @returns {string}
*/
locationNameForViewId(viewId) {
const locationName = this._locationNameByViewId.get(viewId);
if (!locationName) {
throw new Error(`No location name for view with id ${viewId}`);
}
return locationName;
}
/**
* Moves a view to a new location
* @param {string} viewId
* @param {string} locationName
* @param {{shouldSelectTab: (boolean), overrideSaving: (boolean)}=} options - Optional parameters for selecting tab and override saving
*/
moveView(viewId, locationName, options) {
const defaultOptions = {shouldSelectTab: true, overrideSaving: false};
const {shouldSelectTab, overrideSaving} = options || defaultOptions;
if (!viewId || !locationName) {
return;
}
const view = this.view(viewId);
if (!view) {
return;
}
if (!overrideSaving) {
// Update the inner map of locations
this._locationNameByViewId.set(viewId, locationName);
// Update the settings of location overwrites
const locations = this._locationOverrideSetting.get();
locations[viewId] = locationName;
this._locationOverrideSetting.set(locations);
}
// Find new location and show view there
this.resolveLocation(locationName).then(location => {
if (!location) {
throw new Error('Move view: Could not resolve location for view: ' + viewId);
}
location._reveal();
return location.showView(view, undefined, /* userGesture*/ true, /* omitFocus*/ false, shouldSelectTab);
});
}
/**
* @param {!View} view
* @return {!Promise<void>}
*/
revealView(view) {
const location = locationForView.get(view);
if (!location) {
return Promise.resolve();
}
location._reveal();
return location.showView(view);
}
/**
* Show view in location
* @param {string} viewId
* @param {string} locationName
* @param {boolean=} shouldSelectTab
*/
showViewInLocation(viewId, locationName, shouldSelectTab = true) {
this.moveView(viewId, locationName, {
shouldSelectTab,
overrideSaving: true,
});
}
/**
* @param {string} viewId
* @return {?View}
*/
view(viewId) {
const view = this._views.get(viewId);
if (!view) {
throw new Error(`No view with id ${viewId} found!`);
}
return view;
}
/**
* @param {string} viewId
* @return {?Widget}
*/
materializedWidget(viewId) {
const view = this.view(viewId);
if (!view) {
return null;
}
return widgetForView.get(view) || null;
}
/**
* @param {string} viewId
* @param {boolean=} userGesture
* @param {boolean=} omitFocus
* @return {!Promise<void>}
*/
showView(viewId, userGesture, omitFocus) {
const view = this._views.get(viewId);
if (!view) {
console.error('Could not find view for id: \'' + viewId + '\' ' + new Error().stack);
return Promise.resolve();
}
const locationName = this._locationNameByViewId.get(viewId);
const location = locationForView.get(view);
if (location) {
location._reveal();
return location.showView(view, undefined, userGesture, omitFocus);
}
return this.resolveLocation(locationName).then(location => {
if (!location) {
throw new Error('Could not resolve location for view: ' + viewId);
}
location._reveal();
return location.showView(view, undefined, userGesture, omitFocus);
});
}
/**
* @param {string=} location
* @return {!Promise<?_Location>}
*/
async resolveLocation(location) {
if (!location) {
return /** @type {!Promise<?_Location>} */ (Promise.resolve(null));
}
const resolverExtensions = Root.Runtime.Runtime.instance()
.extensions(ViewLocationResolver)
.filter(extension => extension.descriptor()['name'] === location);
if (!resolverExtensions.length) {
throw new Error('Unresolved location: ' + location);
}
const resolverExtension = resolverExtensions[0];
const resolver = /** @type {!ViewLocationResolver} */ (await resolverExtension.instance());
return /** @type {?_Location} */ (resolver.resolveLocation(location));
}
/**
* @param {function():void=} revealCallback
* @param {string=} location
* @param {boolean=} restoreSelection
* @param {boolean=} allowReorder
* @param {?string=} defaultTab
* @return {!TabbedViewLocation}
*/
createTabbedLocation(revealCallback, location, restoreSelection, allowReorder, defaultTab) {
return new _TabbedLocation(this, revealCallback, location, restoreSelection, allowReorder, defaultTab);
}
/**
* @param {function():void=} revealCallback
* @param {string=} location
* @return {!ViewLocation}
*/
createStackLocation(revealCallback, location) {
return new _StackLocation(this, revealCallback, location);
}
/**
* @param {string} location
* @return {boolean}
*/
hasViewsForLocation(location) {
return !!this._viewsForLocation(location).length;
}
/**
* @param {string} location
* @return {!Array<!View>}
*/
_viewsForLocation(location) {
const result = [];
for (const [id, view] of this._views.entries()) {
if (this._locationNameByViewId.get(id) === location) {
result.push(view);
}
}
return result;
}
}
/** @type {!WeakMap<!View, !Widget>} */
const widgetForView = new WeakMap();
export class ContainerWidget extends VBox {
/**
* @param {!View} view
*/
constructor(view) {
super();
this.element.classList.add('flex-auto', 'view-container', 'overflow-auto');
this._view = view;
this.element.tabIndex = -1;
ARIAUtils.markAsTabpanel(this.element);
ARIAUtils.setAccessibleName(this.element, ls`${view.title()} panel`);
this.setDefaultFocusedElement(this.element);
}
/**
* @return {!Promise<*>}
*/
_materialize() {
if (this._materializePromise) {
return this._materializePromise;
}
const promises = [];
// TODO(crbug.com/1006759): Transform to async-await
promises.push(this._view.toolbarItems().then(toolbarItems => {
const toolbarElement = ViewManager._createToolbar(toolbarItems);
if (toolbarElement) {
this.element.insertBefore(toolbarElement, this.element.firstChild);
}
}));
promises.push(this._view.widget().then(widget => {
// Move focus from |this| to loaded |widget| if any.
const shouldFocus = this.element.hasFocus();
this.setDefaultFocusedElement(null);
widgetForView.set(this._view, widget);
widget.show(this.element);
if (shouldFocus) {
widget.focus();
}
}));
this._materializePromise = Promise.all(promises);
return this._materializePromise;
}
/**
* @override
*/
wasShown() {
this._materialize().then(() => {
const widget = widgetForView.get(this._view);
if (widget) {
widget.show(this.element);
this._wasShownForTest();
}
});
}
_wasShownForTest() {
// This method is sniffed in tests.
}
}
export class _ExpandableContainerWidget extends VBox {
/**
* @param {!View} view
*/
constructor(view) {
super(true);
this.element.classList.add('flex-none');
this.registerRequiredCSS('ui/viewContainers.css', {enableLegacyPatching: true});
this._titleElement = document.createElement('div');
this._titleElement.classList.add('expandable-view-title');
ARIAUtils.markAsButton(this._titleElement);
this._titleExpandIcon = Icon.create('smallicon-triangle-right', 'title-expand-icon');
this._titleElement.appendChild(this._titleExpandIcon);
const titleText = view.title();
createTextChild(this._titleElement, titleText);
ARIAUtils.setAccessibleName(this._titleElement, titleText);
ARIAUtils.setExpanded(this._titleElement, false);
this._titleElement.tabIndex = 0;
self.onInvokeElement(this._titleElement, this._toggleExpanded.bind(this));
this._titleElement.addEventListener('keydown', this._onTitleKeyDown.bind(this), false);
this.contentElement.insertBefore(this._titleElement, this.contentElement.firstChild);
ARIAUtils.setControls(this._titleElement, this.contentElement.createChild('slot'));
this._view = view;
expandableContainerForView.set(view, this);
}
/**
* @override
*/
wasShown() {
if (this._widget && this._materializePromise) {
this._materializePromise.then(() => {
if (this._titleElement.classList.contains('expanded') && this._widget) {
this._widget.show(this.element);
}
});
}
}
/**
* @return {!Promise<*>}
*/
_materialize() {
if (this._materializePromise) {
return this._materializePromise;
}
// TODO(crbug.com/1006759): Transform to async-await
const promises = [];
promises.push(this._view.toolbarItems().then(toolbarItems => {
const toolbarElement = ViewManager._createToolbar(toolbarItems);
if (toolbarElement) {
this._titleElement.appendChild(toolbarElement);
}
}));
promises.push(this._view.widget().then(widget => {
this._widget = widget;
widgetForView.set(this._view, widget);
widget.show(this.element);
}));
this._materializePromise = Promise.all(promises);
return this._materializePromise;
}
/**
* @return {!Promise<*>}
*/
_expand() {
if (this._titleElement.classList.contains('expanded')) {
return this._materialize();
}
this._titleElement.classList.add('expanded');
ARIAUtils.setExpanded(this._titleElement, true);
this._titleExpandIcon.setIconType('smallicon-triangle-down');
return this._materialize().then(() => {
if (this._widget) {
this._widget.show(this.element);
}
});
}
_collapse() {
if (!this._titleElement.classList.contains('expanded')) {
return;
}
this._titleElement.classList.remove('expanded');
ARIAUtils.setExpanded(this._titleElement, false);
this._titleExpandIcon.setIconType('smallicon-triangle-right');
this._materialize().then(() => {
if (this._widget) {
this._widget.detach();
}
});
}
/**
* @param {!Event} event
*/
_toggleExpanded(event) {
if (event.type === 'keydown' && event.target !== this._titleElement) {
return;
}
if (this._titleElement.classList.contains('expanded')) {
this._collapse();
} else {
this._expand();
}
}
/**
* @param {!Event} event
*/
_onTitleKeyDown(event) {
if (event.target !== this._titleElement) {
return;
}
const keyEvent = /** @type {!KeyboardEvent} */ (event);
if (keyEvent.key === 'ArrowLeft') {
this._collapse();
} else if (keyEvent.key === 'ArrowRight') {
if (!this._titleElement.classList.contains('expanded')) {
this._expand();
} else if (this._widget) {
this._widget.focus();
}
}
}
}
/** @type {!WeakMap<!View, !_ExpandableContainerWidget>} */
const expandableContainerForView = new WeakMap();
class _Location {
/**
* @param {!ViewManager} manager
* @param {!Widget} widget
* @param {function():void=} revealCallback
*/
constructor(manager, widget, revealCallback) {
this._manager = manager;
this._revealCallback = revealCallback;
this._widget = widget;
}
/**
* @return {!Widget}
*/
widget() {
return this._widget;
}
_reveal() {
if (this._revealCallback) {
this._revealCallback();
}
}
/**
* @param {!View} view
* @param {?View=} insertBefore
* @param {boolean=} userGesture
* @param {boolean=} omitFocus
* @param {boolean=} shouldSelectTab
* @return {!Promise<void>}
*/
showView(view, insertBefore, userGesture, omitFocus, shouldSelectTab) {
throw new Error('not implemented');
}
/**
* @param {!View} view
*/
removeView(view) {
throw new Error('not implemented');
}
}
/** @type {!WeakMap<!View, !_Location>} */
const locationForView = new WeakMap();
/**
* @implements {TabbedViewLocation}
*/
export class _TabbedLocation extends _Location {
/**
* @param {!ViewManager} manager
* @param {function():void=} revealCallback
* @param {string=} location
* @param {boolean=} restoreSelection
* @param {boolean=} allowReorder
* @param {?string=} defaultTab
*/
constructor(manager, revealCallback, location, restoreSelection, allowReorder, defaultTab) {
const tabbedPane = new TabbedPane();
if (allowReorder) {
tabbedPane.setAllowTabReorder(true);
}
super(manager, tabbedPane, revealCallback);
this._tabbedPane = tabbedPane;
this._allowReorder = allowReorder;
this._tabbedPane.addEventListener(TabbedPaneEvents.TabSelected, this._tabSelected, this);
this._tabbedPane.addEventListener(TabbedPaneEvents.TabClosed, this._tabClosed, this);
this._closeableTabSetting = Common.Settings.Settings.instance().createSetting('closeableTabs', {});
// As we give tabs the capability to be closed we also need to add them to the setting so they are still open
// until the user decide to close them
this._setOrUpdateCloseableTabsSetting();
this._tabOrderSetting = Common.Settings.Settings.instance().createSetting(location + '-tabOrder', {});
this._tabbedPane.addEventListener(TabbedPaneEvents.TabOrderChanged, this._persistTabOrder, this);
if (restoreSelection) {
this._lastSelectedTabSetting = Common.Settings.Settings.instance().createSetting(location + '-selectedTab', '');
}
this._defaultTab = defaultTab;
/** @type {!Map.<string, !View>} */
this._views = new Map();
if (location) {
this.appendApplicableItems(location);
}
}
_setOrUpdateCloseableTabsSetting() {
// Update the setting value, we respect the closed state decided by the user
// and append the new tabs with value of true so they are shown open
const defaultOptionsForTabs = {'security': true};
const tabs = this._closeableTabSetting.get();
const newClosable = Object.assign(defaultOptionsForTabs, tabs);
this._closeableTabSetting.set(newClosable);
}
/**
* @override
* @return {!Widget}
*/
widget() {
return this._tabbedPane;
}
/**
* @override
* @return {!TabbedPane}
*/
tabbedPane() {
return this._tabbedPane;
}
/**
* @override
* @return {!ToolbarMenuButton}
*/
enableMoreTabsButton() {
const moreTabsButton = new ToolbarMenuButton(this._appendTabsToMenu.bind(this));
this._tabbedPane.leftToolbar().appendToolbarItem(moreTabsButton);
this._tabbedPane.disableOverflowMenu();
return moreTabsButton;
}
/**
* @override
* @param {string} locationName
*/
appendApplicableItems(locationName) {
const views = this._manager._viewsForLocation(locationName);
if (this._allowReorder) {
let i = 0;
const persistedOrders = this._tabOrderSetting.get();
const orders = new Map();
for (const view of views) {
orders.set(view.viewId(), persistedOrders[view.viewId()] || (++i) * _TabbedLocation.orderStep);
}
views.sort((a, b) => orders.get(a.viewId()) - orders.get(b.viewId()));
}
for (const view of views) {
const id = view.viewId();
this._views.set(id, view);
locationForView.set(view, this);
if (view.isTransient()) {
continue;
}
if (!view.isCloseable()) {
this._appendTab(view);
} else if (this._closeableTabSetting.get()[id]) {
this._appendTab(view);
}
}
// If a default tab was provided we open or select it
if (this._defaultTab) {
if (this._tabbedPane.hasTab(this._defaultTab)) {
// If the tabbed pane already has the tab we just have to select it
this._tabbedPane.selectTab(this._defaultTab);
} else {
// If the tab is not present already it can be because:
// it doesn't correspond to this tabbed location
// or because it is closed
const view = Array.from(this._views.values()).find(view => view.viewId() === this._defaultTab);
if (view) {
// _defaultTab is indeed part of the views for this tabbed location
this.showView(view);
}
}
} else if (this._lastSelectedTabSetting && this._tabbedPane.hasTab(this._lastSelectedTabSetting.get())) {
this._tabbedPane.selectTab(this._lastSelectedTabSetting.get());
}
}
/**
* @param {!ContextMenu} contextMenu
*/
_appendTabsToMenu(contextMenu) {
const views = Array.from(this._views.values());
views.sort((viewa, viewb) => viewa.title().localeCompare(viewb.title()));
for (const view of views) {
const title = view.title();
if (view.viewId() === 'issues-pane') {
contextMenu.defaultSection().appendItem(title, () => {
Host.userMetrics.issuesPanelOpenedFrom(Host.UserMetrics.IssueOpener.HamburgerMenu);
this.showView(view, undefined, true);
});
continue;
}
contextMenu.defaultSection().appendItem(title, this.showView.bind(this, view, undefined, true));
}
}
/**
* @param {!View} view
* @param {number=} index
*/
_appendTab(view, index) {
this._tabbedPane.appendTab(
view.viewId(), view.title(), new ContainerWidget(view), undefined, false,
view.isCloseable() || view.isTransient(), index);
}
/**
* @override
* @param {!View} view
* @param {?View=} insertBefore
*/
appendView(view, insertBefore) {
if (this._tabbedPane.hasTab(view.viewId())) {
return;
}
const oldLocation = locationForView.get(view);
if (oldLocation && oldLocation !== this) {
oldLocation.removeView(view);
}
locationForView.set(view, this);
this._manager._views.set(view.viewId(), view);
this._views.set(view.viewId(), view);
let index = undefined;
const tabIds = this._tabbedPane.tabIds();
if (this._allowReorder) {
const orderSetting = this._tabOrderSetting.get();
const order = orderSetting[view.viewId()];
for (let i = 0; order && i < tabIds.length; ++i) {
if (orderSetting[tabIds[i]] && orderSetting[tabIds[i]] > order) {
index = i;
break;
}
}
} else if (insertBefore) {
for (let i = 0; i < tabIds.length; ++i) {
if (tabIds[i] === insertBefore.viewId()) {
index = i;
break;
}
}
}
this._appendTab(view, index);
if (view.isCloseable()) {
const tabs = this._closeableTabSetting.get();
const tabId = view.viewId();
if (!tabs[tabId]) {
tabs[tabId] = true;
this._closeableTabSetting.set(tabs);
}
}
this._persistTabOrder();
}
/**
* @override
* @param {!View} view
* @param {?View=} insertBefore
* @param {boolean=} userGesture
* @param {boolean=} omitFocus
* @param {boolean=} shouldSelectTab
* @return {!Promise<void>}
*/
async showView(view, insertBefore, userGesture, omitFocus, shouldSelectTab = true) {
this.appendView(view, insertBefore);
if (shouldSelectTab) {
this._tabbedPane.selectTab(view.viewId(), userGesture);
}
if (!omitFocus) {
this._tabbedPane.focus();
}
const widget = /** @type {!ContainerWidget} */ (this._tabbedPane.tabView(view.viewId()));
await widget._materialize();
}
/**
* @param {!View} view
* @override
*/
removeView(view) {
if (!this._tabbedPane.hasTab(view.viewId())) {
return;
}
locationForView.delete(view);
this._manager._views.delete(view.viewId());
this._tabbedPane.closeTab(view.viewId());
this._views.delete(view.viewId());
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_tabSelected(event) {
const tabId = /** @type {string} */ (event.data.tabId);
if (this._lastSelectedTabSetting && event.data['isUserGesture']) {
this._lastSelectedTabSetting.set(tabId);
}
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_tabClosed(event) {
const id = /** @type {string} */ (event.data['tabId']);
const tabs = this._closeableTabSetting.get();
if (tabs[id]) {
tabs[id] = false;
this._closeableTabSetting.set(tabs);
}
const view = this._views.get(id);
if (view) {
view.disposeView();
}
}
_persistTabOrder() {
const tabIds = this._tabbedPane.tabIds();
/** @type {!Object<string, number>} */
const tabOrders = {};
for (let i = 0; i < tabIds.length; i++) {
tabOrders[tabIds[i]] = (i + 1) * _TabbedLocation.orderStep;
}
const oldTabOrder = this._tabOrderSetting.get();
const oldTabArray = Object.keys(oldTabOrder);
oldTabArray.sort((a, b) => oldTabOrder[a] - oldTabOrder[b]);
let lastOrder = 0;
for (const key of oldTabArray) {
if (key in tabOrders) {
lastOrder = tabOrders[key];
continue;
}
tabOrders[key] = ++lastOrder;
}
this._tabOrderSetting.set(tabOrders);
}
}
_TabbedLocation.orderStep = 10; // Keep in sync with descriptors.
/**
* @implements {ViewLocation}
*/
class _StackLocation extends _Location {
/**
* @param {!ViewManager} manager
* @param {function():void=} revealCallback
* @param {string=} location
*/
constructor(manager, revealCallback, location) {
const vbox = new VBox();
super(manager, vbox, revealCallback);
this._vbox = vbox;
/** @type {!Map<string, !_ExpandableContainerWidget>} */
this._expandableContainers = new Map();
if (location) {
this.appendApplicableItems(location);
}
}
/**
* @override
* @param {!View} view
* @param {?View=} insertBefore
*/
appendView(view, insertBefore) {
const oldLocation = locationForView.get(view);
if (oldLocation && oldLocation !== this) {
oldLocation.removeView(view);
}
let container = this._expandableContainers.get(view.viewId());
if (!container) {
locationForView.set(view, this);
this._manager._views.set(view.viewId(), view);
container = new _ExpandableContainerWidget(view);
let beforeElement = null;
if (insertBefore) {
const beforeContainer = expandableContainerForView.get(insertBefore);
beforeElement = beforeContainer ? beforeContainer.element : null;
}
container.show(this._vbox.contentElement, beforeElement);
this._expandableContainers.set(view.viewId(), container);
}
}
/**
* @override
* @param {!View} view
* @param {?View=} insertBefore
* @return {!Promise<void>}
*/
async showView(view, insertBefore) {
this.appendView(view, insertBefore);
const container = this._expandableContainers.get(view.viewId());
if (container) {
await container._expand();
}
}
/**
* @param {!View} view
* @override
*/
removeView(view) {
const container = this._expandableContainers.get(view.viewId());
if (!container) {
return;
}
container.detach();
this._expandableContainers.delete(view.viewId());
locationForView.delete(view);
this._manager._views.delete(view.viewId());
}
/**
* @override
* @param {string} locationName
*/
appendApplicableItems(locationName) {
for (const view of this._manager._viewsForLocation(locationName)) {
this.appendView(view);
}
}
}