// ***************************************************************************** // Copyright (C) 2020 Ericsson and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { TabBar, Widget, Title } from '@theia/core/shared/@lumino/widgets'; import { Message, ContextMenuRenderer, LabelProvider, StatefulWidget, codicon } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import URI from '@theia/core/lib/common/uri'; import { FileStat } from '@theia/filesystem/lib/common/files'; import { PreferenceScopeCommandManager } from '../util/preference-scope-command-manager'; import { Preference, PreferenceMenus } from '../util/preference-types'; import { CommandRegistry, DisposableCollection, Emitter, MenuModelRegistry, PreferenceScope } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common/nls'; const USER_TAB_LABEL = nls.localizeByDefault('User'); const USER_TAB_INDEX = PreferenceScope['User']; const WORKSPACE_TAB_LABEL = nls.localizeByDefault('Workspace'); const WORKSPACE_TAB_INDEX = PreferenceScope['Workspace']; const FOLDER_TAB_LABEL = nls.localizeByDefault('Folder'); const FOLDER_TAB_INDEX = PreferenceScope['Folder']; const PREFERENCE_TAB_CLASSNAME = 'preferences-scope-tab'; const GENERAL_FOLDER_TAB_CLASSNAME = 'preference-folder'; const LABELED_FOLDER_TAB_CLASSNAME = 'preferences-folder-tab'; const FOLDER_DROPDOWN_CLASSNAME = 'preferences-folder-dropdown'; const FOLDER_DROPDOWN_ICON_CLASSNAME = 'preferences-folder-dropdown-icon ' + codicon('chevron-down'); const TABBAR_UNDERLINE_CLASSNAME = 'tabbar-underline'; const SINGLE_FOLDER_TAB_CLASSNAME = `${PREFERENCE_TAB_CLASSNAME} ${GENERAL_FOLDER_TAB_CLASSNAME} ${LABELED_FOLDER_TAB_CLASSNAME}`; const UNSELECTED_FOLDER_DROPDOWN_CLASSNAME = `${PREFERENCE_TAB_CLASSNAME} ${GENERAL_FOLDER_TAB_CLASSNAME} ${FOLDER_DROPDOWN_CLASSNAME}`; const SELECTED_FOLDER_DROPDOWN_CLASSNAME = `${PREFERENCE_TAB_CLASSNAME} ${GENERAL_FOLDER_TAB_CLASSNAME} ${LABELED_FOLDER_TAB_CLASSNAME} ${FOLDER_DROPDOWN_CLASSNAME}`; const SHADOW_CLASSNAME = 'with-shadow'; export interface PreferencesScopeTabBarState { scopeDetails: Preference.SelectedScopeDetails; } @injectable() export class PreferencesScopeTabBar extends TabBar implements StatefulWidget { static ID = 'preferences-scope-tab-bar'; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(PreferenceScopeCommandManager) protected readonly preferencesMenuFactory: PreferenceScopeCommandManager; @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @inject(MenuModelRegistry) protected readonly menuModelRegistry: MenuModelRegistry; protected readonly onScopeChangedEmitter = new Emitter(); readonly onScopeChanged = this.onScopeChangedEmitter.event; protected toDispose = new DisposableCollection(); protected folderTitle: Title; protected currentWorkspaceRoots: FileStat[] = []; protected currentSelection: Preference.SelectedScopeDetails = Preference.DEFAULT_SCOPE; protected editorScrollAtTop = true; get currentScope(): Preference.SelectedScopeDetails { return this.currentSelection; } protected setNewScopeSelection(newSelection: Preference.SelectedScopeDetails): void { const stringifiedSelectionScope = newSelection.scope.toString(); const newIndex = this.titles.findIndex(title => title.dataset.scope === stringifiedSelectionScope); if (newIndex !== -1) { this.currentSelection = newSelection; this.currentIndex = newIndex; if (newSelection.scope === PreferenceScope.Folder) { this.addOrUpdateFolderTab(); } this.emitNewScope(); } } @postConstruct() protected init(): void { this.id = PreferencesScopeTabBar.ID; this.setupInitialDisplay(); this.tabActivateRequested.connect((sender, args) => { const scopeDetails = this.toScopeDetails(args.title); if (scopeDetails) { this.setNewScopeSelection(scopeDetails); } }); this.toDispose.pushAll([ this.workspaceService.onWorkspaceChanged(newRoots => this.doUpdateDisplay(newRoots)), this.workspaceService.onWorkspaceLocationChanged(() => this.doUpdateDisplay(this.workspaceService.tryGetRoots())), ]); const tabUnderline = document.createElement('div'); tabUnderline.className = TABBAR_UNDERLINE_CLASSNAME; this.node.append(tabUnderline); } protected toScopeDetails(title?: Title | Preference.SelectedScopeDetails): Preference.SelectedScopeDetails | undefined { if (title) { const source = 'dataset' in title ? title.dataset : title; const { scope, uri, activeScopeIsFolder } = source; return { scope: Number(scope), uri: uri || undefined, activeScopeIsFolder: activeScopeIsFolder === 'true' || activeScopeIsFolder === true, }; } } protected toDataSet(scopeDetails: Preference.SelectedScopeDetails): Title.Dataset { const { scope, uri, activeScopeIsFolder } = scopeDetails; return { scope: scope.toString(), uri: uri ?? '', activeScopeIsFolder: activeScopeIsFolder.toString() }; } protected setupInitialDisplay(): void { this.addUserTab(); if (this.workspaceService.workspace) { this.addWorkspaceTab(this.workspaceService.workspace); } this.addOrUpdateFolderTab(); } protected override onUpdateRequest(msg: Message): void { super.onUpdateRequest(msg); this.addTabIndexToTabs(); } protected addTabIndexToTabs(): void { this.node.querySelectorAll('li').forEach((tab, index) => { tab.tabIndex = 0; const handler = () => { if (tab.className.includes(GENERAL_FOLDER_TAB_CLASSNAME) && this.currentWorkspaceRoots.length > 1) { const tabRect = tab.getBoundingClientRect(); this.openContextMenu(tabRect, tab, 'keypress'); } else { const details = this.toScopeDetails(this.titles[index]); if (details) { this.setNewScopeSelection(details); } } }; tab.onkeydown = handler; tab.onclick = handler; }); } protected addUserTab(): void { this.addTab(new Title({ dataset: { uri: '', scope: USER_TAB_INDEX.toString() }, label: USER_TAB_LABEL, owner: this, className: PREFERENCE_TAB_CLASSNAME })); } protected addWorkspaceTab(currentWorkspace: FileStat): Title { const scopeDetails = this.getWorkspaceDataset(currentWorkspace); const workspaceTabTitle = new Title({ dataset: this.toDataSet(scopeDetails), label: WORKSPACE_TAB_LABEL, owner: this, className: PREFERENCE_TAB_CLASSNAME, }); this.addTab(workspaceTabTitle); return workspaceTabTitle; } protected getWorkspaceDataset(currentWorkspace: FileStat): Preference.SelectedScopeDetails { const { resource, isDirectory } = currentWorkspace; const scope = WORKSPACE_TAB_INDEX; return { uri: resource.toString(), activeScopeIsFolder: isDirectory, scope }; } protected addOrUpdateFolderTab(): void { if (!!this.workspaceService.workspace) { this.currentWorkspaceRoots = this.workspaceService.tryGetRoots(); const multipleFolderRootsAreAvailable = this.currentWorkspaceRoots && this.currentWorkspaceRoots.length > 1; const noFolderRootsAreAvailable = this.currentWorkspaceRoots.length === 0; const shouldShowFoldersSeparately = this.workspaceService.saved; if (!noFolderRootsAreAvailable) { if (!this.folderTitle) { this.folderTitle = new Title({ label: '', caption: FOLDER_TAB_LABEL, owner: this, }); } this.setFolderTitleProperties(multipleFolderRootsAreAvailable); if (multipleFolderRootsAreAvailable || shouldShowFoldersSeparately) { this.addTab(this.folderTitle); } } else { const folderTabIndex = this.titles.findIndex(title => title.caption === FOLDER_TAB_LABEL); if (folderTabIndex > -1) { this.removeTabAt(folderTabIndex); } } } } protected setFolderTitleProperties(multipleFolderRootsAreAvailable: boolean): void { this.folderTitle.iconClass = multipleFolderRootsAreAvailable ? FOLDER_DROPDOWN_ICON_CLASSNAME : ''; if (this.currentSelection.scope === FOLDER_TAB_INDEX) { this.folderTitle.label = this.labelProvider.getName(new URI(this.currentSelection.uri)); this.folderTitle.dataset = this.toDataSet(this.currentSelection); this.folderTitle.className = multipleFolderRootsAreAvailable ? SELECTED_FOLDER_DROPDOWN_CLASSNAME : SINGLE_FOLDER_TAB_CLASSNAME; } else { const singleFolderRoot = this.currentWorkspaceRoots[0].resource; const singleFolderLabel = this.labelProvider.getName(singleFolderRoot); const defaultURI = multipleFolderRootsAreAvailable ? '' : singleFolderRoot.toString(); this.folderTitle.label = multipleFolderRootsAreAvailable ? FOLDER_TAB_LABEL : singleFolderLabel; this.folderTitle.className = multipleFolderRootsAreAvailable ? UNSELECTED_FOLDER_DROPDOWN_CLASSNAME : SINGLE_FOLDER_TAB_CLASSNAME; this.folderTitle.dataset = { folderTitle: 'true', scope: FOLDER_TAB_INDEX.toString(), uri: defaultURI }; } } protected folderSelectionCallback = (newScope: Preference.SelectedScopeDetails): void => { this.setNewScopeSelection(newScope); }; protected getFolderContextMenu(workspaceRoots = this.workspaceService.tryGetRoots()): void { this.preferencesMenuFactory.createFolderWorkspacesMenu(workspaceRoots, this.currentSelection.uri); } override handleEvent(): void { // Don't - the handlers are defined in PreferenceScopeTabbarWidget.addTabIndexToTabs() } protected openContextMenu(tabRect: DOMRect | ClientRect, folderTabNode: HTMLElement, source: 'click' | 'keypress'): void { const toDisposeOnHide = new DisposableCollection(); for (const root of this.workspaceService.tryGetRoots()) { const id = `set-scope-to-${root.resource.toString()}`; toDisposeOnHide.pushAll([ this.commandRegistry.registerCommand( { id }, { execute: () => this.setScope(root.resource) } ), this.menuModelRegistry.registerMenuAction(PreferenceMenus.FOLDER_SCOPE_MENU_PATH, { commandId: id, label: this.labelProvider.getName(root), } ) ]); } this.contextMenuRenderer.render({ menuPath: PreferenceMenus.FOLDER_SCOPE_MENU_PATH, anchor: { x: tabRect.left, y: tabRect.bottom }, context: folderTabNode, onHide: () => { setTimeout(() => toDisposeOnHide.dispose()); if (source === 'click') { folderTabNode.blur(); } } }); } protected doUpdateDisplay(newRoots: FileStat[]): void { const folderWasRemoved = newRoots.length < this.currentWorkspaceRoots.length; this.currentWorkspaceRoots = newRoots; if (folderWasRemoved) { const removedFolderWasSelectedScope = !this.currentWorkspaceRoots.some(root => root.resource.toString() === this.currentSelection.uri); if (removedFolderWasSelectedScope) { this.setNewScopeSelection(Preference.DEFAULT_SCOPE); } } this.updateWorkspaceTab(); this.addOrUpdateFolderTab(); } protected updateWorkspaceTab(): void { const currentWorkspace = this.workspaceService.workspace; if (currentWorkspace) { const workspaceTitle = this.titles.find(title => title.label === WORKSPACE_TAB_LABEL) ?? this.addWorkspaceTab(currentWorkspace); const scopeDetails = this.getWorkspaceDataset(currentWorkspace); workspaceTitle.dataset = this.toDataSet(scopeDetails); if (this.currentSelection.scope === PreferenceScope.Workspace) { this.setNewScopeSelection(scopeDetails); } } } protected emitNewScope(): void { this.onScopeChangedEmitter.fire(this.currentSelection); } setScope(scope: PreferenceScope.User | PreferenceScope.Workspace | URI): void { const details = scope instanceof URI ? this.getDetailsForResource(scope) : this.getDetailsForScope(scope); if (details) { this.setNewScopeSelection(details); } } protected getDetailsForScope(scope: PreferenceScope.User | PreferenceScope.Workspace): Preference.SelectedScopeDetails | undefined { const stringifiedSelectionScope = scope.toString(); const correspondingTitle = this.titles.find(title => title.dataset.scope === stringifiedSelectionScope); return this.toScopeDetails(correspondingTitle); } protected getDetailsForResource(resource: URI): Preference.SelectedScopeDetails | undefined { const parent = this.workspaceService.getWorkspaceRootUri(resource); if (!parent) { return undefined; } if (!this.workspaceService.isMultiRootWorkspaceOpened) { return this.getDetailsForScope(PreferenceScope.Workspace); } return ({ scope: PreferenceScope.Folder, uri: parent.toString(), activeScopeIsFolder: true }); } storeState(): PreferencesScopeTabBarState { return { scopeDetails: this.currentScope }; } restoreState(oldState: PreferencesScopeTabBarState): void { const scopeDetails = this.toScopeDetails(oldState.scopeDetails); if (scopeDetails) { this.setNewScopeSelection(scopeDetails); } } toggleShadow(showShadow: boolean): void { this.toggleClass(SHADOW_CLASSNAME, showShadow); } override dispose(): void { super.dispose(); this.toDispose.dispose(); } }