diff --git a/packages/preferences/src/browser/preference-contribution.ts b/packages/preferences/src/browser/preference-contribution.ts new file mode 100644 index 0000000000000..eecc6ae9691c8 --- /dev/null +++ b/packages/preferences/src/browser/preference-contribution.ts @@ -0,0 +1,224 @@ +/******************************************************************************** + * Copyright (C) 2018 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject, postConstruct, named } from 'inversify'; +import { MenuModelRegistry, CommandRegistry } from '@theia/core'; +import { + CommonMenus, + AbstractViewContribution, + CommonCommands, + KeybindingRegistry, + Widget, + PreferenceScope, + PreferenceProvider, + PreferenceService, + PreferenceItem +} from '@theia/core/lib/browser'; +import { isFirefox } from '@theia/core/lib/browser'; +import { isOSX } from '@theia/core/lib/common/os'; +import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { FileSystem } from '@theia/filesystem/lib/common'; +import { EditorManager, EditorWidget } from '@theia/editor/lib/browser'; +import URI from '@theia/core/lib/common/uri'; +import { PreferencesWidget } from './views/preference-widget'; +import { PreferencesEventService } from './util/preference-event-service'; +import { WorkspacePreferenceProvider } from './workspace-preference-provider'; +import { USER_PREFERENCE_URI } from './user-preference-provider'; +import { Preference, PreferencesCommands, PreferenceMenus } from './util/preference-types'; +import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; + +@injectable() +export class PreferencesContribution extends AbstractViewContribution { + + @inject(PreferencesEventService) protected readonly preferencesEventService: PreferencesEventService; + @inject(FileSystem) protected readonly filesystem: FileSystem; + @inject(PreferenceProvider) @named(PreferenceScope.Workspace) protected readonly workspacePreferenceProvider: WorkspacePreferenceProvider; + @inject(EditorManager) protected readonly editorManager: EditorManager; + @inject(PreferenceService) protected readonly preferenceValueRetrievalService: PreferenceService; + @inject(ClipboardService) protected readonly clipboardService: ClipboardService; + + protected preferencesScope = Preference.DEFAULT_SCOPE; + + constructor() { + super({ + widgetId: PreferencesWidget.ID, + widgetName: PreferencesWidget.LABEL, + defaultWidgetOptions: { + area: 'main', + }, + }); + } + + @postConstruct() + init(): void { + this.preferencesEventService.onTabScopeSelected.event(async e => { + const widget: PreferencesWidget = await this.widget; + this.preferencesScope = e; + widget.preferenceScope = this.preferencesScope; + }); + } + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(CommonCommands.OPEN_PREFERENCES, { + execute: () => this.openView({ reveal: true }), + }); + commands.registerCommand(PreferencesCommands.OPEN_PREFERENCES_JSON_TOOLBAR, { + isEnabled: () => true, + isVisible: w => this.withWidget(w, () => true), + execute: (preferenceNode: Preference.NodeWithValueInAllScopes) => { + this.openPreferencesJSON(preferenceNode); + } + }); + commands.registerCommand(PreferencesCommands.COPY_JSON_NAME, { + isEnabled: Preference.EditorCommandArgs.is, + isVisible: Preference.EditorCommandArgs.is, + execute: ({ id, value }: Preference.EditorCommandArgs) => { + this.clipboardService.writeText(id); + } + }); + commands.registerCommand(PreferencesCommands.COPY_JSON_VALUE, { + isEnabled: Preference.EditorCommandArgs.is, + isVisible: Preference.EditorCommandArgs.is, + execute: ({ id, value }: { id: string, value: string }) => { + const jsonString = `"${id}": ${JSON.stringify(value)}`; + this.clipboardService.writeText(jsonString); + } + }); + commands.registerCommand(PreferencesCommands.RESET_PREFERENCE, { + isEnabled: Preference.EditorCommandArgs.is, + isVisible: Preference.EditorCommandArgs.is, + execute: ({ id, value }: Preference.EditorCommandArgs) => { + this.preferenceValueRetrievalService.set(id, undefined, Number(this.preferencesScope.scope), this.preferencesScope.uri); + } + }); + } + + registerMenus(menus: MenuModelRegistry): void { + menus.registerMenuAction(CommonMenus.FILE_SETTINGS_SUBMENU_OPEN, { + commandId: CommonCommands.OPEN_PREFERENCES.id, + label: CommonCommands.OPEN_PREFERENCES.label, + order: 'a10', + }); + menus.registerMenuAction(PreferenceMenus.PREFERENCE_EDITOR_CONTEXT_MENU, { + commandId: PreferencesCommands.RESET_PREFERENCE.id, + label: PreferencesCommands.RESET_PREFERENCE.label, + order: 'a' + }); + menus.registerMenuAction(PreferenceMenus.PREFERENCE_EDITOR_COPY_ACTIONS, { + commandId: PreferencesCommands.COPY_JSON_VALUE.id, + label: PreferencesCommands.COPY_JSON_VALUE.label, + order: 'b' + }); + menus.registerMenuAction(PreferenceMenus.PREFERENCE_EDITOR_COPY_ACTIONS, { + commandId: PreferencesCommands.COPY_JSON_NAME.id, + label: PreferencesCommands.COPY_JSON_NAME.label, + order: 'c' + }); + } + + registerKeybindings(keybindings: KeybindingRegistry): void { + if (isOSX && !isFirefox) { + keybindings.registerKeybinding({ + command: CommonCommands.OPEN_PREFERENCES.id, + keybinding: 'cmd+,' + }); + } + + keybindings.registerKeybinding({ + command: CommonCommands.OPEN_PREFERENCES.id, + keybinding: 'ctrl+,', + }); + } + + registerToolbarItems(toolbar: TabBarToolbarRegistry): void { + toolbar.registerItem({ + id: PreferencesCommands.OPEN_PREFERENCES_JSON_TOOLBAR.id, + command: PreferencesCommands.OPEN_PREFERENCES_JSON_TOOLBAR.id, + tooltip: 'Open Preferences in JSON', + priority: 0, + }); + } + + protected async openPreferencesJSON(preferenceNode: Preference.NodeWithValueInAllScopes): Promise { + const wasOpenedFromEditor = preferenceNode.constructor.name !== 'PreferencesWidget'; + const { scope, activeScopeIsFolder, uri } = this.preferencesScope; + const preferenceId = wasOpenedFromEditor ? preferenceNode.id : ''; + // when opening from toolbar, widget is passed as arg by default (we don't need this info) + if (wasOpenedFromEditor) { + const currentPreferenceValue = preferenceNode.preference.values!; + const key = Preference.LookupKeys[Number(scope)] as keyof Preference.ValuesInAllScopes; + const valueInCurrentScope = currentPreferenceValue[key] === undefined ? currentPreferenceValue.defaultValue : currentPreferenceValue[key] as PreferenceItem; + this.preferenceValueRetrievalService.set(preferenceId, valueInCurrentScope, Number(scope), uri); + } + + let jsonEditorWidget: EditorWidget; + const jsonUriToOpen = await this.getPreferencesJSONUri(scope, activeScopeIsFolder, uri); + if (jsonUriToOpen) { + jsonEditorWidget = await this.editorManager.open(jsonUriToOpen); + + if (wasOpenedFromEditor) { + const text = jsonEditorWidget.editor.document.getText(); + if (preferenceId) { + const { index } = text.match(preferenceId)!; + const numReturns = text.slice(0, index).match(new RegExp('\n', 'g'))!.length; + jsonEditorWidget.editor.cursor = { line: numReturns, character: 4 + preferenceId.length + 4 }; + } + } + } + } + + private async getPreferencesJSONUri(scope: string, activeScopeIsFolder: string, uri: string): Promise { + const scopeNumber = Number(scope); + if (PreferenceScope.User === scopeNumber) { + return USER_PREFERENCE_URI; + } else if (PreferenceScope.Workspace === scopeNumber) { + if (activeScopeIsFolder === 'true') { + return this.getOrCreateSettingsFile(uri); + } else { + const wsURI = this.workspacePreferenceProvider.getConfigUri(); + if (wsURI) { + const wsURIString = wsURI.toString(); + if (!await this.filesystem.exists(wsURIString)) { + await this.filesystem.createFile(wsURIString); + } + return new URI(wsURIString); + } + } + + } else if (PreferenceScope.Folder === scopeNumber) { + return this.getOrCreateSettingsFile(uri); + } + return undefined; + } + + protected async getOrCreateSettingsFile(folderURI: string): Promise { + const folderSettingsURI = `${folderURI}/.theia/settings.json`; + if (folderSettingsURI && !await this.filesystem.exists(folderSettingsURI)) { + await this.filesystem.createFile(folderSettingsURI); + } + return new URI(folderSettingsURI); + } + + /** + * Determine if the current widget is the PreferencesWidget. + */ + protected withWidget(widget: Widget | undefined = this.tryGetWidget(), fn: (widget: PreferencesWidget) => T): T | false { + if (widget instanceof PreferencesWidget && widget.id === PreferencesWidget.ID) { + return fn(widget); + } + return false; + } +} diff --git a/packages/preferences/src/browser/preference-editor-widget.ts b/packages/preferences/src/browser/preference-editor-widget.ts deleted file mode 100644 index ddee4f07c4357..0000000000000 --- a/packages/preferences/src/browser/preference-editor-widget.ts +++ /dev/null @@ -1,145 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2019 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 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { Title } from '@phosphor/widgets'; -import { AttachedProperty } from '@phosphor/properties'; -import { DockPanel, Menu, TabBar, Widget } from '@phosphor/widgets'; -import { CommandRegistry } from '@phosphor/commands'; -import { VirtualElement, h } from '@phosphor/virtualdom'; -import { PreferenceScope } from '@theia/core/lib/browser'; -import { EditorWidget } from '@theia/editor/lib/browser'; -import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; -import URI from '@theia/core/lib/common/uri'; -import { FileAccess, FileSystem } from '@theia/filesystem/lib/common'; -import { FoldersPreferencesProvider } from './folders-preferences-provider'; - -export class PreferencesEditorWidgetTitle extends Title { - clickableText?: string; - clickableTextTooltip?: string; - clickableTextCallback?: (value: string) => void; -} - -export class PreferencesEditorWidget extends EditorWidget { - scope: PreferenceScope | undefined; - - get title(): PreferencesEditorWidgetTitle { - return new AttachedProperty({ - name: 'title', - create: owner => new PreferencesEditorWidgetTitle({ owner }), - }).get(this); - } -} - -// TODO: put into DI context -export class PreferenceEditorTabHeaderRenderer extends TabBar.Renderer { - - constructor( - private readonly workspaceService: WorkspaceService, - private readonly fileSystem: FileSystem, - private readonly foldersPreferenceProvider: FoldersPreferencesProvider - ) { - super(); - } - - renderTab(data: TabBar.IRenderData): VirtualElement { - const title = data.title; - title.closable = false; - const key = this.createTabKey(data); - const style = this.createTabStyle(data); - const className = this.createTabClass(data); - return h.li( - { key, className, title: title.caption, style }, - this.renderIcon(data), - this.renderLabel(data), - this.renderCloseIcon(data) - ); - } - - renderLabel(data: TabBar.IRenderData): VirtualElement { - const clickableTitle = data.title.owner.title; - if (clickableTitle.clickableText) { - return h.div( - h.span({ className: 'p-TabBar-tabLabel' }, data.title.label), - h.span({ - className: 'p-TabBar-tabLabel p-TabBar-tab-secondary-label', - title: clickableTitle.clickableTextTooltip, - onclick: event => { - const editorUri = data.title.owner.editor.uri; - this.refreshContextMenu(editorUri.parent.parent.toString(), clickableTitle.clickableTextCallback || (() => { })) - .then(menu => menu.open(event.x, event.y)); - } - }, clickableTitle.clickableText) - ); - } - return super.renderLabel(data); - } - - protected async refreshContextMenu(activeMenuId: string, menuItemAction: (value: string) => void): Promise { - const commands = new CommandRegistry(); - const menu = new Menu({ commands }); - const roots = this.workspaceService.tryGetRoots().map(r => r.uri); - for (const root of roots) { - if (await this.canAccessSettings(root)) { - const commandId = `switch_folder_pref_editor_to_${root}`; - if (!commands.hasCommand(commandId)) { - const rootUri = new URI(root); - const isActive = rootUri.toString() === activeMenuId; - commands.addCommand(commandId, { - label: rootUri.displayName, - iconClass: isActive ? 'fa fa-check' : '', - execute: () => { - if (!isActive) { - menuItemAction(root); - } - } - }); - } - - menu.addItem({ - type: 'command', - command: commandId - }); - } - } - return menu; - } - - private async canAccessSettings(folderUriStr: string): Promise { - const settingsUri = this.foldersPreferenceProvider.getConfigUri(folderUriStr); - if (settingsUri) { - return this.fileSystem.access(settingsUri.toString(), FileAccess.Constants.R_OK); - } - return this.fileSystem.access(folderUriStr, FileAccess.Constants.W_OK); - } -} - -// TODO put into DI context -export class PreferenceEditorContainerTabBarRenderer extends DockPanel.Renderer { - - constructor( - private readonly workspaceService: WorkspaceService, - private readonly fileSystem: FileSystem, - private readonly foldersPreferenceProvider: FoldersPreferencesProvider - ) { - super(); - } - - createTabBar(): TabBar { - const bar = new TabBar({ renderer: new PreferenceEditorTabHeaderRenderer(this.workspaceService, this.fileSystem, this.foldersPreferenceProvider) }); - bar.addClass('p-DockPanel-tabBar'); - return bar; - } -} diff --git a/packages/preferences/src/browser/preference-frontend-module.ts b/packages/preferences/src/browser/preference-frontend-module.ts index 395d8908c79af..93473d92a6e04 100644 --- a/packages/preferences/src/browser/preference-frontend-module.ts +++ b/packages/preferences/src/browser/preference-frontend-module.ts @@ -15,45 +15,32 @@ ********************************************************************************/ import '../../src/browser/style/index.css'; - +import './preferences-monaco-contribution'; import { ContainerModule, interfaces } from 'inversify'; -import { bindViewContribution, WidgetFactory, FrontendApplicationContribution } from '@theia/core/lib/browser'; -import { PreferencesContribution } from './preferences-contribution'; -import { createPreferencesTreeWidget } from './preference-tree-container'; -import { PreferencesMenuFactory } from './preferences-menu-factory'; -import { PreferencesFrontendApplicationContribution } from './preferences-frontend-application-contribution'; -import { PreferencesContainer, PreferencesTreeWidget, PreferencesEditorsContainer } from './preferences-tree-widget'; +import { FrontendApplicationContribution, bindViewContribution } from '@theia/core/lib/browser'; +import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { PreferenceTreeGenerator } from './util/preference-tree-generator'; import { bindPreferenceProviders } from './preference-bindings'; - -import './preferences-monaco-contribution'; - -export const PreferencesWidgetFactory = Symbol('PreferencesWidgetFactory'); +import { bindPreferencesWidgets } from './views/preference-widget-bindings'; +import { PreferencesEventService } from './util/preference-event-service'; +import { PreferencesTreeProvider } from './preference-tree-provider'; +import { PreferencesContribution } from './preference-contribution'; +import { PreferenceScopeCommandManager } from './util/preference-scope-command-manager'; +import { PreferencesFrontendApplicationContribution } from './preferences-frontend-application-contribution'; export function bindPreferences(bind: interfaces.Bind, unbind: interfaces.Unbind): void { bindPreferenceProviders(bind, unbind); + bindPreferencesWidgets(bind); - bindViewContribution(bind, PreferencesContribution); - - bind(PreferencesContainer).toSelf(); - bind(WidgetFactory).toDynamicValue(({ container }) => ({ - id: PreferencesContainer.ID, - createWidget: () => container.get(PreferencesContainer) - })); + bind(PreferencesEventService).toSelf().inSingletonScope(); + bind(PreferencesTreeProvider).toSelf().inSingletonScope(); + bind(PreferenceTreeGenerator).toSelf().inSingletonScope(); - bind(PreferencesWidgetFactory).toDynamicValue(({ container }) => ({ - id: PreferencesTreeWidget.ID, - createWidget: () => createPreferencesTreeWidget(container) - })).inSingletonScope(); - bind(WidgetFactory).toService(PreferencesWidgetFactory); - - bind(PreferencesEditorsContainer).toSelf(); - bind(WidgetFactory).toDynamicValue(({ container }) => ({ - id: PreferencesEditorsContainer.ID, - createWidget: () => container.get(PreferencesEditorsContainer) - })); + bindViewContribution(bind, PreferencesContribution); - bind(PreferencesMenuFactory).toSelf(); + bind(PreferenceScopeCommandManager).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).to(PreferencesFrontendApplicationContribution).inSingletonScope(); + bind(TabBarToolbarContribution).toService(PreferencesContribution); } export default new ContainerModule((bind, unbind, isBound, rebind) => { diff --git a/packages/preferences/src/browser/preference-tree-container.ts b/packages/preferences/src/browser/preference-tree-container.ts deleted file mode 100644 index cd0975ec194c0..0000000000000 --- a/packages/preferences/src/browser/preference-tree-container.ts +++ /dev/null @@ -1,45 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. 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 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { interfaces } from 'inversify'; -import { PreferencesDecorator } from './preferences-decorator'; -import { PreferencesDecoratorService } from './preferences-decorator-service'; -import { - createTreeContainer, - defaultTreeProps, - TreeDecoratorService, - TreeProps, - TreeWidget -} from '@theia/core/lib/browser'; -import { PreferencesTreeWidget } from './preferences-tree-widget'; - -export function createPreferencesTreeWidget(parent: interfaces.Container): PreferencesTreeWidget { - const child = createTreeContainer(parent); - - child.bind(PreferencesTreeWidget).toSelf(); - child.rebind(TreeProps).toConstantValue({ ...defaultTreeProps, search: true }); - child.rebind(TreeWidget).toService(PreferencesTreeWidget); - - bindPreferencesDecorator(child); - - return child.get(PreferencesTreeWidget); -} - -function bindPreferencesDecorator(parent: interfaces.Container): void { - parent.bind(PreferencesDecorator).toSelf().inSingletonScope(); - parent.bind(PreferencesDecoratorService).toSelf().inSingletonScope(); - parent.rebind(TreeDecoratorService).toService(PreferencesDecoratorService); -} diff --git a/packages/preferences/src/browser/preference-tree-provider.ts b/packages/preferences/src/browser/preference-tree-provider.ts new file mode 100644 index 0000000000000..6f90b61d7c288 --- /dev/null +++ b/packages/preferences/src/browser/preference-tree-provider.ts @@ -0,0 +1,140 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable, postConstruct } from 'inversify'; +import * as fuzzy from 'fuzzy'; +import { debounce } from 'lodash'; +import { TreeNode, CompositeTreeNode, PreferenceSchemaProvider, PreferenceDataSchema, PreferenceDataProperty, PreferenceScope } from '@theia/core/lib/browser'; +import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; +import { PreferencesEventService } from './util/preference-event-service'; +import { PreferenceTreeGenerator } from './util/preference-tree-generator'; +import { Preference } from './util/preference-types'; + +interface PreferenceFilterOptions { + minLength?: number; + baseSchemaAltered?: boolean; +}; + +const filterDefaults: Required = { + minLength: 1, + baseSchemaAltered: false, +}; + +@injectable() +export class PreferencesTreeProvider { + + protected lastSearchedLiteral: string = ''; + protected lastSearchedFuzzy: string = ''; + protected baseSchema: PreferenceDataSchema; + protected baseTree: CompositeTreeNode; + protected _currentTree: CompositeTreeNode; + protected currentScope: Preference.SelectedScopeDetails = Preference.DEFAULT_SCOPE; + protected handleUnderlyingDataChange = debounce( + (options: PreferenceFilterOptions, newScope?: Preference.SelectedScopeDetails) => this.updateUnderlyingData(options, newScope), + 200 + ); + protected handleSearchChange = debounce((term: string) => this.updateDisplay(term), 100); + + @inject(PreferencesEventService) protected readonly preferencesEventService: PreferencesEventService; + @inject(PreferenceSchemaProvider) protected readonly schemaProvider: PreferenceSchemaProvider; + @inject(PreferenceConfigurations) protected readonly preferenceConfigs: PreferenceConfigurations; + @inject(PreferenceTreeGenerator) protected readonly preferencesTreeGenerator: PreferenceTreeGenerator; + + @postConstruct() + protected init(): void { + this.updateUnderlyingData({ baseSchemaAltered: true }); + this.schemaProvider.onDidPreferenceSchemaChanged(() => this.handleUnderlyingDataChange({ baseSchemaAltered: true })); + this.preferencesEventService.onSearch.event(searchEvent => this.updateDisplay(searchEvent.query)); + this.preferencesEventService.onTabScopeSelected.event(scopeEvent => this.handleUnderlyingDataChange({}, scopeEvent)); + } + + protected updateUnderlyingData(options: PreferenceFilterOptions, newScope?: Preference.SelectedScopeDetails): void { + if (options.baseSchemaAltered) { + this.baseSchema = this.schemaProvider.getCombinedSchema(); + } + if (newScope) { + this.currentScope = newScope; + } + this.updateDisplay(this.lastSearchedLiteral, options); + } + + protected updateDisplay(term: string = this.lastSearchedLiteral, options: PreferenceFilterOptions = {}): void { + if (options.baseSchemaAltered) { + this.baseTree = this.preferencesTreeGenerator.generateTree(); + } + + this._currentTree = this.filter(term, Number(this.currentScope.scope), this.baseTree, options); + + this.preferencesEventService.onDisplayChanged.fire(); + } + + protected filter( + searchTerm: string, + currentScope: PreferenceScope, + tree: Tree, + filterOptions: PreferenceFilterOptions = {}, + ): Tree { + const { minLength } = { ...filterDefaults, ...filterOptions }; + + this.lastSearchedLiteral = searchTerm; + this.lastSearchedFuzzy = searchTerm.replace(/\s/g, ''); + const reset = searchTerm.length < minLength; + + return this.recurseAndSetVisible(currentScope, tree, reset); + } + + protected recurseAndSetVisible( + scope: PreferenceScope, + tree: Tree, + reset: boolean, + ): Tree { + let currentNodeShouldBeVisible = false; + + if (CompositeTreeNode.is(tree)) { + tree.children = tree.children.map(child => { + const newChild = this.recurseAndSetVisible(scope, child, reset); + currentNodeShouldBeVisible = currentNodeShouldBeVisible || !!newChild.visible; + return newChild; + }); + if (Preference.Branch.is(tree)) { + tree.leaves = (tree.leaves || []).map(child => { + const newChild = this.recurseAndSetVisible(scope, child, reset); + currentNodeShouldBeVisible = currentNodeShouldBeVisible || !!newChild.visible; + return newChild; + }); + } + } else { + currentNodeShouldBeVisible = this.schemaProvider.isValidInScope(tree.id, scope) + && ( + reset // search too short. + || fuzzy.test(this.lastSearchedFuzzy, tree.id || '') // search matches preference name. + // search matches description. Fuzzy isn't ideal here because the score dependens on the order of discovery. + || (this.baseSchema.properties[tree.id].description || '').includes(this.lastSearchedLiteral) + ); + } + + return { ...tree, visible: currentNodeShouldBeVisible }; + } + + get currentTree(): CompositeTreeNode { + return this._currentTree; + } + + get propertyList(): { [key: string]: PreferenceDataProperty; } { + return this.baseSchema.properties; + } + +} diff --git a/packages/preferences/src/browser/preferences-contribution.ts b/packages/preferences/src/browser/preferences-contribution.ts deleted file mode 100644 index 8ade1b12117ee..0000000000000 --- a/packages/preferences/src/browser/preferences-contribution.ts +++ /dev/null @@ -1,90 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 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 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { injectable, inject, named } from 'inversify'; -import { MenuModelRegistry, CommandRegistry } from '@theia/core'; -import { - CommonMenus, - PreferenceScope, - PreferenceProvider, - AbstractViewContribution, - CommonCommands, - KeybindingRegistry -} from '@theia/core/lib/browser'; -import { WorkspacePreferenceProvider } from './workspace-preference-provider'; -import { FileSystem } from '@theia/filesystem/lib/common'; -import { UserStorageService } from '@theia/userstorage/lib/browser'; -import { PreferencesContainer } from './preferences-tree-widget'; -import { EditorManager } from '@theia/editor/lib/browser'; -import { isFirefox } from '@theia/core/lib/browser'; -import { isOSX } from '@theia/core/lib/common/os'; - -@injectable() -export class PreferencesContribution extends AbstractViewContribution { - - @inject(UserStorageService) protected readonly userStorageService: UserStorageService; - @inject(PreferenceProvider) @named(PreferenceScope.Workspace) protected readonly workspacePreferenceProvider: WorkspacePreferenceProvider; - @inject(FileSystem) protected readonly filesystem: FileSystem; - @inject(EditorManager) protected readonly editorManager: EditorManager; - - constructor() { - super({ - widgetId: PreferencesContainer.ID, - widgetName: 'Preferences', - defaultWidgetOptions: { area: 'main' } - }); - } - - async registerCommands(commands: CommandRegistry): Promise { - commands.registerCommand(CommonCommands.OPEN_PREFERENCES, { - isEnabled: () => true, - execute: (preferenceScope = PreferenceScope.User) => this.openPreferences(preferenceScope) - }); - } - - registerMenus(menus: MenuModelRegistry): void { - menus.registerMenuAction(CommonMenus.FILE_SETTINGS_SUBMENU_OPEN, { - commandId: CommonCommands.OPEN_PREFERENCES.id, - order: 'a10' - }); - } - - registerKeybindings(keybindings: KeybindingRegistry): void { - if (isOSX && !isFirefox) { - keybindings.registerKeybinding({ - command: CommonCommands.OPEN_PREFERENCES.id, - keybinding: 'cmd+,' - }); - } - keybindings.registerKeybinding({ - command: CommonCommands.OPEN_PREFERENCES.id, - keybinding: 'ctrl+,' - }); - } - - protected async openPreferences(preferenceScope: PreferenceScope): Promise { - const wsUri = this.workspacePreferenceProvider.getConfigUri(); - if (wsUri && !await this.filesystem.exists(wsUri.toString())) { - await this.filesystem.createFile(wsUri.toString()); - } - - const widget = await this.widget; - widget.preferenceScope = preferenceScope; - super.openView({ activate: true }); - widget.activatePreferenceEditor(preferenceScope); - } - -} diff --git a/packages/preferences/src/browser/preferences-decorator.ts b/packages/preferences/src/browser/preferences-decorator.ts index 00ce44b3a4ef5..f9ef80efd9d7c 100644 --- a/packages/preferences/src/browser/preferences-decorator.ts +++ b/packages/preferences/src/browser/preferences-decorator.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { inject, injectable } from 'inversify'; +import { inject, injectable, postConstruct } from 'inversify'; import { Tree, TreeDecorator, TreeDecoration, PreferenceDataProperty, PreferenceService } from '@theia/core/lib/browser'; import { Emitter, Event, MaybePromise } from '@theia/core'; import { escapeInvisibleChars } from '@theia/core/lib/common/strings'; @@ -23,15 +23,15 @@ import { escapeInvisibleChars } from '@theia/core/lib/common/strings'; export class PreferencesDecorator implements TreeDecorator { readonly id: string = 'theia-preferences-decorator'; - private activeFolderUri: string | undefined; - + protected activeFolderUri: string | undefined; protected preferences: { [id: string]: PreferenceDataProperty }[]; - protected preferencesDecorations: Map; - + protected preferencesDecorations: Map = new Map(); protected readonly emitter: Emitter<(tree: Tree) => Map> = new Emitter(); - constructor(@inject(PreferenceService) private readonly preferencesService: PreferenceService) { - this.preferencesDecorations = new Map(); + @inject(PreferenceService) protected readonly preferencesService: PreferenceService; + + @postConstruct() + protected init(): void { this.preferencesService.onPreferenceChanged(() => { this.fireDidChangeDecorations(this.preferences); }); @@ -45,23 +45,25 @@ export class PreferencesDecorator implements TreeDecorator { if (!this.preferences) { this.preferences = preferences; } - this.preferencesDecorations = new Map(preferences.map(m => { - const preferenceName = Object.keys(m)[0]; - const preferenceValue = m[preferenceName]; - const storedValue = this.preferencesService.get(preferenceName, undefined, this.activeFolderUri); - const description = this.getDescription(preferenceValue); - return [preferenceName, { - tooltip: this.buildTooltip(preferenceValue), - captionSuffixes: [ - { - data: `: ${this.getPreferenceDisplayValue(storedValue, preferenceValue.defaultValue)}` - }, - { - data: ' ' + description, - fontData: { color: 'var(--theia-descriptionForeground)' } - }] - }] as [string, TreeDecoration.Data]; - })); + if (preferences) { + this.preferencesDecorations = new Map(preferences.map(m => { + const preferenceName = Object.keys(m)[0]; + const preferenceValue = m[preferenceName]; + const storedValue = this.preferencesService.get(preferenceName, undefined, this.activeFolderUri); + const description = this.getDescription(preferenceValue); + return [preferenceName, { + tooltip: this.buildTooltip(preferenceValue), + captionSuffixes: [ + { + data: `: ${this.getPreferenceDisplayValue(storedValue, preferenceValue.defaultValue)}` + }, + { + data: ' ' + description, + fontData: { color: 'var(--theia-descriptionForeground)' } + }] + }] as [string, TreeDecoration.Data]; + })); + } this.emitter.fire(() => this.preferencesDecorations); } @@ -69,13 +71,13 @@ export class PreferencesDecorator implements TreeDecorator { return this.preferencesDecorations; } - setActiveFolder(folder: string): void { + protected setActiveFolder(folder: string): void { this.activeFolderUri = folder; this.fireDidChangeDecorations(this.preferences); } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private getPreferenceDisplayValue(storedValue: any, defaultValue: any): any { + protected getPreferenceDisplayValue(storedValue: any, defaultValue: any): any { if (storedValue !== undefined) { if (typeof storedValue === 'string') { return escapeInvisibleChars(storedValue); @@ -85,7 +87,7 @@ export class PreferencesDecorator implements TreeDecorator { return defaultValue; } - private buildTooltip(data: PreferenceDataProperty): string { + protected buildTooltip(data: PreferenceDataProperty): string { let tooltips: string = ''; if (data.description) { tooltips = data.description; @@ -109,7 +111,7 @@ export class PreferencesDecorator implements TreeDecorator { * @param value {PreferenceDataProperty} the preference data property. * @returns the description if available. */ - private getDescription(value: PreferenceDataProperty): string { + protected getDescription(value: PreferenceDataProperty): string { /** * Format the string for consistency and display purposes. diff --git a/packages/preferences/src/browser/preferences-menu-factory.ts b/packages/preferences/src/browser/preferences-menu-factory.ts deleted file mode 100644 index db06d89545e4a..0000000000000 --- a/packages/preferences/src/browser/preferences-menu-factory.ts +++ /dev/null @@ -1,84 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. 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 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { injectable } from 'inversify'; -import { Menu } from '@phosphor/widgets'; -import { CommandRegistry } from '@phosphor/commands'; -import { escapeInvisibleChars, unescapeInvisibleChars } from '@theia/core/lib/common/strings'; -import { PreferenceDataProperty } from '@theia/core/lib/browser'; - -@injectable() -export class PreferencesMenuFactory { - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createPreferenceContextMenu(id: string, savedPreference: any, property: PreferenceDataProperty, execute: (property: string, value: any) => void): Menu { - const commands = new CommandRegistry(); - const menu = new Menu({ commands }); - if (property) { - const enumConst = property.enum; - if (enumConst) { - enumConst.map(escapeInvisibleChars) - .forEach(enumValue => { - const commandId = id + '-' + enumValue; - if (!commands.hasCommand(commandId)) { - commands.addCommand(commandId, { - label: enumValue, - iconClass: escapeInvisibleChars(savedPreference) === enumValue || !savedPreference && property.defaultValue === enumValue ? 'fa fa-check' : '', - execute: () => execute(id, unescapeInvisibleChars(enumValue)) - }); - menu.addItem({ - type: 'command', - command: commandId - }); - } - }); - } else if (property.type && property.type === 'boolean') { - const commandTrue = id + '-true'; - commands.addCommand(commandTrue, { - label: 'true', - iconClass: savedPreference === true || savedPreference === 'true' || savedPreference === undefined && property.defaultValue === true ? 'fa fa-check' : '', - execute: () => execute(id, true) - }); - menu.addItem({ - type: 'command', - command: commandTrue - }); - - const commandFalse = id + '-false'; - commands.addCommand(commandFalse, { - label: 'false', - iconClass: savedPreference === false || savedPreference === 'false' || savedPreference === undefined && property.defaultValue === false ? 'fa fa-check' : '', - execute: () => execute(id, false) - }); - menu.addItem({ - type: 'command', - command: commandFalse - }); - } else { - const commandId = id + '-stringValue'; - commands.addCommand(commandId, { - label: 'Add Value', - execute: () => execute(id, property.defaultValue ? property.defaultValue : '') - }); - menu.addItem({ - type: 'command', - command: commandId - }); - } - } - return menu; - } -} diff --git a/packages/preferences/src/browser/preferences-monaco-contribution.ts b/packages/preferences/src/browser/preferences-monaco-contribution.ts index 770768423ba71..c69415b221289 100644 --- a/packages/preferences/src/browser/preferences-monaco-contribution.ts +++ b/packages/preferences/src/browser/preferences-monaco-contribution.ts @@ -21,5 +21,8 @@ monaco.languages.register({ ], 'filenames': [ 'settings.json' + ], + 'extensions': [ + '.theia-workspace' ] }); diff --git a/packages/preferences/src/browser/preferences-tree-widget.ts b/packages/preferences/src/browser/preferences-tree-widget.ts deleted file mode 100644 index ba525717713d4..0000000000000 --- a/packages/preferences/src/browser/preferences-tree-widget.ts +++ /dev/null @@ -1,636 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. 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 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { inject, postConstruct, named, injectable } from 'inversify'; -import { Message } from '@phosphor/messaging'; -import { PreferencesMenuFactory } from './preferences-menu-factory'; -import { PreferencesDecorator } from './preferences-decorator'; -import { toArray } from '@phosphor/algorithm'; -import { BoxPanel, DockPanel, SplitPanel, Widget } from '@phosphor/widgets'; -import { - ApplicationShell, - ContextMenuRenderer, - ExpandableTreeNode, - PreferenceDataProperty, - PreferenceSchemaProvider, - PreferenceScope, - PreferenceService, - Saveable, - SelectableTreeNode, - TreeModel, - TreeNode, - TreeProps, - TreeWidget, - WidgetManager, - PreferenceProvider, - LabelProvider -} from '@theia/core/lib/browser'; -import { UserPreferenceProvider } from './user-preference-provider'; -import { WorkspacePreferenceProvider } from './workspace-preference-provider'; -import { PreferencesEditorWidget, PreferenceEditorContainerTabBarRenderer } from './preference-editor-widget'; -import { EditorWidget, EditorManager } from '@theia/editor/lib/browser'; -import { DisposableCollection, Emitter, Event, MessageService } from '@theia/core'; -import { Deferred } from '@theia/core/lib/common/promise-util'; -import { FileSystem, FileSystemUtils } from '@theia/filesystem/lib/common'; -import { UserStorageUri } from '@theia/userstorage/lib/browser'; -import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; -import URI from '@theia/core/lib/common/uri'; -import { FoldersPreferencesProvider } from './folders-preferences-provider'; -import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; -import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; - -@injectable() -export class PreferencesContainer extends SplitPanel implements ApplicationShell.TrackableWidgetProvider, Saveable { - - static ID = 'preferences_container_widget'; - - protected treeWidget: PreferencesTreeWidget | undefined; - protected editorsContainer: PreferencesEditorsContainer; - private currentEditor: PreferencesEditorWidget | undefined; - private readonly editors: PreferencesEditorWidget[] = []; - private deferredEditors = new Deferred(); - - protected readonly onDirtyChangedEmitter = new Emitter(); - readonly onDirtyChanged: Event = this.onDirtyChangedEmitter.event; - - protected readonly onDidChangeTrackableWidgetsEmitter = new Emitter(); - readonly onDidChangeTrackableWidgets = this.onDidChangeTrackableWidgetsEmitter.event; - - protected readonly toDispose = new DisposableCollection(); - - @inject(WidgetManager) - protected readonly widgetManager: WidgetManager; - - @inject(ApplicationShell) - protected readonly shell: ApplicationShell; - - @inject(MessageService) - protected readonly messageService: MessageService; - - @inject(PreferenceService) - protected readonly preferenceService: PreferenceService; - - @inject(WorkspaceService) - protected readonly workspaceService: WorkspaceService; - - protected _preferenceScope: PreferenceScope = PreferenceScope.User; - - @postConstruct() - protected init(): void { - this.id = PreferencesContainer.ID; - this.title.label = 'Preferences'; - this.title.caption = this.title.label; - this.title.closable = true; - this.title.iconClass = 'fa fa-sliders'; - - this.toDispose.pushAll([this.onDirtyChangedEmitter, this.onDidChangeTrackableWidgetsEmitter]); - } - - dispose(): void { - if (this.isDisposed) { - return; - } - super.dispose(); - this.toDispose.dispose(); - } - - get autoSave(): 'on' | 'off' { - return this.editors.some(editor => editor.saveable.autoSave === 'on') ? 'on' : 'off'; - } - - get dirty(): boolean { - return this.editors.some(editor => editor.saveable.dirty); - } - - save(): void { - this.editors.forEach(editor => editor.saveable.save()); - } - - getTrackableWidgets(): Promise { - return this.deferredEditors.promise; - } - - get preferenceScope(): PreferenceScope { - return this._preferenceScope; - } - - set preferenceScope(preferenceScope: PreferenceScope) { - this._preferenceScope = preferenceScope; - } - - protected async onAfterAttach(msg: Message): Promise { - if (this.widgets.length > 0) { - return; - } - - this.treeWidget = await this.widgetManager.getOrCreateWidget(PreferencesTreeWidget.ID); - this.treeWidget.onPreferenceSelected(value => { - const preferenceName = Object.keys(value)[0]; - const preferenceValue = value[preferenceName]; - if (this.dirty) { - this.messageService.warn('Preferences editor(s) has/have unsaved changes'); - } else if (this.currentEditor) { - this.preferenceService.set(preferenceName, preferenceValue, this.currentEditor.scope, this.currentEditor.editor.uri.toString()); - } - }); - - this.editorsContainer = await this.widgetManager.getOrCreateWidget(PreferencesEditorsContainer.ID); - this.toDispose.push(this.editorsContainer); - this.editorsContainer.activatePreferenceEditor(this.preferenceScope); - this.toDispose.push(this.editorsContainer.onInit(() => { - this.handleEditorsChanged(); - this.deferredEditors.resolve(this.editors); - })); - this.toDispose.push(this.editorsContainer.onEditorChanged(editor => { - if (this.currentEditor && this.currentEditor.editor.uri.toString() !== editor.editor.uri.toString()) { - this.currentEditor.saveable.save(); - } - if (editor) { - this.preferenceScope = editor.scope || PreferenceScope.User; - } else { - this.preferenceScope = PreferenceScope.User; - } - this.currentEditor = editor; - })); - this.toDispose.push(this.editorsContainer.onFolderPreferenceEditorUriChanged(uriStr => { - if (this.treeWidget) { - this.treeWidget.setActiveFolder(uriStr); - } - this.handleEditorsChanged(); - })); - this.toDispose.push(this.workspaceService.onWorkspaceLocationChanged(async workspaceFile => { - await this.editorsContainer.refreshWorkspacePreferenceEditor(); - await this.refreshFoldersPreferencesEditor(); - this.handleEditorsChanged(); - })); - this.toDispose.push(this.workspaceService.onWorkspaceChanged(async roots => { - await this.refreshFoldersPreferencesEditor(); - })); - - const treePanel = new BoxPanel(); - treePanel.addWidget(this.treeWidget); - this.addWidget(treePanel); - this.addWidget(this.editorsContainer); - this.treeWidget.activate(); - super.onAfterAttach(msg); - } - - protected onActivateRequest(msg: Message): void { - if (this.currentEditor) { - this.currentEditor.activate(); - } - super.onActivateRequest(msg); - } - - protected onCloseRequest(msg: Message): void { - if (this.treeWidget) { - this.treeWidget.close(); - } - this.editorsContainer.close(); - super.onCloseRequest(msg); - this.dispose(); - } - - public async activatePreferenceEditor(preferenceScope: PreferenceScope): Promise { - await this.deferredEditors.promise; - this.doActivatePreferenceEditor(preferenceScope); - } - - private doActivatePreferenceEditor(preferenceScope: PreferenceScope): void { - this.preferenceScope = preferenceScope; - if (this.editorsContainer) { - this.editorsContainer.activatePreferenceEditor(preferenceScope); - } - } - - protected handleEditorsChanged(): void { - const currentEditors = toArray(this.editorsContainer.widgets()); - currentEditors.forEach(editor => { - if (editor instanceof EditorWidget && this.editors.findIndex(e => e === editor) < 0) { - const editorWidget = editor as PreferencesEditorWidget; - this.editors.push(editorWidget); - const savable = editorWidget.saveable; - savable.onDirtyChanged(() => { - this.onDirtyChangedEmitter.fire(undefined); - }); - } - }); - for (let i = this.editors.length - 1; i >= 0; i--) { - if (currentEditors.findIndex(e => e === this.editors[i]) < 0) { - this.editors.splice(i, 1); - } - } - this.onDidChangeTrackableWidgetsEmitter.fire(this.editors); - this.doActivatePreferenceEditor(this.preferenceScope); - } - - private async refreshFoldersPreferencesEditor(): Promise { - const roots = this.workspaceService.tryGetRoots(); - if (roots.length === 0) { - this.editorsContainer.closeFoldersPreferenceEditorWidget(); - } else if (!roots.some(r => r.uri === this.editorsContainer.activeFolder)) { - const firstRoot = roots[0]; - await this.editorsContainer.refreshFoldersPreferencesEditorWidget(firstRoot ? firstRoot.uri : undefined); - } - } -} - -@injectable() -export class PreferencesEditorsContainer extends DockPanel { - - static ID = 'preferences_editors_container'; - - @inject(EditorManager) - protected readonly editorManager: EditorManager; - - @inject(LabelProvider) - protected readonly labelProvider: LabelProvider; - - @inject(PreferenceProvider) @named(PreferenceScope.User) - protected readonly userPreferenceProvider: UserPreferenceProvider; - - @inject(PreferenceProvider) @named(PreferenceScope.Workspace) - protected readonly workspacePreferenceProvider: WorkspacePreferenceProvider; - - @inject(EnvVariablesServer) - protected readonly envServer: EnvVariablesServer; - - private userPreferenceEditorWidget: PreferencesEditorWidget; - private workspacePreferenceEditorWidget: PreferencesEditorWidget | undefined; - private foldersPreferenceEditorWidget: PreferencesEditorWidget | undefined; - - private readonly onInitEmitter = new Emitter(); - readonly onInit: Event = this.onInitEmitter.event; - - private readonly onEditorChangedEmitter = new Emitter(); - readonly onEditorChanged: Event = this.onEditorChangedEmitter.event; - - private readonly onFolderPreferenceEditorUriChangedEmitter = new Emitter(); - readonly onFolderPreferenceEditorUriChanged: Event = this.onFolderPreferenceEditorUriChangedEmitter.event; - - protected readonly toDispose = new DisposableCollection( - this.onEditorChangedEmitter, - this.onInitEmitter - ); - - protected readonly toDisposeOnDetach = new DisposableCollection(); - - constructor( - @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, - @inject(FileSystem) protected readonly fileSystem: FileSystem, - @inject(PreferenceProvider) @named(PreferenceScope.Folder) - protected readonly foldersPreferenceProvider: FoldersPreferencesProvider - ) { - super({ renderer: new PreferenceEditorContainerTabBarRenderer(workspaceService, fileSystem, foldersPreferenceProvider) }); - } - - dispose(): void { - this.toDispose.dispose(); - super.dispose(); - } - - onCloseRequest(msg: Message): void { - toArray(this.widgets()).forEach(widget => widget.close()); - super.onCloseRequest(msg); - } - - onUpdateRequest(msg: Message): void { - const editor = this.selectedWidgets().next(); - if (editor) { - this.onEditorChangedEmitter.fire(editor); - } - super.onUpdateRequest(msg); - } - - onBeforeDetach(): void { - this.toDisposeOnDetach.dispose(); - } - - protected async onAfterAttach(msg: Message): Promise { - this.userPreferenceEditorWidget = await this.getUserPreferenceEditorWidget(); - this.addWidget(this.userPreferenceEditorWidget); - await this.refreshWorkspacePreferenceEditor(); - await this.refreshFoldersPreferencesEditorWidget(undefined); - - super.onAfterAttach(msg); - this.onInitEmitter.fire(undefined); - this.toDisposeOnDetach.push( - this.labelProvider.onDidChange(() => { - // Listen to changes made by the label provider and apply updates to the preference editors. - const icon = this.labelProvider.getIcon(new URI('settings.json')); - this.userPreferenceEditorWidget.title.iconClass = icon; - if (this.workspacePreferenceEditorWidget) { - // Explicitly update the workspace preference title to `Workspace` for single and multi-root workspaces. - this.workspacePreferenceEditorWidget.title.label = 'Workspace'; - this.workspacePreferenceEditorWidget.title.iconClass = icon; - } - }) - ); - } - - protected async getUserPreferenceEditorWidget(): Promise { - const userPreferenceUri = this.userPreferenceProvider.getConfigUri(); - const userPreferences = await this.editorManager.getOrCreateByUri(userPreferenceUri) as PreferencesEditorWidget; - userPreferences.title.label = 'User'; - userPreferences.title.iconClass = this.labelProvider.getIcon(new URI('settings.json')); - userPreferences.title.caption = `User Preferences: ${await this.getPreferenceEditorCaption(userPreferenceUri)}`; - userPreferences.scope = PreferenceScope.User; - return userPreferences; - } - - async refreshWorkspacePreferenceEditor(): Promise { - const newWorkspacePreferenceEditorWidget = await this.getWorkspacePreferenceEditorWidget(); - if (newWorkspacePreferenceEditorWidget) { - this.addWidget(newWorkspacePreferenceEditorWidget, - { ref: this.workspacePreferenceEditorWidget || this.userPreferenceEditorWidget }); - if (this.workspacePreferenceEditorWidget) { - this.workspacePreferenceEditorWidget.close(); - this.workspacePreferenceEditorWidget.dispose(); - } - this.workspacePreferenceEditorWidget = newWorkspacePreferenceEditorWidget; - } - } - - protected async getWorkspacePreferenceEditorWidget(): Promise { - const workspacePreferenceUri = this.workspacePreferenceProvider.getConfigUri(); - const workspacePreferences = workspacePreferenceUri && await this.editorManager.getOrCreateByUri(workspacePreferenceUri) as PreferencesEditorWidget; - - if (workspacePreferences) { - workspacePreferences.title.label = 'Workspace'; - workspacePreferences.title.caption = `Workspace Preferences: ${await this.getPreferenceEditorCaption(workspacePreferenceUri!)}`; - workspacePreferences.title.iconClass = this.labelProvider.getIcon(new URI('settings.json')); - workspacePreferences.editor.setLanguage('jsonc'); - workspacePreferences.scope = PreferenceScope.Workspace; - } - return workspacePreferences; - } - - get activeFolder(): string | undefined { - if (this.foldersPreferenceEditorWidget) { - return this.foldersPreferenceEditorWidget.editor.uri.parent.parent.toString(); - } - } - - async refreshFoldersPreferencesEditorWidget(currentFolderUri: string | undefined): Promise { - const folders = this.workspaceService.tryGetRoots().map(r => r.uri); - const newFolderUri = currentFolderUri || folders[0]; - const newFoldersPreferenceEditorWidget = await this.getFoldersPreferencesEditor(newFolderUri); - if (newFoldersPreferenceEditorWidget && // new widget is created - // the FolderPreferencesEditor is not available, OR the existing FolderPreferencesEditor is displaying the content of a different file - (!this.foldersPreferenceEditorWidget || this.foldersPreferenceEditorWidget.editor.uri.parent.parent.toString() !== newFolderUri)) { - this.addWidget(newFoldersPreferenceEditorWidget, - { ref: this.foldersPreferenceEditorWidget || this.workspacePreferenceEditorWidget || this.userPreferenceEditorWidget }); - this.closeFoldersPreferenceEditorWidget(); - this.foldersPreferenceEditorWidget = newFoldersPreferenceEditorWidget; - this.onFolderPreferenceEditorUriChangedEmitter.fire(newFoldersPreferenceEditorWidget.editor.uri.toString()); - } - } - - closeFoldersPreferenceEditorWidget(): void { - if (this.foldersPreferenceEditorWidget) { - this.foldersPreferenceEditorWidget.close(); - this.foldersPreferenceEditorWidget.dispose(); - this.foldersPreferenceEditorWidget = undefined; - } - } - - protected async getFoldersPreferencesEditor(folderUri: string | undefined): Promise { - if (this.workspaceService.saved) { - const settingsUri = await this.getFolderSettingsUri(folderUri); - const foldersPreferences = settingsUri && await this.editorManager.getOrCreateByUri(settingsUri) as PreferencesEditorWidget; - if (foldersPreferences) { - foldersPreferences.title.label = 'Folder'; - foldersPreferences.title.caption = `Folder Preferences: ${await this.getPreferenceEditorCaption(settingsUri!)}`; - foldersPreferences.title.clickableText = new URI(folderUri).displayName; - foldersPreferences.title.clickableTextTooltip = 'Click to manage preferences in another folder'; - foldersPreferences.title.clickableTextCallback = async (folderUriStr: string) => { - await foldersPreferences.saveable.save(); - await this.refreshFoldersPreferencesEditorWidget(folderUriStr); - this.activatePreferenceEditor(PreferenceScope.Folder); - }; - foldersPreferences.scope = PreferenceScope.Folder; - } - return foldersPreferences; - } - } - - private async getFolderSettingsUri(folderUri: string | undefined): Promise { - let configUri = this.foldersPreferenceProvider.getConfigUri(folderUri); - if (!configUri) { - configUri = this.foldersPreferenceProvider.getContainingConfigUri(folderUri); - if (configUri) { - await this.fileSystem.createFile(configUri.toString()); - } - } - return configUri; - } - - activatePreferenceEditor(preferenceScope: PreferenceScope): void { - for (const widget of toArray(this.widgets())) { - const preferenceEditor = widget as PreferencesEditorWidget; - if (preferenceEditor.scope === preferenceScope) { - this.activateWidget(widget); - break; - } - } - } - - private async getPreferenceEditorCaption(preferenceUri: URI): Promise { - const homeStat = await this.fileSystem.getCurrentUserHome(); - const homeUri = homeStat ? new URI(homeStat.uri) : undefined; - - let uri = preferenceUri; - if (preferenceUri.scheme === UserStorageUri.SCHEME && homeUri) { - const configDirUri = await this.envServer.getConfigDirUri(); - uri = new URI(configDirUri).resolve(preferenceUri.path); - } - return homeUri - ? FileSystemUtils.tildifyPath(uri.path.toString(), homeUri.path.toString()) - : uri.path.toString(); - } -} - -@injectable() -export class PreferencesTreeWidget extends TreeWidget { - - static ID = 'preferences_tree_widget'; - - private activeFolderUri: string | undefined; - private preferencesGroupNames = new Set(); - private properties: { [name: string]: PreferenceDataProperty }; - private readonly onPreferenceSelectedEmitter: Emitter<{ [key: string]: string }>; - readonly onPreferenceSelected: Event<{ [key: string]: string }>; - - @inject(PreferencesMenuFactory) protected readonly preferencesMenuFactory: PreferencesMenuFactory; - @inject(PreferenceService) protected readonly preferenceService: PreferenceService; - @inject(PreferencesDecorator) protected readonly decorator: PreferencesDecorator; - @inject(PreferenceConfigurations) protected readonly preferenceConfigs: PreferenceConfigurations; - - protected constructor( - @inject(TreeModel) readonly model: TreeModel, - @inject(TreeProps) protected readonly treeProps: TreeProps, - @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer, - @inject(PreferenceSchemaProvider) protected readonly preferenceSchemaProvider: PreferenceSchemaProvider - ) { - super(treeProps, model, contextMenuRenderer); - - this.onPreferenceSelectedEmitter = new Emitter<{ [key: string]: string }>(); - this.onPreferenceSelected = this.onPreferenceSelectedEmitter.event; - this.toDispose.push(this.onPreferenceSelectedEmitter); - - this.id = PreferencesTreeWidget.ID; - } - - dispose(): void { - super.dispose(); - } - - protected onAfterAttach(msg: Message): void { - this.initializeModel(); - this.toDisposeOnDetach.push(this.preferenceSchemaProvider.onDidPreferenceSchemaChanged(() => { - this.initializeModel(); - })); - super.onAfterAttach(msg); - } - - protected handleContextMenuEvent(node: TreeNode | undefined, event: React.MouseEvent): void { - super.handleContextMenuEvent(node, event); - if ((node).expanded === undefined) { - this.openContextMenu(node, event.nativeEvent.x, event.nativeEvent.y); - } - } - - protected handleClickEvent(node: TreeNode | undefined, event: React.MouseEvent): void { - super.handleClickEvent(node, event); - if ((node).expanded === undefined) { - this.openContextMenu(node, event.nativeEvent.x, event.nativeEvent.y); - } - } - - protected handleEnter(event: KeyboardEvent): void { - super.handleEnter(event); - const node: TreeNode = this.model.selectedNodes[0]; - if ((node).expanded === undefined) { - if (node) { - const nodeElement = document.getElementById(node.id); - if (nodeElement) { - const position = nodeElement.getBoundingClientRect(); - this.openContextMenu(this.model.selectedNodes[0], position.left, position.bottom); - } - } - } - } - - private openContextMenu(node: TreeNode | undefined, positionX: number, positionY: number): void { - if (node && SelectableTreeNode.is(node)) { - const contextMenu = this.preferencesMenuFactory.createPreferenceContextMenu( - node.id, - this.preferenceService.get(node.id, undefined, this.activeFolderUri), - this.properties[node.id], - (property, value) => { - this.onPreferenceSelectedEmitter.fire({ [property]: value }); - } - ); - contextMenu.aboutToClose.connect(() => { - this.activate(); - }); - contextMenu.activeItem = contextMenu.items[0]; - contextMenu.open(positionX, positionY); - } - } - - protected initializeModel(): void { - this.properties = this.preferenceSchemaProvider.getCombinedSchema().properties; - for (const property in this.properties) { - if (property) { - // Compute preference group name and accept those which have the proper format. - const group: string = property.substring(0, property.indexOf('.')); - if (property.split('.').length > 1) { - this.preferencesGroupNames.add(group); - } - } - } - - type GroupNode = SelectableTreeNode & ExpandableTreeNode; - const preferencesGroups: GroupNode[] = []; - const nodes: { [id: string]: PreferenceDataProperty }[] = []; - const groupNames: string[] = Array.from(this.preferencesGroupNames).sort((a, b) => this.sort(a, b)); - - const root: ExpandableTreeNode = { - id: 'root-node-id', - name: 'Apply the preference to selected preferences file', - parent: undefined, - visible: true, - children: preferencesGroups, - expanded: true, - }; - - for (const group of groupNames) { - const propertyNodes: SelectableTreeNode[] = []; - const properties: string[] = []; - - // Add a preference property if it is currently part of the group name. - // Properties which satisfy the condition `isSectionName` should not be added. - for (const property in this.properties) { - if (property.split('.', 1)[0] === group && - !this.preferenceConfigs.isSectionName(property)) { - properties.push(property); - } - } - - // Build the group name node (used to categorize common preferences together). - const preferencesGroup: GroupNode = { - id: group + '-id', - name: group.toLocaleUpperCase().substring(0, 1) + group.substring(1) + ' (' + properties.length + ')', - visible: true, - parent: root, - children: propertyNodes, - expanded: false, - selected: false - }; - - properties.sort((a, b) => this.sort(a, b)).forEach(property => { - const node: SelectableTreeNode = { - id: property, - name: property.substring(property.indexOf('.') + 1), - parent: preferencesGroup, - visible: true, - selected: false - }; - propertyNodes.push(node); - nodes.push({ [property]: this.properties[property] }); - }); - preferencesGroups.push(preferencesGroup); - } - this.decorator.fireDidChangeDecorations(nodes); - this.model.root = root; - } - - setActiveFolder(folder: string): void { - this.activeFolderUri = folder; - this.decorator.setActiveFolder(folder); - } - - /** - * Sort two string. - * - * @param a the first string. - * @param b the second string. - */ - protected sort(a: string, b: string): number { - return a.localeCompare(b, undefined, { ignorePunctuation: true }); - } -} diff --git a/packages/preferences/src/browser/style/index.css b/packages/preferences/src/browser/style/index.css index 7a1dae291168a..bfd78a0fc1e27 100644 --- a/packages/preferences/src/browser/style/index.css +++ b/packages/preferences/src/browser/style/index.css @@ -23,3 +23,310 @@ display: flex; line-height: var(--theia-content-line-height) !important; } + +/* UI View */ + +@import url("./preference-context-menu.css"); +@import url("./preference-array.css"); +@import url("./preference-object.css"); +@import url("./search-input.css"); + +.theia-settings-container { + max-width: 1000px; + padding-top: 11px; + display: grid; + grid-template-areas: + "header header" + "tabbar tabbar" + "navbar editor"; + grid-template-columns: minmax(150px, 280px) 1fr; + grid-template-rows: 45px 45px 1fr; +} + +.theia-settings-container .settings-no-results-announcement { + font-weight: bold; + font-size: var(--theia-ui-font-size3); + padding-left: var(--theia-ui-padding); + margin: calc(2*var(--theia-ui-padding)) 0px; +} + +.theia-settings-container .preferences-searchbar-widget { + grid-area: header; + margin: 3px 24px 0px 24px; +} + +.theia-settings-container .preferences-tabbar-widget { + grid-area: tabbar; + margin: 3px 24px 0px 24px; + box-shadow: 0px 6px 5px -5px var(--theia-widget-shadow); +} + +.theia-settings-container .preferences-tabbar-widget .preferences-scope-tab .p-TabBar-tabIcon:not(.preferences-folder-dropdown-icon) { + display: none; +} + +#theia-main-content-panel .theia-settings-container #preferences-scope-tab-bar .preferences-scope-tab { + background: var(--theia-editor-background); +} + +#theia-main-content-panel .theia-settings-container #preferences-scope-tab-bar .preferences-scope-tab.p-mod-current { + color: var(--theia-panelTitle-activeForeground); + border-top: var(--theia-border-width) solid var(--theia-panelTitle-activeBorder); +} + + +#theia-main-content-panel .theia-settings-container #preferences-scope-tab-bar .preferences-scope-tab.preferences-folder-tab .p-TabBar-tabLabel::after { + content: 'Folder'; + padding-left: 4px; + font-size: 0.8em; + color: var(--theia-tab-inactiveForeground); +} + +#theia-main-content-panel .theia-settings-container #preferences-scope-tab-bar .preferences-scope-tab.preferences-folder-dropdown { + position: relative; + padding-right: 23px; +} + +.preferences-folder-dropdown-icon { + background: var(--theia-icon-chevron-right) center center no-repeat; + transform: rotate(90deg); + width: 15px; + height: 15px; + position: absolute; + right: var(--theia-ui-padding); +} + +.theia-settings-container .preferences-editor-widget { + grid-area: editor; + padding: 0 24px; +} + +.theia-settings-container .preferences-editor-widget.full-pane { + grid-column-start: 1; + grid-column-end: 3; +} + +.theia-settings-container .preferences-tree-widget { + grid-area: navbar; + padding-left: 31px; +} + +.theia-settings-container .preferences-tree-widget .theia-mod-selected { + font-weight: bold; +} + +.theia-settings-container .preferences-tree-widget .theia-TreeNodeSegment { + text-overflow: ellipsis; + overflow: hidden; + max-width: 90%; +} + +.theia-settings-container .settings-main { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; +} + +.theia-settings-container .settings-main-scroll-container { + position: relative; + width: 100%; + flex: 1 1 auto; +} + +.theia-settings-container .settings-main-sticky-misc { + display: flex; + align-items: center; + justify-content: space-between; + flex: 0 1 50px; +} + +.theia-settings-container .settings-main-sticky-misc .json-button>i { + display: inline-block; + background: var(--theia-icon-open-json) no-repeat; + background-position-y: 1px; + -webkit-filter: invert(1); + filter: invert(1); + height: var(--theia-icon-size); + width: var(--theia-icon-size); +} + +.theia-settings-container .settings-scope>label { + margin-right: 12px; +} + +.theia-settings-container .settings-section { + padding-left: 0; + padding-top: var(--theia-ui-padding); + margin-top: calc(var(--theia-ui-padding) * -1); +} + +.theia-settings-container .settings-section a { + border: none; + color: var(--theia-foreground); + font-weight: 500; + outline: 0; + text-decoration: none; +} + +.theia-settings-container .settings-section a:hover { + text-decoration: underline; +} + + + +.theia-settings-container .settings-section-title { + font-weight: bold; + font-size: var(--theia-ui-font-size3); + padding-left: calc(2 * var(--theia-ui-padding)); +} + +.theia-settings-container .settings-section>li { + list-style-type: none; + margin: var(--theia-ui-padding) 0px; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: start; +} + +.theia-settings-container li.single-pref { + list-style-type: none; + margin: 12px 0 18px 0; + width: 100%; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding-left: calc(2 * var(--theia-ui-padding)); + position: relative; +} + +.theia-settings-container li.single-pref .pref-context-gutter { + position: absolute; + height: 100%; + left: -16px; + padding-right: 4px; + border-right: 2px hidden; +} + +.theia-settings-container li.single-pref .pref-context-gutter .settings-context-menu-btn { + opacity: 0; + transition: opacity .5s; +} + +.theia-settings-container li.single-pref .pref-context-gutter .settings-context-menu-btn.show-cog { + opacity: 1; +} + +.theia-settings-container li.single-pref .pref-context-gutter.theia-mod-item-modified { + border-right: 2px solid var(--theia-settings-modifiedItemIndicator); +} + +.theia-settings-container li.single-pref input[type="text"] { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +.theia-settings-container .settings-main { + margin: 0; +} + +.theia-settings-container .settings-main-sticky { + top: 0; + padding-top: calc(var(--theia-ui-padding)); + margin-top: calc(var(--theia-ui-padding) * -1); + background-color: var(--theia-editor-background); + -webkit-box-sizing: border-box; + box-sizing: border-box; + z-index: 1000; + display: flex; + flex-direction: column; + justify-content: space-around; +} + +.theia-settings-container .pref-name { + padding: 0; + font-weight: bold; +} + +.preferences-tree-spacer { + padding-left: calc(var(--theia-ui-padding)/2); + padding-right: calc(var(--theia-ui-padding)/2); + min-width: var(--theia-icon-size); + min-height: var(--theia-icon-size); +} + +.theia-settings-container .pref-description { + padding: var(--theia-ui-padding) 0; + color: var(--theia-descriptionforeground); + line-height: 18px; +} + +.theia-settings-container .theia-select:focus { + outline-width: 1px; + outline-style: solid; + outline-offset: -1px; + opacity: 1 !important; + outline-color: var(--theia-focusBorder); +} + +.theia-settings-container .theia-input[type="text"] { + border: 1px solid var(--theia-dropdown-border); +} + +.theia-settings-container .theia-input[type="checkbox"]:focus { + outline-width: 2px; +} + +.theia-settings-container .pref-content-container a.theia-json-input { + text-decoration: underline; +} + +.theia-settings-container .pref-content-container a.theia-json-input:hover { + text-decoration: none; + cursor: pointer; +} + +.theia-settings-container .pref-content-container { + width: 100%; +} + +.theia-settings-container .pref-content-container .pref-input { + padding: var(--theia-ui-padding) 0; + width: 100%; + max-width: 320px; +} + +.theia-settings-container .pref-content-container .pref-input>select, +.theia-settings-container .pref-content-container .pref-input>input { + width: 100%; +} + + +/* These specifications for the boolean class ensure that the + checkbox is rendered to the left of the description. +*/ +.theia-settings-container .pref-content-container.boolean { + display: grid; + grid-template-columns: 30px 1fr; +} + +.theia-settings-container .pref-content-container.boolean .pref-description { + grid-column-start: 2; + grid-row-start: 1; +} + +.theia-settings-container .pref-content-container.boolean .pref-input { + grid-column-start: 1; + grid-row-start: 1; + margin: 0; +} + +.theia-settings-container .settings-section>li:last-child { + margin-bottom: 20px; +} diff --git a/packages/preferences/src/browser/style/preference-array.css b/packages/preferences/src/browser/style/preference-array.css new file mode 100644 index 0000000000000..244cf8d7a1d76 --- /dev/null +++ b/packages/preferences/src/browser/style/preference-array.css @@ -0,0 +1,96 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +.theia-settings-container .preference-array { + list-style: none; + padding : 0; +} + +.theia-settings-container .preference-array-element { + display : -webkit-box; + display : -ms-flexbox; + display : flex; + -webkit-box-pack : justify; + -ms-flex-pack : justify; + justify-content : space-between; + -webkit-box-align: center; + -ms-flex-align : center; + align-items : center; + padding : calc(var(--thiea-ui-padding) / 2) var(--thiea-ui-padding); + border-bottom : var(--theia-panel-border) 2px solid; +} + +.theia-settings-container .pref-input li:nth-last-child(2) { + border-bottom: none; +} + +.theia-settings-container .pref-input li:last-child { + display : -webkit-box; + display : -ms-flexbox; + display : flex; + -webkit-box-align: center; + -ms-flex-align : center; + align-items : center; +} + +.theia-settings-container .preference-array-element:hover { + background-color: rgba(50%, 50%, 50%, 0.1); +} + +.theia-settings-container .preference-array-element-btn { + width : 1.5em; + height : 1.5em; + display : -webkit-box; + display : -ms-flexbox; + display : flex; + -webkit-box-pack : center; + -ms-flex-pack : center; + justify-content : center; + -webkit-box-align: center; + -ms-flex-align : center; + align-items : center; +} + +.theia-settings-container .preference-array-element .preference-array-element-btn { + opacity: 0; +} + +.theia-settings-container .preference-array-element:hover .preference-array-element-btn { + opacity: 1; +} + +.theia-settings-container .preference-array-element-btn:hover { + background-color: rgba(50%, 50%, 50%, 0.1); + cursor : pointer; +} + +.theia-settings-container .preference-array .add-btn { + margin-left : calc((var(--theia-icon-size) + 4px) * -1); + margin-right: 4px; + width : var(--theia-icon-size); + height : var(--theia-icon-size); +} + +.theia-settings-container .preference-array-clear-item { + background: var(--theia-icon-close) no-repeat; + width : var(--theia-icon-size); + height : var(--theia-icon-size); +} + +.theia-settings-container .preference-array-input { + padding-right: calc(var(--theia-icon-size) + var(--thiea-ui-padding)); + width : 100%; +} diff --git a/packages/preferences/src/browser/style/preference-context-menu.css b/packages/preferences/src/browser/style/preference-context-menu.css new file mode 100644 index 0000000000000..ab58ab6c0c3a1 --- /dev/null +++ b/packages/preferences/src/browser/style/preference-context-menu.css @@ -0,0 +1,74 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +.theia-settings-container .settings-context-menu-container { + position: relative; + padding-left: var(--theia-ui-padding); +} + +.theia-settings-container .settings-context-menu-btn { + cursor: pointer; +} + +.theia-settings-container .settings-context-menu { + position: absolute; + width: var(--theia-settingsSidebar-width); + list-style: none; + padding: var(--theia-ui-padding); + bottom: calc(100% + 10px); + left: -10px; + z-index: 9999; + background-color: var(--theia-menu-background); +} + +.theia-settings-container .settings-context-menu:before { + content: ""; + position: absolute; + left: 10px; + bottom: -10px; + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-top: 10px solid var(--theia-menu-background); +} + +.theia-settings-container .settings-context-menu li { + padding: var(--theia-ui-padding); +} + +.theia-settings-container .settings-context-menu li:hover { + background-color: var(--theia-menu-selectionBackground); +} + +.theia-settings-container .settings-context-menu i { + padding-right: var(--theia-ui-padding); + width: var(--theia-icon-size); + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; +} + +.theia-settings-container .pref-context-menu-btn { + margin-left: 5px; +} + +.theia-settings-container .pref-context-menu-btn:hover { + background-color: rgba(50%, 50%, 50%, 0.1); +} diff --git a/packages/preferences/src/browser/style/preference-object.css b/packages/preferences/src/browser/style/preference-object.css new file mode 100644 index 0000000000000..db5ac76c3a682 --- /dev/null +++ b/packages/preferences/src/browser/style/preference-object.css @@ -0,0 +1,49 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +.theia-settings-container .object-preference-input-container { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -ms-flex-wrap: wrap; + flex-wrap: wrap; +} + +.theia-settings-container .object-preference-input { + width: 100%; + max-height: 250px; + resize: none; + color: var(--theia-settings-textInputForeground); + background-color: var(--theia-settings-textInputBackground); + border-color: var(--theia-panel-border); + font-size: var(--theia-code-font-size); + margin-bottom: 10px; +} + +.theia-settings-container .object-preference-input-btn-toggle { + padding: 0 calc(var(--theia-ui-padding) / 2); +} + +.theia-settings-container .object-preference-input-btn-toggle-icon { + display: inline-block; + background: var(--theia-icon-open-json) no-repeat; + background-position-y: 1px; + height: var(--theia-icon-size); + width: var(--theia-icon-size); +} diff --git a/packages/preferences/src/browser/style/search-input.css b/packages/preferences/src/browser/style/search-input.css new file mode 100644 index 0000000000000..ab539f2ce29df --- /dev/null +++ b/packages/preferences/src/browser/style/search-input.css @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +.theia-settings-container .settings-search-container { + width: 100%; + position: relative; +} + +.theia-settings-container .settings-search-container .settings-search-input { + width: 100%; + text-indent: 8px; + padding: calc(var(--theia-ui-padding) / 2) 0; + box-sizing: border-box; + border: 1px solid var(--theia-dropdown-border); +} + +.theia-settings-container .settings-search-container .settings-search-icon { + position: absolute; + height: 100%; + left: 8px; + font-size: var(--theia-ui-font-size0); + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} diff --git a/packages/preferences/src/browser/util/preference-event-service.ts b/packages/preferences/src/browser/util/preference-event-service.ts new file mode 100644 index 0000000000000..24542fabaa17b --- /dev/null +++ b/packages/preferences/src/browser/util/preference-event-service.ts @@ -0,0 +1,27 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { injectable } from 'inversify'; +import { Emitter } from '@theia/core/lib/common/event'; +import { Preference } from './preference-types'; + +@injectable() +export class PreferencesEventService { + onTabScopeSelected = new Emitter(); + onSearch = new Emitter(); + onEditorScroll = new Emitter(); + onNavTreeSelection = new Emitter(); + onDisplayChanged = new Emitter(); +} diff --git a/packages/preferences/src/browser/util/preference-scope-command-manager.ts b/packages/preferences/src/browser/util/preference-scope-command-manager.ts new file mode 100644 index 0000000000000..98b654704453d --- /dev/null +++ b/packages/preferences/src/browser/util/preference-scope-command-manager.ts @@ -0,0 +1,70 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { PreferenceScope, LabelProvider } from '@theia/core/lib/browser'; +import { FileStat } from '@theia/filesystem/lib/common'; +import URI from '@theia/core/lib/common/uri'; +import { CommandRegistry, MenuModelRegistry, Command } from '@theia/core/lib/common'; +import { Preference } from './preference-types'; + +export const FOLDER_SCOPE_MENU_PATH = ['preferences:scope.menu']; + +@injectable() +export class PreferenceScopeCommandManager { + @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; + @inject(MenuModelRegistry) protected readonly menuModelRegistry: MenuModelRegistry; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + + protected foldersAsCommands: Command[] = []; + + createFolderWorkspacesMenu( + folderWorkspaces: FileStat[], + currentFolderURI: string, + ): void { + this.foldersAsCommands.forEach(folderCommand => { + this.menuModelRegistry.unregisterMenuAction(folderCommand, FOLDER_SCOPE_MENU_PATH); + this.commandRegistry.unregisterCommand(folderCommand); + }); + this.foldersAsCommands.length = 0; + + folderWorkspaces.forEach(folderWorkspace => { + const folderLabel = this.labelProvider.getName(new URI(folderWorkspace.uri)); + + const iconClass = currentFolderURI === folderWorkspace.uri ? 'fa fa-check' : ''; + const newFolderAsCommand = { + id: `preferenceScopeCommand:${folderWorkspace.uri}`, + label: folderLabel, + iconClass: iconClass + }; + + this.foldersAsCommands.push(newFolderAsCommand); + + this.commandRegistry.registerCommand(newFolderAsCommand, { + isVisible: (callback, check) => check === 'from-tabbar', + isEnabled: (callback, check) => check === 'from-tabbar', + execute: (callback: (scopeDetails: Preference.SelectedScopeDetails) => void) => { + callback({ scope: PreferenceScope.Folder.toString(), uri: folderWorkspace.uri, activeScopeIsFolder: 'true' }); + } + }); + + this.menuModelRegistry.registerMenuAction(FOLDER_SCOPE_MENU_PATH, { + commandId: newFolderAsCommand.id, + label: newFolderAsCommand.label + }); + }); + } +} diff --git a/packages/preferences/src/browser/util/preference-tree-generator.ts b/packages/preferences/src/browser/util/preference-tree-generator.ts new file mode 100644 index 0000000000000..282a2d20c927e --- /dev/null +++ b/packages/preferences/src/browser/util/preference-tree-generator.ts @@ -0,0 +1,110 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import { CompositeTreeNode, PreferenceSchemaProvider, SelectableTreeNode } from '@theia/core/lib/browser'; +import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; +import { Preference } from './preference-types'; + +@injectable() +export class PreferenceTreeGenerator { + + @inject(PreferenceSchemaProvider) schemaProvider: PreferenceSchemaProvider; + @inject(PreferenceConfigurations) preferenceConfigs: PreferenceConfigurations; + + generateTree(): CompositeTreeNode { + const preferencesSchema = this.schemaProvider.getCombinedSchema(); + const propertyNames = Object.keys(preferencesSchema.properties).sort((a, b) => a.localeCompare(b)); + const preferencesGroups: Preference.Branch[] = []; + const groups = new Map(); + const propertyPattern = Object.keys(preferencesSchema.patternProperties)[0]; // TODO: there may be a better way to get this data. + const overridePropertyIdentifier = new RegExp(propertyPattern, 'i'); + + const root = this.createRootNode(preferencesGroups); + + for (const propertyName of propertyNames) { + if (!this.preferenceConfigs.isSectionName(propertyName) && !overridePropertyIdentifier.test(propertyName)) { + const labels = propertyName.split('.'); + const group = labels[0]; + const subgroup = labels.length > 2 && labels.slice(0, 2).join('.'); + if (!groups.has(group)) { + const parentPreferencesGroup = this.createPreferencesGroup(group, root); + groups.set(group, parentPreferencesGroup); + preferencesGroups.push(parentPreferencesGroup); + } + if (subgroup && !groups.has(subgroup)) { + const remoteParent = groups.get(group) as Preference.Branch; + const newBranch = this.createPreferencesGroup(subgroup, remoteParent); + groups.set(subgroup, newBranch); + CompositeTreeNode.addChild(remoteParent, newBranch); + } + const parent = groups.get(subgroup || group) as Preference.Branch; + const leafNode = this.createLeafNode(propertyName, parent); + parent.leaves.push(leafNode); + } + } + + return root; + }; + + protected createRootNode = (preferencesGroups: Preference.Branch[]): CompositeTreeNode => ({ + id: 'root-node-id', + name: '', + parent: undefined, + visible: true, + children: preferencesGroups + }); + + protected createLeafNode = (property: string, preferencesGroup: Preference.Branch): SelectableTreeNode => { + const splitter = /[\W_]|(?<=[^A-Z])(?=[A-Z])/; // Any non-word character or the 0-length space between a non-upper-case character and an upper-case character + const propertySpecifier = property.split(splitter).slice(1); + const name = propertySpecifier.map(word => word.slice(0, 1).toLocaleUpperCase() + word.slice(1)).join(' ').trim(); + return { + id: property, + name, + parent: preferencesGroup, + visible: true, + selected: false, + }; + }; + + protected createPreferencesGroup = (group: string, root: CompositeTreeNode): Preference.Branch => { + const isSubgroup = 'expanded' in root; + const [groupname, subgroupname] = group.split('.'); + const label = isSubgroup ? subgroupname : groupname; + const newNode = { + id: `${group}-id`, + name: this.toTitleCase(label), + visible: true, + parent: root, + children: [], + leaves: [], + expanded: false, + selected: false, + }; + return newNode; + }; + + protected toTitleCase(nonTitle: string): string { + // Any non-word character or the 0-length space between a non-upper-case character and an upper-case character + const splitter = /[\W_]|(?<=[^A-Z])(?=[A-Z])/; + return nonTitle.split(splitter).map(word => this.capitalizeFirst(word)).join(' ').trim(); + } + + protected capitalizeFirst(maybeLowerCase: string): string { + return maybeLowerCase.slice(0, 1).toLocaleUpperCase() + maybeLowerCase.slice(1); + } +} diff --git a/packages/preferences/src/browser/util/preference-types.ts b/packages/preferences/src/browser/util/preference-types.ts new file mode 100644 index 0000000000000..489ea1bec94fa --- /dev/null +++ b/packages/preferences/src/browser/util/preference-types.ts @@ -0,0 +1,137 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { PreferenceDataProperty, SelectableTreeNode, PreferenceItem, Title, PreferenceScope, TreeNode, CompositeTreeNode, ExpandableTreeNode } from '@theia/core/lib/browser'; +import { Command, MenuPath } from '@theia/core'; + +export namespace Preference { + interface MaximalTree extends SelectableTreeNode, ExpandableTreeNode { + leaves: TreeExtension[]; + } + + export type TreeExtension = TreeNode & { + expanded?: MaximalTree['expanded']; + selected?: MaximalTree['selected']; + focus?: MaximalTree['focus']; + children?: MaximalTree['children']; + leaves?: MaximalTree['leaves']; + }; + + export interface Branch extends CompositeTreeNode { + leaves: TreeExtension[]; + } + + export namespace Branch { + export function is(node: TreeNode | Branch): node is Branch { + return 'leaves' in node && Array.isArray(node.leaves); + } + } + + export interface ValueInSingleScope { value?: PreferenceItem, data: PreferenceDataProperty; } + export interface NodeWithValueInSingleScope extends SelectableTreeNode { + preference: ValueInSingleScope; + } + + export interface ValuesInAllScopes { + preferenceName: string; + defaultValue: PreferenceItem | undefined; + globalValue: PreferenceItem | undefined; + workspaceValue: PreferenceItem | undefined; + workspaceFolderValue: PreferenceItem | undefined; + } + + export interface PreferenceWithValueInAllScopes { + values?: ValuesInAllScopes; + data: PreferenceDataProperty; + } + + export interface EditorCommandArgs { + id: string; + value: string | undefined; + } + + export namespace EditorCommandArgs { + export function is(prefObject: EditorCommandArgs): prefObject is EditorCommandArgs { + return !!prefObject && 'id' in prefObject && 'value' in prefObject; + } + } + + export interface NodeWithValueInAllScopes extends SelectableTreeNode { + preference: PreferenceWithValueInAllScopes; + } + + export enum LookupKeys { + defaultValue, + globalValue, + workspaceValue, + workspaceFolderValue, + } + + export interface SelectedScopeDetails extends Title.Dataset { + scope: string; + uri: string; + activeScopeIsFolder: string; + }; + + export const DEFAULT_SCOPE: SelectedScopeDetails = { + scope: PreferenceScope.User.toString(), + uri: '', + activeScopeIsFolder: 'false' + }; + + export interface SearchQuery { + query: string; + }; + + export interface MouseScrollDetails { + firstVisibleChildId: string; + }; + + export interface SelectedTreeNode { + nodeID: string; + } + + export interface ContextMenuCallbacks { + resetCallback(): void; + copyIDCallback(): void; + copyJSONCallback(): void; + } +} + +export namespace PreferencesCommands { + export const OPEN_PREFERENCES_JSON_TOOLBAR: Command = { + id: 'preferences:openJson.toolbar', + iconClass: 'codicon codicon-json' + }; + export const COPY_JSON_NAME: Command = { + id: 'preferences:copyJson.name', + label: 'Copy Setting ID' + }; + export const RESET_PREFERENCE: Command = { + id: 'preferences:reset', + label: 'Reset Setting' + }; + + export const COPY_JSON_VALUE: Command = { + id: 'preferences:copyJson.value', + label: 'Copy Setting as JSON', + }; +} + +export namespace PreferenceMenus { + export const PREFERENCE_EDITOR_CONTEXT_MENU: MenuPath = ['preferences:editor.contextMenu']; + export const PREFERENCE_EDITOR_COPY_ACTIONS: MenuPath = [...PREFERENCE_EDITOR_CONTEXT_MENU, 'preferences:editor.contextMenu.copy']; +} diff --git a/packages/preferences/src/browser/views/components/index.tsx b/packages/preferences/src/browser/views/components/index.tsx new file mode 100644 index 0000000000000..76494e5c09e76 --- /dev/null +++ b/packages/preferences/src/browser/views/components/index.tsx @@ -0,0 +1,22 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +export { PreferenceArrayInput } from './preference-array-input'; +export { PreferenceBooleanInput } from './preference-boolean-input'; +export { PreferenceJSONInput } from './preference-json-input'; +export { PreferenceNumberInput } from './preference-number-input'; +export { PreferenceSelectInput } from './preference-select-input'; +export { PreferenceStringInput } from './preference-string-input'; diff --git a/packages/preferences/src/browser/views/components/preference-array-input.tsx b/packages/preferences/src/browser/views/components/preference-array-input.tsx new file mode 100644 index 0000000000000..c842c32ed3419 --- /dev/null +++ b/packages/preferences/src/browser/views/components/preference-array-input.tsx @@ -0,0 +1,106 @@ + +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { Preference } from '../../util/preference-types'; + +interface PreferenceArrayInputProps { + preferenceDisplayNode: Preference.NodeWithValueInSingleScope; + setPreference(preferenceName: string, preferenceValue: string[]): void; +} + +export const PreferenceArrayInput: React.FC = ({ preferenceDisplayNode, setPreference }) => { + const values = preferenceDisplayNode.preference.value || [] as string[]; + const { id: preferenceID } = preferenceDisplayNode; + const [value, setValue] = React.useState(''); + + const doSubmit = React.useCallback((): void => { + if (value) { + setPreference(preferenceID, [...values, value]); + setValue(''); + } + }, [values, value]); + + const handleEnter = React.useCallback((e: React.KeyboardEvent): void => { + if (e.key === 'Enter') { + e.preventDefault(); + doSubmit(); + } + }, []); + + const handleChange = React.useCallback((e: React.ChangeEvent): void => { + setValue(e.target.value); + }, []); + + const handleRemove = React.useCallback((e: React.MouseEvent | React.KeyboardEvent): void => { + const target = e.currentTarget as HTMLSpanElement; + const key = (e as React.KeyboardEvent).key; + if (key && key !== 'Enter') { + return; + } + + const indexAttribute = target.getAttribute('data-index'); + const removalIndex = Number(indexAttribute); + if (indexAttribute) { + const newValues = [...values.slice(0, removalIndex), ...values.slice(removalIndex + 1)]; + setPreference(preferenceID, newValues); + } + }, []); + + return ( +
    + { + values.map((val: string, i: number): JSX.Element => ( +
  • + {val} + + + +
  • + )) + } +
  • + + + + +
  • +
+ ); +}; diff --git a/packages/preferences/src/browser/views/components/preference-boolean-input.tsx b/packages/preferences/src/browser/views/components/preference-boolean-input.tsx new file mode 100644 index 0000000000000..56d9d7e1fb9d0 --- /dev/null +++ b/packages/preferences/src/browser/views/components/preference-boolean-input.tsx @@ -0,0 +1,55 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { Preference } from '../../util/preference-types'; + +interface PreferenceBooleanInputProps { + preferenceDisplayNode: Preference.NodeWithValueInSingleScope; + setPreference: (preferenceName: string, preferenceValue: boolean) => void; +} + +export const PreferenceBooleanInput: React.FC = ({ preferenceDisplayNode, setPreference }) => { + const { id } = preferenceDisplayNode; + const value = preferenceDisplayNode.preference.value as boolean | undefined; + + // Tracks local state for quicker refreshes on user click. + const [checked, setChecked] = React.useState(!!value); + + // Allows user to reset value using cogwheel. + React.useEffect(() => { + setChecked(!!value); + }, [value]); + + const setValue = React.useCallback((e: React.ChangeEvent) => { + setChecked(!checked); + setPreference(id, e.target.checked); + }, [checked]); + + return ( + + ); +}; diff --git a/packages/preferences/src/browser/views/components/preference-json-input.tsx b/packages/preferences/src/browser/views/components/preference-json-input.tsx new file mode 100644 index 0000000000000..5ee0bf23382ce --- /dev/null +++ b/packages/preferences/src/browser/views/components/preference-json-input.tsx @@ -0,0 +1,34 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { Preference } from '../../util/preference-types'; + +interface PreferenceJSONInputProps { + preferenceDisplayNode: Preference.NodeWithValueInSingleScope; + onClick(): void; +} + +export const PreferenceJSONInput: React.FC = ({ preferenceDisplayNode, onClick }) => ( + + Edit in settings.json + +); diff --git a/packages/preferences/src/browser/views/components/preference-number-input.tsx b/packages/preferences/src/browser/views/components/preference-number-input.tsx new file mode 100644 index 0000000000000..e73e498799053 --- /dev/null +++ b/packages/preferences/src/browser/views/components/preference-number-input.tsx @@ -0,0 +1,57 @@ + +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { Preference } from '../../util/preference-types'; + +interface PreferenceNumberInputProps { + preferenceDisplayNode: Preference.NodeWithValueInSingleScope; + setPreference(preferenceName: string, preferenceValue: number): void; +} + +export const PreferenceNumberInput: React.FC = ({ preferenceDisplayNode, setPreference }) => { + const { id } = preferenceDisplayNode; + const { data, value } = preferenceDisplayNode.preference; + + const externalValue = (value !== undefined ? value : data.defaultValue) || ''; + + const [currentTimeout, setCurrentTimetout] = React.useState(0); + const [currentValue, setCurrentValue] = React.useState(externalValue); + + React.useEffect(() => { + setCurrentValue(externalValue); + }, [externalValue]); + + const onChange = React.useCallback(e => { + const { value: newValue } = e.target; + clearTimeout(currentTimeout); + const newTimeout = setTimeout(() => setPreference(id, Number(newValue)), 750); + setCurrentTimetout(Number(newTimeout)); + setCurrentValue(newValue); + }, [currentTimeout]); + + return ( + + ); +}; diff --git a/packages/preferences/src/browser/views/components/preference-select-input.tsx b/packages/preferences/src/browser/views/components/preference-select-input.tsx new file mode 100644 index 0000000000000..a1036881b3bd5 --- /dev/null +++ b/packages/preferences/src/browser/views/components/preference-select-input.tsx @@ -0,0 +1,57 @@ + +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { Preference } from '../../util/preference-types'; + +interface PreferenceSelectInputProps { + preferenceDisplayNode: Preference.NodeWithValueInSingleScope; + setPreference: (preferenceName: string, preferenceValue: string | number | string[]) => void; +} + +export const PreferenceSelectInput: React.FC = ({ preferenceDisplayNode, setPreference }) => { + const { id } = preferenceDisplayNode; + const { value, data } = preferenceDisplayNode.preference; + + const externalValue = (value !== undefined ? value : data.defaultValue) || ''; + + const [currentTimeout, setCurrentTimetout] = React.useState(0); + const [currentValue, setCurrentValue] = React.useState(externalValue); + + React.useEffect(() => { + setCurrentValue(externalValue); + }, [externalValue]); + + const onChange = React.useCallback(e => { + const { value: newValue } = e.target; + clearTimeout(currentTimeout); + const newTimeout = setTimeout(() => setPreference(id, newValue), 250); + setCurrentTimetout(Number(newTimeout)); + setCurrentValue(newValue); + }, [currentTimeout]); + + return ( + + ); +}; diff --git a/packages/preferences/src/browser/views/components/preference-string-input.tsx b/packages/preferences/src/browser/views/components/preference-string-input.tsx new file mode 100644 index 0000000000000..6c3affb5fdaab --- /dev/null +++ b/packages/preferences/src/browser/views/components/preference-string-input.tsx @@ -0,0 +1,56 @@ + +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { Preference } from '../../util/preference-types'; + +interface PreferenceStringInputProps { + preferenceDisplayNode: Preference.NodeWithValueInSingleScope; + setPreference(preferenceName: string, preferenceValue: string): void; +} + +export const PreferenceStringInput: React.FC = ({ preferenceDisplayNode, setPreference }) => { + const { id } = preferenceDisplayNode; + const { data, value } = preferenceDisplayNode.preference; + + const externalValue = (value !== undefined ? value : data.defaultValue) || ''; + + const [currentTimeout, setCurrentTimetout] = React.useState(0); + const [currentValue, setCurrentValue] = React.useState(externalValue); + + React.useEffect(() => { + setCurrentValue(externalValue); + }, [externalValue]); + + const onChange = React.useCallback(e => { + const { value: newValue } = e.target; + clearTimeout(currentTimeout); + const newTimeout = setTimeout(() => setPreference(id, newValue), 750); + setCurrentTimetout(Number(newTimeout)); + setCurrentValue(newValue); + }, [currentTimeout]); + + return ( + + ); +}; diff --git a/packages/preferences/src/browser/views/components/single-preference-display-factory.tsx b/packages/preferences/src/browser/views/components/single-preference-display-factory.tsx new file mode 100644 index 0000000000000..0a075415fa709 --- /dev/null +++ b/packages/preferences/src/browser/views/components/single-preference-display-factory.tsx @@ -0,0 +1,54 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { injectable, inject, postConstruct } from 'inversify'; +import { PreferenceService, ContextMenuRenderer } from '@theia/core/lib/browser'; +import { CommandService } from '@theia/core'; +import { Preference, PreferencesCommands } from '../../util/preference-types'; +import { PreferencesEventService } from '../../util/preference-event-service'; +import { PreferenceScopeCommandManager } from '../../util/preference-scope-command-manager'; +import { SinglePreferenceWrapper } from './single-preference-wrapper'; + +@injectable() +export class SinglePreferenceDisplayFactory { + protected currentScope: Preference.SelectedScopeDetails = Preference.DEFAULT_SCOPE; + @inject(PreferencesEventService) protected readonly preferencesEventService: PreferencesEventService; + @inject(PreferenceService) protected readonly preferenceValueRetrievalService: PreferenceService; + @inject(PreferenceScopeCommandManager) protected readonly preferencesMenuFactory: PreferenceScopeCommandManager; + @inject(CommandService) protected readonly commandService: CommandService; + @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; + @postConstruct() + init(): void { + this.preferencesEventService.onTabScopeSelected.event(e => this.currentScope = e); + } + + protected openJSON = (preferenceNode: Preference.NodeWithValueInAllScopes): void => { + this.commandService.executeCommand(PreferencesCommands.OPEN_PREFERENCES_JSON_TOOLBAR.id, preferenceNode); + }; + + render(preferenceNode: Preference.NodeWithValueInAllScopes): React.ReactElement { + return ; + } +} diff --git a/packages/preferences/src/browser/views/components/single-preference-wrapper.tsx b/packages/preferences/src/browser/views/components/single-preference-wrapper.tsx new file mode 100644 index 0000000000000..a0dd4d5d76996 --- /dev/null +++ b/packages/preferences/src/browser/views/components/single-preference-wrapper.tsx @@ -0,0 +1,203 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { Menu, PreferenceDataProperty, PreferenceScope, PreferenceItem, PreferenceService, ContextMenuRenderer } from '@theia/core/lib/browser'; +import { PreferenceSelectInput, PreferenceBooleanInput, PreferenceStringInput, PreferenceNumberInput, PreferenceJSONInput, PreferenceArrayInput } from '.'; +import { Preference, PreferenceMenus } from '../../util/preference-types'; + +interface SinglePreferenceWrapperProps { + contextMenuRenderer: ContextMenuRenderer; + preferenceDisplayNode: Preference.NodeWithValueInAllScopes; + currentScope: number; + currentScopeURI: string; + preferencesService: PreferenceService; + openJSON(preferenceNode: Preference.NodeWithValueInAllScopes): void; +} + +interface SinglePreferenceWrapperState { + showCog: boolean; + menuOpen: boolean; +} + +export class SinglePreferenceWrapper extends React.Component { + protected contextMenu: Menu; + protected value: PreferenceItem | undefined; + + state: SinglePreferenceWrapperState = { + showCog: false, + menuOpen: false + }; + + protected handleOnCogClick = (e: React.MouseEvent | React.KeyboardEvent): void => { + if (this.value !== undefined) { + const target = (e.target as HTMLElement); + const domRect = target.getBoundingClientRect(); + this.props.contextMenuRenderer.render({ + menuPath: PreferenceMenus.PREFERENCE_EDITOR_CONTEXT_MENU, + anchor: { x: domRect.left, y: domRect.bottom }, + args: [{ id: this.props.preferenceDisplayNode.id, value: this.value }], + onHide: this.setMenuHidden + }); + this.setMenuShown(); + } + }; + + protected setMenuShown = () => { + this.setState({ menuOpen: true }); + }; + + protected setMenuHidden = () => { + this.setState({ menuOpen: false }); + }; + + protected showCog = () => { + this.setState({ showCog: true }); + }; + + protected hideCog = () => { + this.setState({ showCog: false }); + }; + + render(): React.ReactNode { + const { preferenceDisplayNode } = this.props; + const { preference: { data, values } } = preferenceDisplayNode; + + this.value = this.getValueInCurrentScope(values, this.props.currentScope); + + const currentValueIsDefaultValue = this.value === data.defaultValue; + + const singlePreferenceValueDisplayNode = { ...preferenceDisplayNode, preference: { data, value: this.value } }; + const description = data.markdownDescription || data.description; + if (preferenceDisplayNode.visible) { + return (
  • +
    + {preferenceDisplayNode.name} + {this.renderOtherModifiedScopes(values, data, this.props.currentScope)} +
    +
    + +
    +
    + {description &&
    {description}
    } +
    {this.getInputType(singlePreferenceValueDisplayNode)}
    +
    +
  • ); + } else { + return <>; + } + } + + protected openJSONForCurrentPreference = () => { + this.props.openJSON(this.props.preferenceDisplayNode); + }; + + protected getValueInCurrentScope(preferenceValuesInAllScopes: Preference.ValuesInAllScopes | undefined, currentScope: number): PreferenceItem | undefined { + if (preferenceValuesInAllScopes) { + const key = Preference.LookupKeys[currentScope] as keyof Preference.ValuesInAllScopes; + return preferenceValuesInAllScopes[key] === undefined ? preferenceValuesInAllScopes.defaultValue : preferenceValuesInAllScopes[key] as PreferenceItem; + } + return undefined; + } + + protected renderOtherModifiedScopes( + preferenceValuesInAllScopes: Preference.ValuesInAllScopes | undefined, + data: PreferenceDataProperty, + currentScope: number): React.ReactNode[] | undefined { + if (preferenceValuesInAllScopes) { + return ['User', 'Workspace'].map((scope: 'User' | 'Workspace') => { + const matchingScope = PreferenceScope[scope]; + const valueInCurrentScope = preferenceValuesInAllScopes[Preference.LookupKeys[currentScope] as keyof Preference.ValuesInAllScopes]; + if (currentScope !== matchingScope) { + const valueInOtherScope = preferenceValuesInAllScopes[Preference.LookupKeys[matchingScope] as keyof Preference.ValuesInAllScopes]; + if (valueInOtherScope && valueInOtherScope !== data.defaultValue) { + const message = valueInCurrentScope && valueInCurrentScope !== data.defaultValue ? `Also modified in: ${scope}.` : `Modified in: ${scope}.`; + return {message}; + } + } + }); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected setPreference = (preferenceName: string, preferenceValue: any): void => { + this.props.preferencesService.set(preferenceName, preferenceValue, this.props.currentScope, this.props.currentScopeURI); + }; + + getInputType = (preferenceDisplayNode: Preference.NodeWithValueInSingleScope): React.ReactNode => { + const { type, items } = preferenceDisplayNode.preference.data; + if (preferenceDisplayNode.preference.data.enum) { + return ; + } if (type === 'boolean') { + return ; + } if (type === 'string') { + return ; + } if (type === 'number' || type === 'integer') { + return ; + } if (type === 'array') { + if (items && items.type === 'object') { + return ; + } + return ; + } if (type === 'object') { + return ; + } + return ; + }; +} diff --git a/packages/preferences/src/browser/views/preference-editor-widget.tsx b/packages/preferences/src/browser/views/preference-editor-widget.tsx new file mode 100644 index 0000000000000..831828cde95c3 --- /dev/null +++ b/packages/preferences/src/browser/views/preference-editor-widget.tsx @@ -0,0 +1,193 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { postConstruct, injectable, inject } from 'inversify'; +import * as React from 'react'; +import { Disposable } from 'vscode-jsonrpc'; +import { + ReactWidget, + PreferenceService, + PreferenceDataProperty, + PreferenceScope, + CompositeTreeNode, + SelectableTreeNode, + PreferenceItem, +} from '@theia/core/lib/browser'; +import { Message, } from '@theia/core/lib/browser/widgets/widget'; +import { SinglePreferenceDisplayFactory } from './components/single-preference-display-factory'; +import { Preference } from '../util/preference-types'; +import { PreferencesEventService } from '../util/preference-event-service'; +import { PreferencesTreeProvider } from '../preference-tree-provider'; + +@injectable() +export class PreferencesEditorWidget extends ReactWidget { + static readonly ID = 'settings.editor'; + static readonly LABEL = 'Settings Editor'; + + protected properties: { [key: string]: PreferenceDataProperty; }; + protected currentDisplay: CompositeTreeNode; + protected activeScope: number = PreferenceScope.User; + protected activeURI: string = ''; + protected activeScopeIsFolder: boolean = false; + protected scrollContainerRef: React.RefObject = React.createRef(); + protected hasRendered = false; + protected _preferenceScope: Preference.SelectedScopeDetails = Preference.DEFAULT_SCOPE; + + @inject(PreferencesEventService) protected readonly preferencesEventService: PreferencesEventService; + @inject(PreferenceService) protected readonly preferenceValueRetrievalService: PreferenceService; + @inject(PreferencesTreeProvider) protected readonly preferenceTreeProvider: PreferencesTreeProvider; + @inject(SinglePreferenceDisplayFactory) protected readonly singlePreferenceFactory: SinglePreferenceDisplayFactory; + + @postConstruct() + protected init(): void { + this.onRender.push(Disposable.create(() => this.hasRendered = true)); + this.id = PreferencesEditorWidget.ID; + this.title.label = PreferencesEditorWidget.LABEL; + this.preferenceValueRetrievalService.onPreferenceChanged((preferenceChange): void => { + this.update(); + }); + this.preferencesEventService.onDisplayChanged.event(() => this.handleChangeDisplay()); + this.preferencesEventService.onNavTreeSelection.event(e => this.scrollToEditorElement(e.nodeID)); + this.currentDisplay = this.preferenceTreeProvider.currentTree; + this.properties = this.preferenceTreeProvider.propertyList; + this.update(); + } + + set preferenceScope(preferenceScopeDetails: Preference.SelectedScopeDetails) { + this._preferenceScope = preferenceScopeDetails; + this.handleChangeScope(this._preferenceScope); + } + + protected callAfterFirstRender(callback: Function): void { + if (this.hasRendered) { + callback(); + } else { + this.onRender.push(Disposable.create(() => callback())); + } + } + + protected onAfterAttach(msg: Message): void { + this.callAfterFirstRender(() => { + super.onAfterAttach(msg); + this.node.addEventListener('scroll', this.onScroll); + }); + } + + protected render(): React.ReactNode { + const visibleCategories = this.currentDisplay.children.filter(category => category.visible); + return ( +
    +
    + {!!visibleCategories.length ? visibleCategories.map(category => this.renderCategory(category as Preference.Branch)) : this.renderNoResultMessage()} +
    +
    + ); + } + + protected handleChangeDisplay = (): void => { + // This is here to avoid using the synthetic event asynchronously + this.currentDisplay = this.preferenceTreeProvider.currentTree; + this.properties = this.preferenceTreeProvider.propertyList; + this.node.scrollTop = 0; + this.update(); + }; + + protected onScroll = (): void => { + const scrollContainer = this.node; + const visibleChildren: string[] = []; + this.addFirstVisibleChildId(scrollContainer, visibleChildren); + if (visibleChildren.length) { + this.preferencesEventService.onEditorScroll.fire({ firstVisibleChildId: visibleChildren[0] }); + } + }; + + protected addFirstVisibleChildId(container: Element, array: string[]): void { + const children = container.children; + for (let i = 0; i < children.length && !array.length; i++) { + const id = children[i].getAttribute('data-id'); + if (id && this.isInView(children[i] as HTMLElement, container as HTMLElement)) { + array.push(id); + } else if (!array.length) { + this.addFirstVisibleChildId(children[i], array); + } + } + } + + protected isInView(e: HTMLElement, parent: HTMLElement): boolean { + const scrollTop = this.node.scrollTop; + const scrollCheckHeight = 0.7; + return this.compare(e.offsetTop).isBetween(scrollTop, scrollTop + parent.offsetHeight) || + this.compare(scrollTop).isBetween(e.offsetTop, e.offsetTop + (e.offsetHeight * scrollCheckHeight)); + } + + protected compare = (value: number): { isBetween: (a: number, b: number) => boolean; } => ({ + isBetween: (a: number, b: number): boolean => ( + (value >= a && value <= b) || (value >= b && value <= a) + ) + }); + + protected handleChangeScope = ({ scope, uri, activeScopeIsFolder }: Preference.SelectedScopeDetails): void => { + this.activeScope = Number(scope); + this.activeURI = uri; + this.activeScopeIsFolder = activeScopeIsFolder === 'true'; + this.update(); + }; + + protected renderCategory(category: Preference.Branch): React.ReactNode { + const children = category.children.concat(category.leaves).sort((a, b) => this.sort(a.id, b.id)); + return category.visible && ( +
      +
    • {category.name}
    • + {children.map((preferenceNode: SelectableTreeNode | Preference.Branch) => { + if (Preference.Branch.is(preferenceNode)) { + return this.renderCategory(preferenceNode); + } + const values = this.preferenceValueRetrievalService.inspect(preferenceNode.id, this.activeURI); + const preferenceNodeWithValueInAllScopes = { ...preferenceNode, preference: { data: this.properties[preferenceNode.id], values } }; + return this.singlePreferenceFactory.render(preferenceNodeWithValueInAllScopes); + })} +
    + ); + } + + protected renderNoResultMessage(): React.ReactNode { + return
    That search query has returned no results.
    ; + } + + protected scrollToEditorElement(nodeID: string): void { + if (nodeID) { + const el = document.getElementById(`${nodeID}-editor`); + if (el) { + el.scrollIntoView(); + } + } + } + + /** + * Sort two strings. + * + * @param a the first string. + * @param b the second string. + */ + protected sort(a: string, b: string): number { + return a.localeCompare(b, undefined, { ignorePunctuation: true }); + } +} diff --git a/packages/preferences/src/browser/views/preference-scope-tabbar-widget.tsx b/packages/preferences/src/browser/views/preference-scope-tabbar-widget.tsx new file mode 100644 index 0000000000000..7f2dd6d362e91 --- /dev/null +++ b/packages/preferences/src/browser/views/preference-scope-tabbar-widget.tsx @@ -0,0 +1,232 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable, postConstruct } from 'inversify'; +import { TabBar, Widget, Title } from '@phosphor/widgets'; +import { PreferenceScope, Message, ContextMenuRenderer, LabelProvider } 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'; +import { PreferencesEventService } from '../util/preference-event-service'; +import { PreferenceScopeCommandManager, FOLDER_SCOPE_MENU_PATH } from '../util/preference-scope-command-manager'; +import { Preference } from '../util/preference-types'; + +const USER_TAB_LABEL = 'User'; +const USER_TAB_INDEX = PreferenceScope[USER_TAB_LABEL].toString(); +const WORKSPACE_TAB_LABEL = 'Workspace'; +const WORKSPACE_TAB_INDEX = PreferenceScope[WORKSPACE_TAB_LABEL].toString(); +const FOLDER_TAB_LABEL = 'Folder'; +const FOLDER_TAB_INDEX = PreferenceScope[FOLDER_TAB_LABEL].toString(); + +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'; +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}`; + +@injectable() +export class PreferencesScopeTabBar extends TabBar { + + static ID = 'preferences-scope-tab-bar'; + @inject(PreferencesEventService) protected readonly preferencesEventService: PreferencesEventService; + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(PreferenceScopeCommandManager) protected readonly preferencesMenuFactory: PreferenceScopeCommandManager; + @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + + protected folderTitle: Title; + protected currentWorkspaceRoots: FileStat[] = []; + protected currentSelection: Preference.SelectedScopeDetails = Preference.DEFAULT_SCOPE; + protected setNewScopeSelection(newSelection: Preference.SelectedScopeDetails): void { + + const newIndex = this.titles.findIndex(title => title.dataset.scope === newSelection.scope); + if (newIndex !== -1) { + this.currentSelection = newSelection; + this.currentIndex = newIndex; + if (newSelection.scope === PreferenceScope.Folder.toString()) { + this.addOrUpdateFolderTab(); + } + this.emitNewScope(); + } + } + + @postConstruct() + protected init(): void { + this.id = PreferencesScopeTabBar.ID; + this.setupInitialDisplay(); + this.tabActivateRequested.connect((sender, args) => { + if (!!args.title) { + this.setNewScopeSelection(args.title.dataset as unknown as Preference.SelectedScopeDetails); + } + }); + this.workspaceService.onWorkspaceChanged(newRoots => { + this.doUpdateDisplay(newRoots); + }); + this.workspaceService.onWorkspaceLocationChanged(() => this.updateWorkspaceTab()); + } + + protected setupInitialDisplay(): void { + this.addUserTab(); + this.addWorkspaceTab(); + this.addOrUpdateFolderTab(); + } + + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + this.addTabIndexToTabs(); + } + + protected addTabIndexToTabs(): void { + this.node.querySelectorAll('li').forEach((tab, index) => { + tab.tabIndex = 0; + tab.onkeypress = () => { + if (tab.className.includes(GENERAL_FOLDER_TAB_CLASSNAME) && this.currentWorkspaceRoots.length > 1) { + const tabRect = tab.getBoundingClientRect(); + this.openContextMenu(tabRect); + } else { + this.setNewScopeSelection(this.titles[index].dataset as unknown as Preference.SelectedScopeDetails); + } + }; + }); + } + + protected addUserTab(): void { + this.addTab(new Title({ + dataset: { uri: '', scope: USER_TAB_INDEX }, + label: USER_TAB_LABEL, + owner: this, + className: PREFERENCE_TAB_CLASSNAME + })); + } + + protected addWorkspaceTab(): void { + if (!!this.workspaceService.workspace) { + this.addTab(new Title({ + dataset: this.getWorkspaceDataset(), + label: WORKSPACE_TAB_LABEL, + owner: this, + className: PREFERENCE_TAB_CLASSNAME, + })); + } + } + + protected getWorkspaceDataset(): Preference.SelectedScopeDetails { + const { uri, isDirectory } = this.workspaceService.workspace!; + const scope = WORKSPACE_TAB_INDEX; + const activeScopeIsFolder = isDirectory.toString(); + return { uri, activeScopeIsFolder, 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); + this.getFolderContextMenu(this.currentWorkspaceRoots); + 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.currentSelection, folderTitle: 'true' }; + this.folderTitle.className = multipleFolderRootsAreAvailable ? SELECTED_FOLDER_DROPDOWN_CLASSNAME : SINGLE_FOLDER_TAB_CLASSNAME; + } else { + const singleFolderRoot = this.currentWorkspaceRoots[0].uri; + const singleFolderLabel = this.labelProvider.getName(new URI(singleFolderRoot)); + const defaultURI = multipleFolderRootsAreAvailable ? '' : singleFolderRoot; + 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, 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); + } + + handleEvent(e: Event): void { + const folderTab = this.contentNode.querySelector(`.${GENERAL_FOLDER_TAB_CLASSNAME}`); + if (folderTab && folderTab.contains(e.target as HTMLElement) && this.currentWorkspaceRoots.length > 1) { + const tabRect = folderTab.getBoundingClientRect(); + this.openContextMenu(tabRect); + return; + } + super.handleEvent(e); + } + + protected openContextMenu(tabRect: DOMRect | ClientRect): void { + this.contextMenuRenderer.render({ + menuPath: FOLDER_SCOPE_MENU_PATH, + anchor: { x: tabRect.left, y: tabRect.bottom }, + args: [this.folderSelectionCallback, 'from-tabbar'] + }); + } + + protected doUpdateDisplay(newRoots: FileStat[]): void { + const folderWasRemoved = newRoots.length < this.currentWorkspaceRoots.length; + this.currentWorkspaceRoots = newRoots; + if (folderWasRemoved) { + const removedFolderWasSelectedScope = !this.currentWorkspaceRoots.some(root => root.uri === this.currentSelection.uri); + if (removedFolderWasSelectedScope) { + this.setNewScopeSelection(Preference.DEFAULT_SCOPE); + } + } + this.updateWorkspaceTab(); + this.addOrUpdateFolderTab(); + } + + protected updateWorkspaceTab(): void { + // Will always be present - otherwise workspace cannot change. + const workspaceTitle = this.titles.find(title => title.label === WORKSPACE_TAB_LABEL)!; + workspaceTitle.dataset = this.getWorkspaceDataset(); + if (this.currentSelection.scope === PreferenceScope.Workspace.toString()) { + this.setNewScopeSelection(workspaceTitle.dataset as Preference.SelectedScopeDetails); + } + } + + protected emitNewScope(): void { + this.preferencesEventService.onTabScopeSelected.fire(this.currentSelection); + } +} diff --git a/packages/preferences/src/browser/views/preference-searchbar-widget.tsx b/packages/preferences/src/browser/views/preference-searchbar-widget.tsx new file mode 100644 index 0000000000000..492d8b50b15d0 --- /dev/null +++ b/packages/preferences/src/browser/views/preference-searchbar-widget.tsx @@ -0,0 +1,71 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject, postConstruct } from 'inversify'; +import { ReactWidget } from '@theia/core/lib/browser'; +import * as React from 'react'; +import { debounce } from 'lodash'; +import { Disposable } from '@theia/languages/lib/browser'; +import { PreferencesEventService } from '../util/preference-event-service'; + +@injectable() +export class PreferencesSearchbarWidget extends ReactWidget { + static readonly ID = 'settings.header'; + static readonly LABEL = 'Settings Header'; + + protected searchbarRef: React.RefObject = React.createRef(); + + @inject(PreferencesEventService) protected readonly preferencesEventService: PreferencesEventService; + + @postConstruct() + protected init(): void { + this.onRender.push(Disposable.create(() => this.focus())); + this.id = PreferencesSearchbarWidget.ID; + this.title.label = PreferencesSearchbarWidget.LABEL; + this.update(); + } + + protected handleSearch = (e: React.ChangeEvent): void => { + this.search(e.target.value); + }; + + protected search = debounce((value: string) => { + this.preferencesEventService.onSearch.fire({ query: value }); + this.update(); + }, 200); + + focus(): void { + if (this.searchbarRef.current) { + this.searchbarRef.current.focus(); + } + } + + render(): React.ReactNode { + return ( +
    +
    + +
    +
    + ); + } +} diff --git a/packages/preferences/src/browser/views/preference-tree-widget.tsx b/packages/preferences/src/browser/views/preference-tree-widget.tsx new file mode 100644 index 0000000000000..f2d126f2977b6 --- /dev/null +++ b/packages/preferences/src/browser/views/preference-tree-widget.tsx @@ -0,0 +1,157 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable, postConstruct } from 'inversify'; +import { Message } from '@phosphor/messaging'; +import { + ContextMenuRenderer, + ExpandableTreeNode, + PreferenceService, + TreeModel, + TreeNode, + TreeProps, + TreeWidget, + SelectableTreeNode, + TREE_NODE_CONTENT_CLASS, + NodeProps, + CompositeTreeNode, +} from '@theia/core/lib/browser'; +import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; +import React = require('react'); +import { PreferencesEventService } from '../util/preference-event-service'; +import { PreferencesDecorator } from '../preferences-decorator'; +import { PreferencesTreeProvider } from '../preference-tree-provider'; +import { Preference } from '../util/preference-types'; + +@injectable() +export class PreferencesTreeWidget extends TreeWidget { + static ID = 'preferences.tree'; + + protected shouldFireSelectionEvents: boolean = true; + protected firstVisibleLeafNodeID: string; + + @inject(PreferenceService) protected readonly preferenceService: PreferenceService; + @inject(PreferencesDecorator) protected readonly decorator: PreferencesDecorator; + @inject(PreferenceConfigurations) protected readonly preferenceConfigs: PreferenceConfigurations; + @inject(PreferencesTreeProvider) protected readonly preferenceTreeProvider: PreferencesTreeProvider; + @inject(TreeModel) readonly model: TreeModel; + @inject(TreeProps) protected readonly treeProps: TreeProps; + @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; + @inject(PreferencesEventService) protected readonly preferencesEventService: PreferencesEventService; + + @postConstruct() + init(): void { + super.init(); + this.preferencesEventService.onDisplayChanged.event(() => this.updateDisplay()); + this.preferencesEventService.onEditorScroll.event(e => { + this.handleEditorScroll(e.firstVisibleChildId); + }); + this.id = PreferencesTreeWidget.ID; + } + + protected handleEditorScroll(firstVisibleChildId: string): void { + this.shouldFireSelectionEvents = false; + if (firstVisibleChildId !== this.firstVisibleLeafNodeID) { + const { selectionAncestor, expansionAncestor } = this.getAncestorsForVisibleNode(firstVisibleChildId); + + this.firstVisibleLeafNodeID = firstVisibleChildId; + this.model.expandNode(expansionAncestor); + this.collapseAllExcept(expansionAncestor); + this.model.selectNode(selectionAncestor); + } + this.shouldFireSelectionEvents = true; + } + + protected collapseAllExcept(openNode: Preference.TreeExtension): void { + const children = (this.model.root as CompositeTreeNode).children as ExpandableTreeNode[]; + children.forEach(child => { + if (child !== openNode && child.expanded) { + this.model.collapseNode(child); + } + }); + } + + protected getAncestorsForVisibleNode(visibleNodeID: string): { selectionAncestor: SelectableTreeNode, expansionAncestor: ExpandableTreeNode } { + const isNonLeafNode = visibleNodeID.endsWith('-id'); + const isSubgroupNode = isNonLeafNode && visibleNodeID.includes('.'); + let expansionAncestor: ExpandableTreeNode; + let selectionAncestor: SelectableTreeNode; + + if (isSubgroupNode) { + selectionAncestor = this.model.getNode(visibleNodeID) as SelectableTreeNode; + expansionAncestor = selectionAncestor.parent as ExpandableTreeNode; + } else if (isNonLeafNode) { + selectionAncestor = this.model.getNode(visibleNodeID) as SelectableTreeNode; + expansionAncestor = selectionAncestor as Preference.TreeExtension as ExpandableTreeNode; + } else { + const labels = visibleNodeID.split('.'); + const hasSubgroupAncestor = labels.length > 2; + const expansionAncestorID = `${labels[0]}-id`; + expansionAncestor = this.model.getNode(expansionAncestorID) as ExpandableTreeNode; + if (hasSubgroupAncestor) { + const subgroupID = labels.slice(0, 2).join('.') + '-id'; + selectionAncestor = this.model.getNode(subgroupID) as SelectableTreeNode; + } else { + // The last selectable child that precedes the visible item alphabetically + selectionAncestor = [...expansionAncestor.children].reverse().find(child => child.id < visibleNodeID) as SelectableTreeNode || expansionAncestor; + } + } + return { selectionAncestor, expansionAncestor }; + } + + protected onAfterAttach(msg: Message): void { + this.updateDisplay(); + this.model.onSelectionChanged(previousAndCurrentSelectedNodes => this.fireEditorScrollForNewSelection(previousAndCurrentSelectedNodes)); + super.onAfterAttach(msg); + } + + protected updateDisplay(): void { + if (this.preferenceTreeProvider) { + this.model.root = this.preferenceTreeProvider.currentTree; + const nodes = Object.keys(this.preferenceTreeProvider.propertyList) + .map(propertyName => ({ [propertyName]: this.preferenceTreeProvider.propertyList[propertyName] })); + this.decorator.fireDidChangeDecorations(nodes); + this.update(); + } + } + + protected fireEditorScrollForNewSelection(previousAndCurrentSelectedNodes: readonly SelectableTreeNode[]): void { + if (this.shouldFireSelectionEvents) { + const [currentSelectedNode] = previousAndCurrentSelectedNodes; + this.firstVisibleLeafNodeID = currentSelectedNode.id; + this.preferencesEventService.onNavTreeSelection.fire({ nodeID: currentSelectedNode.id }); + } + } + + protected renderNode(node: TreeNode, props: NodeProps): React.ReactNode { + if (!TreeNode.isVisible(node)) { + return undefined; + } + const attributes = this.createNodeAttributes(node, props); + const content =
    + {this.renderExpansionToggle(node as Preference.Branch, props)} + {this.renderCaption(node, props)} +
    ; + return React.createElement('div', attributes, content); + } + + protected renderExpansionToggle(node: Preference.Branch, props: NodeProps): React.ReactNode { + if (node.children && node.children.every(child => !child.visible)) { + return
    ; + } + return super.renderExpansionToggle(node, props); + } +} diff --git a/packages/preferences/src/browser/views/preference-widget-bindings.ts b/packages/preferences/src/browser/views/preference-widget-bindings.ts new file mode 100644 index 0000000000000..77ca490e23f34 --- /dev/null +++ b/packages/preferences/src/browser/views/preference-widget-bindings.ts @@ -0,0 +1,81 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { interfaces } from 'inversify'; +import { WidgetFactory, createTreeContainer, TreeWidget, TreeProps, defaultTreeProps, TreeDecoratorService } from '@theia/core/lib/browser'; +import { SinglePreferenceDisplayFactory } from './components/single-preference-display-factory'; +import { SinglePreferenceWrapper } from './components/single-preference-wrapper'; +import { PreferencesWidget } from './preference-widget'; +import { PreferencesTreeWidget } from './preference-tree-widget'; +import { PreferencesEditorWidget } from './preference-editor-widget'; +import { PreferencesSearchbarWidget } from './preference-searchbar-widget'; +import { PreferencesScopeTabBar } from './preference-scope-tabbar-widget'; +import { PreferencesDecorator } from '../preferences-decorator'; +import { PreferencesDecoratorService } from '../preferences-decorator-service'; + +export function bindPreferencesWidgets(bind: interfaces.Bind): void { + bind(PreferencesWidget).toSelf().inSingletonScope(); + bind(WidgetFactory).toDynamicValue(({ container }) => ({ + id: PreferencesWidget.ID, + createWidget: () => container.get(PreferencesWidget) + })).inSingletonScope(); + + bind(SinglePreferenceWrapper).toSelf(); + + bind(PreferencesTreeWidget).toDynamicValue(ctx => + createPreferencesTree(ctx.container) + ).inSingletonScope(); + bind(WidgetFactory).toDynamicValue(context => ({ + id: PreferencesTreeWidget.ID, + createWidget: (): PreferencesTreeWidget => context.container.get(PreferencesTreeWidget), + })).inSingletonScope(); + + bind(PreferencesEditorWidget).toSelf().inSingletonScope(); + bind(WidgetFactory).toDynamicValue(context => ({ + id: PreferencesEditorWidget.ID, + createWidget: (): PreferencesEditorWidget => context.container.get(PreferencesEditorWidget), + })).inSingletonScope(); + + bind(PreferencesSearchbarWidget).toSelf().inSingletonScope(); + bind(WidgetFactory).toDynamicValue(context => ({ + id: PreferencesSearchbarWidget.ID, + createWidget: (): PreferencesSearchbarWidget => context.container.get(PreferencesSearchbarWidget), + })).inSingletonScope(); + + bind(PreferencesScopeTabBar).toSelf().inSingletonScope(); + bind(WidgetFactory).toDynamicValue(context => ({ + id: PreferencesScopeTabBar.ID, + createWidget: (): PreferencesScopeTabBar => context.container.get(PreferencesScopeTabBar), + })).inSingletonScope(); + + bind(SinglePreferenceDisplayFactory).toSelf().inSingletonScope(); +} + +function createPreferencesTree(parent: interfaces.Container): PreferencesTreeWidget { + const child = createTreeContainer(parent); + child.unbind(TreeWidget); + child.bind(PreferencesTreeWidget).toSelf(); + child.rebind(TreeProps).toConstantValue({ ...defaultTreeProps, search: false }); + + bindPreferencesDecorator(child); + + return child.get(PreferencesTreeWidget); +} + +function bindPreferencesDecorator(parent: interfaces.Container): void { + parent.bind(PreferencesDecorator).toSelf().inSingletonScope(); + parent.bind(PreferencesDecoratorService).toSelf().inSingletonScope(); + parent.rebind(TreeDecoratorService).toService(PreferencesDecoratorService); +} diff --git a/packages/preferences/src/browser/views/preference-widget.tsx b/packages/preferences/src/browser/views/preference-widget.tsx new file mode 100644 index 0000000000000..4326b82f49e16 --- /dev/null +++ b/packages/preferences/src/browser/views/preference-widget.tsx @@ -0,0 +1,97 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { postConstruct, injectable, inject } from 'inversify'; +import { WidgetManager, Panel, Widget, Message, } from '@theia/core/lib/browser'; +import { Preference } from '../util/preference-types'; +import { PreferencesEditorWidget } from './preference-editor-widget'; +import { PreferencesTreeWidget } from './preference-tree-widget'; +import { PreferencesSearchbarWidget } from './preference-searchbar-widget'; +import { PreferencesScopeTabBar } from './preference-scope-tabbar-widget'; + +@injectable() +export class PreferencesWidget extends Panel { + /** + * The widget `id`. + */ + static readonly ID = 'settings_widget'; + /** + * The widget `label` which is used for display purposes. + */ + static readonly LABEL = 'Preferences'; + + static readonly COMMAND_LABEL = 'Open Preferences'; + + protected _preferenceScope: Preference.SelectedScopeDetails = Preference.DEFAULT_SCOPE; + + @inject(PreferencesEditorWidget) protected editorWidget: PreferencesEditorWidget; + @inject(PreferencesTreeWidget) protected treeWidget: PreferencesTreeWidget; + @inject(PreferencesSearchbarWidget) protected searchbarWidget: PreferencesSearchbarWidget; + @inject(PreferencesScopeTabBar) protected tabBarWidget: PreferencesScopeTabBar; + @inject(WidgetManager) protected readonly manager: WidgetManager; + + get preferenceScope(): Preference.SelectedScopeDetails { + return this._preferenceScope; + } + + set preferenceScope(preferenceScopeDetails: Preference.SelectedScopeDetails) { + this._preferenceScope = preferenceScopeDetails; + this.editorWidget.preferenceScope = this._preferenceScope; + } + + protected onResize(msg: Widget.ResizeMessage): void { + super.onResize(msg); + if (msg.width < 600 && this.treeWidget && !this.treeWidget.isHidden) { + this.treeWidget.hide(); + this.editorWidget.addClass('full-pane'); + } else if (msg.width >= 600 && this.treeWidget && this.treeWidget.isHidden) { + this.treeWidget.show(); + this.editorWidget.removeClass('full-pane'); + } + } + + protected onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + this.searchbarWidget.focus(); + } + + @postConstruct() + protected async init(): Promise { + this.id = PreferencesWidget.ID; + this.title.label = PreferencesWidget.LABEL; + this.title.closable = true; + this.addClass('theia-settings-container'); + this.title.iconClass = 'fa fa-sliders'; + + this.searchbarWidget = await this.manager.getOrCreateWidget(PreferencesSearchbarWidget.ID); + this.searchbarWidget.addClass('preferences-searchbar-widget'); + this.addWidget(this.searchbarWidget); + + this.tabBarWidget = await this.manager.getOrCreateWidget(PreferencesScopeTabBar.ID); + this.tabBarWidget.addClass('preferences-tabbar-widget'); + this.addWidget(this.tabBarWidget); + + this.treeWidget = await this.manager.getOrCreateWidget(PreferencesTreeWidget.ID); + this.treeWidget.addClass('preferences-tree-widget'); + this.addWidget(this.treeWidget); + + this.editorWidget = await this.manager.getOrCreateWidget(PreferencesEditorWidget.ID); + this.editorWidget.addClass('preferences-editor-widget'); + this.addWidget(this.editorWidget); + + this.update(); + } +}