diff --git a/examples/browser/package.json b/examples/browser/package.json index 54495fe1d258d..c3e8f821077b9 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -49,6 +49,7 @@ "@theia/task": "1.22.1", "@theia/terminal": "1.22.1", "@theia/timeline": "1.22.1", + "@theia/toolbar": "1.22.1", "@theia/typehierarchy": "1.22.1", "@theia/userstorage": "1.22.1", "@theia/variable-resolver": "1.22.1", diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json index 68e09491d6eeb..163dd78f86414 100644 --- a/examples/browser/tsconfig.json +++ b/examples/browser/tsconfig.json @@ -110,6 +110,9 @@ { "path": "../../packages/timeline" }, + { + "path": "../../packages/toolbar" + }, { "path": "../../packages/typehierarchy" }, diff --git a/examples/electron/package.json b/examples/electron/package.json index 215d79c4d4be1..ebaa6402d0af0 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -50,6 +50,7 @@ "@theia/task": "1.22.1", "@theia/terminal": "1.22.1", "@theia/timeline": "1.22.1", + "@theia/toolbar": "1.22.1", "@theia/typehierarchy": "1.22.1", "@theia/userstorage": "1.22.1", "@theia/variable-resolver": "1.22.1", diff --git a/examples/electron/tsconfig.json b/examples/electron/tsconfig.json index bf8bd216f341b..1ba97be103fc7 100644 --- a/examples/electron/tsconfig.json +++ b/examples/electron/tsconfig.json @@ -113,6 +113,9 @@ { "path": "../../packages/timeline" }, + { + "path": "../../packages/toolbar" + }, { "path": "../../packages/typehierarchy" }, diff --git a/packages/core/src/browser/quick-input/quick-command-service.ts b/packages/core/src/browser/quick-input/quick-command-service.ts index 49963538a5935..bfab32360de48 100644 --- a/packages/core/src/browser/quick-input/quick-command-service.ts +++ b/packages/core/src/browser/quick-input/quick-command-service.ts @@ -98,7 +98,7 @@ export class QuickCommandService implements QuickAccessContribution, QuickAccess return items; } - private toItem(command: Command): QuickPickItem { + toItem(command: Command): QuickPickItem { const label = (command.category) ? `${command.category}: ` + command.label! : command.label!; const iconClasses = this.getItemIconClasses(command); const activeElement = window.document.activeElement as HTMLElement; diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index 6748a855f6e91..81ca2d18c3ec7 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -143,25 +143,25 @@ export class ApplicationShell extends Widget { /** * The dock panel in the main shell area. This is where editors usually go to. */ - readonly mainPanel: TheiaDockPanel; + mainPanel: TheiaDockPanel; /** * The dock panel in the bottom shell area. In contrast to the main panel, the bottom panel * can be collapsed and expanded. */ - readonly bottomPanel: TheiaDockPanel; + bottomPanel: TheiaDockPanel; /** * Handler for the left side panel. The primary application views go here, such as the * file explorer and the git view. */ - readonly leftPanelHandler: SidePanelHandler; + leftPanelHandler: SidePanelHandler; /** * Handler for the right side panel. The secondary application views go here, such as the * outline view. */ - readonly rightPanelHandler: SidePanelHandler; + rightPanelHandler: SidePanelHandler; /** * General options for the application shell. @@ -171,7 +171,7 @@ export class ApplicationShell extends Widget { /** * The fixed-size panel shown on top. This one usually holds the main menu. */ - readonly topPanel: Panel; + topPanel: Panel; /** * The current state of the bottom panel. @@ -212,29 +212,49 @@ export class ApplicationShell extends Widget { constructor( @inject(DockPanelRendererFactory) protected dockPanelRendererFactory: () => DockPanelRenderer, @inject(StatusBarImpl) protected readonly statusBar: StatusBarImpl, - @inject(SidePanelHandlerFactory) sidePanelHandlerFactory: () => SidePanelHandler, + @inject(SidePanelHandlerFactory) protected readonly sidePanelHandlerFactory: () => SidePanelHandler, @inject(SplitPositionHandler) protected splitPositionHandler: SplitPositionHandler, @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService, @inject(ApplicationShellOptions) @optional() options: RecursivePartial = {}, @inject(CorePreferences) protected readonly corePreferences: CorePreferences ) { super(options as Widget.IOptions); + } + + @postConstruct() + protected init(): void { + this.initializeShell(); + this.initSidebarVisibleKeyContext(); + this.initFocusKeyContexts(); + + if (!environment.electron.is()) { + this.corePreferences.ready.then(() => { + this.setTopPanelVisibility(this.corePreferences['window.menuBarVisibility']); + }); + this.corePreferences.onPreferenceChanged(preference => { + if (preference.preferenceName === 'window.menuBarVisibility') { + this.setTopPanelVisibility(preference.newValue); + } + }); + } + } + + protected initializeShell(): void { this.addClass(APPLICATION_SHELL_CLASS); this.id = 'theia-app-shell'; - // Merge the user-defined application options with the default options this.options = { bottomPanel: { ...ApplicationShell.DEFAULT_OPTIONS.bottomPanel, - ...options.bottomPanel || {} + ...this.options?.bottomPanel || {} }, leftPanel: { ...ApplicationShell.DEFAULT_OPTIONS.leftPanel, - ...options.leftPanel || {} + ...this.options?.leftPanel || {} }, rightPanel: { ...ApplicationShell.DEFAULT_OPTIONS.rightPanel, - ...options.rightPanel || {} + ...this.options?.rightPanel || {} } }; @@ -242,12 +262,12 @@ export class ApplicationShell extends Widget { this.topPanel = this.createTopPanel(); this.bottomPanel = this.createBottomPanel(); - this.leftPanelHandler = sidePanelHandlerFactory(); + this.leftPanelHandler = this.sidePanelHandlerFactory(); this.leftPanelHandler.create('left', this.options.leftPanel); this.leftPanelHandler.dockPanel.widgetAdded.connect((_, widget) => this.fireDidAddWidget(widget)); this.leftPanelHandler.dockPanel.widgetRemoved.connect((_, widget) => this.fireDidRemoveWidget(widget)); - this.rightPanelHandler = sidePanelHandlerFactory(); + this.rightPanelHandler = this.sidePanelHandlerFactory(); this.rightPanelHandler.create('right', this.options.rightPanel); this.rightPanelHandler.dockPanel.widgetAdded.connect((_, widget) => this.fireDidAddWidget(widget)); this.rightPanelHandler.dockPanel.widgetRemoved.connect((_, widget) => this.fireDidRemoveWidget(widget)); @@ -258,23 +278,6 @@ export class ApplicationShell extends Widget { this.tracker.activeChanged.connect(this.onActiveChanged, this); } - @postConstruct() - protected init(): void { - this.initSidebarVisibleKeyContext(); - this.initFocusKeyContexts(); - - if (!environment.electron.is()) { - this.corePreferences.ready.then(() => { - this.setTopPanelVisibility(this.corePreferences['window.menuBarVisibility']); - }); - this.corePreferences.onPreferenceChanged(preference => { - if (preference.preferenceName === 'window.menuBarVisibility') { - this.setTopPanelVisibility(preference.newValue); - } - }); - } - } - protected initSidebarVisibleKeyContext(): void { const leftSideBarPanel = this.leftPanelHandler.dockPanel; const sidebarVisibleKey = this.contextKeyService.createKey('sidebarVisible', leftSideBarPanel.isVisible); diff --git a/packages/toolbar/.eslintrc.js b/packages/toolbar/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/toolbar/.eslintrc.js @@ -0,0 +1,10 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: [ + '../../configs/build.eslintrc.json' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'tsconfig.json' + } +}; diff --git a/packages/toolbar/fetch-icons.js b/packages/toolbar/fetch-icons.js new file mode 100644 index 0000000000000..78cf2311f5030 --- /dev/null +++ b/packages/toolbar/fetch-icons.js @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (C) 2022 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 + ********************************************************************************/ + +const fs = require('fs'); +const path = require('path'); + +// This script generates an JSON array of font-awesome classnames from the font-awesome.css files +const fontAwesomeCSSPath = path.resolve(__dirname, '../../node_modules/font-awesome/css/font-awesome.css'); +const fontAwesomeDestination = path.resolve(__dirname, './src/browser/font-awesome.json'); + +const codiconCSSPath = path.resolve(__dirname, '../../node_modules/@vscode/codicons/dist/codicon.css') +const codiconDestination = path.resolve(__dirname, './src/browser/codicon.json') + +const faContent = fs.readFileSync(fontAwesomeCSSPath, 'utf-8'); +const regexp = /([\w,-]*):before/gm; +let faArray; +const faMatches = []; +while (faArray = regexp.exec(faContent)) { + faMatches.push(faArray[1]); +} +fs.writeFileSync(fontAwesomeDestination, JSON.stringify(faMatches)); + +const codiconContent = fs.readFileSync(codiconCSSPath, 'utf-8'); +let codiconArray; +const codiconMatches = []; +while (codiconArray = regexp.exec(codiconContent)) { + codiconMatches.push(codiconArray[1]); +} +fs.writeFileSync(codiconDestination, JSON.stringify(codiconMatches)); diff --git a/packages/toolbar/package.json b/packages/toolbar/package.json new file mode 100644 index 0000000000000..108de826237ec --- /dev/null +++ b/packages/toolbar/package.json @@ -0,0 +1,50 @@ +{ + "name": "@theia/toolbar", + "version": "1.22.1", + "description": "Theia - Toolbar", + "keywords": [ + "theia-extension" + ], + "homepage": "https://github.com/eclipse-theia/theia", + "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-theia/theia.git" + }, + "bugs": { + "url": "https://github.com/eclipse-theia/theia/issues" + }, + "files": [ + "lib", + "src" + ], + "scripts": { + "build": "theiaext build", + "clean": "theiaext clean", + "compile": "theiaext compile", + "lint": "theiaext lint", + "test": "theiaext test", + "watch": "theiaext watch" + }, + "dependencies": { + "@theia/core": "1.22.1", + "@theia/editor": "1.22.1", + "@theia/file-search": "1.22.1", + "@theia/filesystem": "1.22.1", + "@theia/monaco": "1.22.1", + "@theia/search-in-workspace": "1.22.1", + "@theia/userstorage": "1.22.1", + "@theia/workspace": "1.22.1", + "ajv": "^6.5.3", + "jsonc-parser": "^2.2.0", + "perfect-scrollbar": "^1.3.0" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/main-toolbar-frontend-module" + } + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/toolbar/src/browser/abstract-main-toolbar-contribution.tsx b/packages/toolbar/src/browser/abstract-main-toolbar-contribution.tsx new file mode 100644 index 0000000000000..121fb8a8fab0c --- /dev/null +++ b/packages/toolbar/src/browser/abstract-main-toolbar-contribution.tsx @@ -0,0 +1,56 @@ +/******************************************************************************** + * Copyright (C) 2022 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 '@theia/core/shared/react'; +import { CommandService, Emitter } from '@theia/core'; +import { injectable, inject } from '@theia/core/shared/inversify'; +import { ContextMenuRenderer, KeybindingRegistry } from '@theia/core/lib/browser'; +import { ReactTabBarToolbarContribution, ToolbarAlignment } from './main-toolbar-interfaces'; + +@injectable() +export abstract class AbstractMainToolbarContribution implements ReactTabBarToolbarContribution { + abstract id: string; + abstract column: ToolbarAlignment; + abstract priority: number; + newGroup = true; + + protected didChangeEmitter = new Emitter(); + readonly onDidChange = this.didChangeEmitter.event; + + @inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry; + @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; + @inject(CommandService) protected readonly commandService: CommandService; + + abstract render(): React.ReactNode; + + toJSON(): { id: string; group: string } { + return { id: this.id, group: 'contributed' }; + } + + protected resolveKeybindingForCommand(commandID: string | undefined): string { + if (!commandID) { + return ''; + } + const keybindings = this.keybindingRegistry.getKeybindingsForCommand(commandID); + if (keybindings.length > 0) { + const binding = keybindings[0]; + const bindingKeySequence = this.keybindingRegistry.resolveKeybinding(binding); + const keyCode = bindingKeySequence[0]; + return ` (${this.keybindingRegistry.acceleratorForKeyCode(keyCode, '+')})`; + } + return ''; + } +} diff --git a/packages/toolbar/src/browser/application-shell-with-toolbar-override.ts b/packages/toolbar/src/browser/application-shell-with-toolbar-override.ts new file mode 100644 index 0000000000000..634ee5307c9e1 --- /dev/null +++ b/packages/toolbar/src/browser/application-shell-with-toolbar-override.ts @@ -0,0 +1,94 @@ +/******************************************************************************** + * Copyright (C) 2022 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 { + ApplicationShell, + Layout, + PreferenceService, + SplitPanel, +} from '@theia/core/lib/browser'; +import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; +import { MAXIMIZED_CLASS } from '@theia/core/lib/browser/shell/theia-dock-panel'; +import { MainToolbar, MainToolbarFactory } from './main-toolbar-interfaces'; +import { MainToolbarPreferences, TOOLBAR_ENABLE_PREFERENCE_ID } from './main-toolbar-preference-contribution'; + +@injectable() +export class ApplicationShellWithToolbarOverride extends ApplicationShell { + @inject(MainToolbarPreferences) protected toolbarPreferences: MainToolbarPreferences; + @inject(PreferenceService) protected readonly preferenceService: PreferenceService; + @inject(MainToolbarFactory) protected readonly mainToolbarFactory: () => MainToolbar; + + protected mainToolbar: MainToolbar; + + @postConstruct() + protected async init(): Promise { + this.mainToolbar = this.mainToolbarFactory(); + this.mainToolbar.id = 'main-toolbar'; + super.init(); + await this.toolbarPreferences.ready; + this.tryShowToolbar(); + this.mainPanel.onDidToggleMaximized(() => { + this.tryShowToolbar(); + }); + this.bottomPanel.onDidToggleMaximized(() => { + this.tryShowToolbar(); + }); + this.preferenceService.onPreferenceChanged(event => { + if (event.preferenceName === TOOLBAR_ENABLE_PREFERENCE_ID) { + this.tryShowToolbar(); + } + }); + } + + protected tryShowToolbar(): boolean { + const doShowToolbarFromPreference = this.toolbarPreferences[TOOLBAR_ENABLE_PREFERENCE_ID]; + const isShellMaximized = this.mainPanel.hasClass(MAXIMIZED_CLASS) || this.bottomPanel.hasClass(MAXIMIZED_CLASS); + if (doShowToolbarFromPreference && !isShellMaximized) { + this.mainToolbar.show(); + return true; + } + this.mainToolbar.hide(); + return false; + } + + protected createLayout(): Layout { + const bottomSplitLayout = this.createSplitLayout( + [this.mainPanel, this.bottomPanel], + [1, 0], + { orientation: 'vertical', spacing: 0 }, + ); + const panelForBottomArea = new SplitPanel({ layout: bottomSplitLayout }); + panelForBottomArea.id = 'theia-bottom-split-panel'; + + const leftRightSplitLayout = this.createSplitLayout( + [this.leftPanelHandler.container, panelForBottomArea, this.rightPanelHandler.container], + [0, 1, 0], + { orientation: 'horizontal', spacing: 0 }, + ); + const panelForSideAreas = new SplitPanel({ layout: leftRightSplitLayout }); + panelForSideAreas.id = 'theia-left-right-split-panel'; + return this.createBoxLayout( + [this.topPanel, this.mainToolbar, panelForSideAreas, this.statusBar], + [0, 0, 1, 0], + { direction: 'top-to-bottom', spacing: 0 }, + ); + } +} + +export const bindToolbarApplicationShell = (bind: interfaces.Bind, rebind: interfaces.Rebind, unbind: interfaces.Unbind): void => { + bind(ApplicationShellWithToolbarOverride).toSelf().inSingletonScope(); + rebind(ApplicationShell).toService(ApplicationShellWithToolbarOverride); +}; diff --git a/packages/toolbar/src/browser/codicons.ts b/packages/toolbar/src/browser/codicons.ts new file mode 100644 index 0000000000000..5371abeb562e1 --- /dev/null +++ b/packages/toolbar/src/browser/codicons.ts @@ -0,0 +1,18 @@ +/******************************************************************************** + * Copyright (C) 2022 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/quotes, max-len */ +export const codicons = ["codicon-add", "codicon-plus", "codicon-gist-new", "codicon-repo-create", "codicon-lightbulb", "codicon-light-bulb", "codicon-repo", "codicon-repo-delete", "codicon-gist-fork", "codicon-repo-forked", "codicon-git-pull-request", "codicon-git-pull-request-abandoned", "codicon-record-keys", "codicon-keyboard", "codicon-tag", "codicon-tag-add", "codicon-tag-remove", "codicon-person", "codicon-person-follow", "codicon-person-outline", "codicon-person-filled", "codicon-git-branch", "codicon-git-branch-create", "codicon-git-branch-delete", "codicon-source-control", "codicon-mirror", "codicon-mirror-public", "codicon-star", "codicon-star-add", "codicon-star-delete", "codicon-star-empty", "codicon-comment", "codicon-comment-add", "codicon-alert", "codicon-warning", "codicon-search", "codicon-search-save", "codicon-log-out", "codicon-sign-out", "codicon-log-in", "codicon-sign-in", "codicon-eye", "codicon-eye-unwatch", "codicon-eye-watch", "codicon-circle-filled", "codicon-primitive-dot", "codicon-close-dirty", "codicon-debug-breakpoint", "codicon-debug-breakpoint-disabled", "codicon-debug-hint", "codicon-primitive-square", "codicon-edit", "codicon-pencil", "codicon-info", "codicon-issue-opened", "codicon-gist-private", "codicon-git-fork-private", "codicon-lock", "codicon-mirror-private", "codicon-close", "codicon-remove-close", "codicon-x", "codicon-repo-sync", "codicon-sync", "codicon-clone", "codicon-desktop-download", "codicon-beaker", "codicon-microscope", "codicon-vm", "codicon-device-desktop", "codicon-file", "codicon-file-text", "codicon-more", "codicon-ellipsis", "codicon-kebab-horizontal", "codicon-mail-reply", "codicon-reply", "codicon-organization", "codicon-organization-filled", "codicon-organization-outline", "codicon-new-file", "codicon-file-add", "codicon-new-folder", "codicon-file-directory-create", "codicon-trash", "codicon-trashcan", "codicon-history", "codicon-clock", "codicon-folder", "codicon-file-directory", "codicon-symbol-folder", "codicon-logo-github", "codicon-mark-github", "codicon-github", "codicon-terminal", "codicon-console", "codicon-repl", "codicon-zap", "codicon-symbol-event", "codicon-error", "codicon-stop", "codicon-variable", "codicon-symbol-variable", "codicon-array", "codicon-symbol-array", "codicon-symbol-module", "codicon-symbol-package", "codicon-symbol-namespace", "codicon-symbol-object", "codicon-symbol-method", "codicon-symbol-function", "codicon-symbol-constructor", "codicon-symbol-boolean", "codicon-symbol-null", "codicon-symbol-numeric", "codicon-symbol-number", "codicon-symbol-structure", "codicon-symbol-struct", "codicon-symbol-parameter", "codicon-symbol-type-parameter", "codicon-symbol-key", "codicon-symbol-text", "codicon-symbol-reference", "codicon-go-to-file", "codicon-symbol-enum", "codicon-symbol-value", "codicon-symbol-ruler", "codicon-symbol-unit", "codicon-activate-breakpoints", "codicon-archive", "codicon-arrow-both", "codicon-arrow-down", "codicon-arrow-left", "codicon-arrow-right", "codicon-arrow-small-down", "codicon-arrow-small-left", "codicon-arrow-small-right", "codicon-arrow-small-up", "codicon-arrow-up", "codicon-bell", "codicon-bold", "codicon-book", "codicon-bookmark", "codicon-debug-breakpoint-conditional-unverified", "codicon-debug-breakpoint-conditional", "codicon-debug-breakpoint-conditional-disabled", "codicon-debug-breakpoint-data-unverified", "codicon-debug-breakpoint-data", "codicon-debug-breakpoint-data-disabled", "codicon-debug-breakpoint-log-unverified", "codicon-debug-breakpoint-log", "codicon-debug-breakpoint-log-disabled", "codicon-briefcase", "codicon-broadcast", "codicon-browser", "codicon-bug", "codicon-calendar", "codicon-case-sensitive", "codicon-check", "codicon-checklist", "codicon-chevron-down", "codicon-chevron-left", "codicon-chevron-right", "codicon-chevron-up", "codicon-chrome-close", "codicon-chrome-maximize", "codicon-chrome-minimize", "codicon-chrome-restore", "codicon-circle-outline", "codicon-debug-breakpoint-unverified", "codicon-circle-slash", "codicon-circuit-board", "codicon-clear-all", "codicon-clippy", "codicon-close-all", "codicon-cloud-download", "codicon-cloud-upload", "codicon-code", "codicon-collapse-all", "codicon-color-mode", "codicon-comment-discussion", "codicon-credit-card", "codicon-dash", "codicon-dashboard", "codicon-database", "codicon-debug-continue", "codicon-debug-disconnect", "codicon-debug-pause", "codicon-debug-restart", "codicon-debug-start", "codicon-debug-step-into", "codicon-debug-step-out", "codicon-debug-step-over", "codicon-debug-stop", "codicon-debug", "codicon-device-camera-video", "codicon-device-camera", "codicon-device-mobile", "codicon-diff-added", "codicon-diff-ignored", "codicon-diff-modified", "codicon-diff-removed", "codicon-diff-renamed", "codicon-diff", "codicon-discard", "codicon-editor-layout", "codicon-empty-window", "codicon-exclude", "codicon-extensions", "codicon-eye-closed", "codicon-file-binary", "codicon-file-code", "codicon-file-media", "codicon-file-pdf", "codicon-file-submodule", "codicon-file-symlink-directory", "codicon-file-symlink-file", "codicon-file-zip", "codicon-files", "codicon-filter", "codicon-flame", "codicon-fold-down", "codicon-fold-up", "codicon-fold", "codicon-folder-active", "codicon-folder-opened", "codicon-gear", "codicon-gift", "codicon-gist-secret", "codicon-gist", "codicon-git-commit", "codicon-git-compare", "codicon-compare-changes", "codicon-git-merge", "codicon-github-action", "codicon-github-alt", "codicon-globe", "codicon-grabber", "codicon-graph", "codicon-gripper", "codicon-heart", "codicon-home", "codicon-horizontal-rule", "codicon-hubot", "codicon-inbox", "codicon-issue-reopened", "codicon-issues", "codicon-italic", "codicon-jersey", "codicon-json", "codicon-kebab-vertical", "codicon-key", "codicon-law", "codicon-lightbulb-autofix", "codicon-link-external", "codicon-link", "codicon-list-ordered", "codicon-list-unordered", "codicon-live-share", "codicon-loading", "codicon-location", "codicon-mail-read", "codicon-mail", "codicon-markdown", "codicon-megaphone", "codicon-mention", "codicon-milestone", "codicon-mortar-board", "codicon-move", "codicon-multiple-windows", "codicon-mute", "codicon-no-newline", "codicon-note", "codicon-octoface", "codicon-open-preview", "codicon-package", "codicon-paintcan", "codicon-pin", "codicon-play", "codicon-run", "codicon-plug", "codicon-preserve-case", "codicon-preview", "codicon-project", "codicon-pulse", "codicon-question", "codicon-quote", "codicon-radio-tower", "codicon-reactions", "codicon-references", "codicon-refresh", "codicon-regex", "codicon-remote-explorer", "codicon-remote", "codicon-remove", "codicon-replace-all", "codicon-replace", "codicon-repo-clone", "codicon-repo-force-push", "codicon-repo-pull", "codicon-repo-push", "codicon-report", "codicon-request-changes", "codicon-rocket", "codicon-root-folder-opened", "codicon-root-folder", "codicon-rss", "codicon-ruby", "codicon-save-all", "codicon-save-as", "codicon-save", "codicon-screen-full", "codicon-screen-normal", "codicon-search-stop", "codicon-server", "codicon-settings-gear", "codicon-settings", "codicon-shield", "codicon-smiley", "codicon-sort-precedence", "codicon-split-horizontal", "codicon-split-vertical", "codicon-squirrel", "codicon-star-full", "codicon-star-half", "codicon-symbol-class", "codicon-symbol-color", "codicon-symbol-constant", "codicon-symbol-enum-member", "codicon-symbol-field", "codicon-symbol-file", "codicon-symbol-interface", "codicon-symbol-keyword", "codicon-symbol-misc", "codicon-symbol-operator", "codicon-symbol-property", "codicon-wrench", "codicon-wrench-subaction", "codicon-symbol-snippet", "codicon-tasklist", "codicon-telescope", "codicon-text-size", "codicon-three-bars", "codicon-thumbsdown", "codicon-thumbsup", "codicon-tools", "codicon-triangle-down", "codicon-triangle-left", "codicon-triangle-right", "codicon-triangle-up", "codicon-twitter", "codicon-unfold", "codicon-unlock", "codicon-unmute", "codicon-unverified", "codicon-verified", "codicon-versions", "codicon-vm-active", "codicon-vm-outline", "codicon-vm-running", "codicon-watch", "codicon-whitespace", "codicon-whole-word", "codicon-window", "codicon-word-wrap", "codicon-zoom-in", "codicon-zoom-out", "codicon-list-filter", "codicon-list-flat", "codicon-list-selection", "codicon-selection", "codicon-list-tree", "codicon-debug-breakpoint-function-unverified", "codicon-debug-breakpoint-function", "codicon-debug-breakpoint-function-disabled", "codicon-debug-stackframe-active", "codicon-debug-stackframe-dot", "codicon-debug-stackframe", "codicon-debug-stackframe-focused", "codicon-debug-breakpoint-unsupported", "codicon-symbol-string", "codicon-debug-reverse-continue", "codicon-debug-step-back", "codicon-debug-restart-frame", "codicon-debug-alt", "codicon-call-incoming", "codicon-call-outgoing", "codicon-menu", "codicon-expand-all", "codicon-feedback", "codicon-group-by-ref-type", "codicon-ungroup-by-ref-type", "codicon-account", "codicon-bell-dot", "codicon-debug-console", "codicon-library", "codicon-output", "codicon-run-all", "codicon-sync-ignored", "codicon-pinned", "codicon-github-inverted", "codicon-server-process", "codicon-server-environment", "codicon-pass", "codicon-issue-closed", "codicon-stop-circle", "codicon-play-circle", "codicon-record", "codicon-debug-alt-small", "codicon-vm-connect", "codicon-cloud", "codicon-merge", "codicon-export", "codicon-graph-left", "codicon-magnet", "codicon-notebook", "codicon-redo", "codicon-check-all", "codicon-pinned-dirty", "codicon-pass-filled", "codicon-circle-large-filled", "codicon-circle-large-outline", "codicon-combine", "codicon-gather", "codicon-table", "codicon-variable-group", "codicon-type-hierarchy", "codicon-type-hierarchy-sub", "codicon-type-hierarchy-super", "codicon-git-pull-request-create", "codicon-run-above", "codicon-run-below", "codicon-notebook-template", "codicon-debug-rerun", "codicon-workspace-trusted", "codicon-workspace-untrusted", "codicon-workspace-unknown", "codicon-terminal-cmd", "codicon-terminal-debian", "codicon-terminal-linux", "codicon-terminal-powershell", "codicon-terminal-tmux", "codicon-terminal-ubuntu", "codicon-terminal-bash", "codicon-arrow-swap", "codicon-copy", "codicon-person-add", "codicon-filter-filled", "codicon-wand", "codicon-debug-line-by-line", "codicon-inspect", "codicon-layers", "codicon-layers-dot", "codicon-layers-active", "codicon-compass", "codicon-compass-dot", "codicon-compass-active", "codicon-azure", "codicon-issue-draft", "codicon-git-pull-request-closed", "codicon-git-pull-request-draft", "codicon-debug-all", "codicon-debug-coverage"]; diff --git a/packages/toolbar/src/browser/easy-search-toolbar-item.tsx b/packages/toolbar/src/browser/easy-search-toolbar-item.tsx new file mode 100644 index 0000000000000..5f0a2c88a84e4 --- /dev/null +++ b/packages/toolbar/src/browser/easy-search-toolbar-item.tsx @@ -0,0 +1,145 @@ +/******************************************************************************** + * Copyright (C) 2022 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 { CommandContribution, CommandRegistry, CommandService, MenuContribution, MenuModelRegistry } from '@theia/core'; +import { ContextMenuRenderer, quickCommand } from '@theia/core/lib/browser'; +import { inject, injectable, interfaces } from '@theia/core/shared/inversify'; +import * as React from '@theia/core/shared/react'; +import { quickFileOpen } from '@theia/file-search/lib/browser/quick-file-open'; +import { SearchInWorkspaceCommands } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { AbstractMainToolbarContribution } from './abstract-main-toolbar-contribution'; +import { SearchInWorkspaceQuickInputService } from './search-in-workspace-root-quick-input-service'; +import { MainToolbarMenus, ReactInteraction } from './main-toolbar-constants'; +import { + ReactTabBarToolbarContribution, + ToolbarAlignment, +} from './main-toolbar-interfaces'; + +export const SEARCH_FOR_FILE = { + id: 'main.toolbar.search.for.file', + category: 'Search', + label: 'Search for a File', +}; + +export const FIND_IN_WORKSPACE_ROOT = { + id: 'main.toolbar.find.in.workspace.root', + category: 'Search', + label: 'Search Workspace Root for Text', +}; + +@injectable() +export class EasySearchToolbarItem extends AbstractMainToolbarContribution + implements CommandContribution, + MenuContribution { + id = 'easy-search-toolbar-widget'; + column = ToolbarAlignment.RIGHT; + priority = 1; + newGroup = true; + + @inject(CommandService) protected readonly commandService: CommandService; + @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; + @inject(SearchInWorkspaceQuickInputService) protected readonly searchPickService: SearchInWorkspaceQuickInputService; + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + + protected handleOnClick = (e: ReactInteraction): void => this.doHandleOnClick(e); + protected doHandleOnClick(e: ReactInteraction): void { + e.stopPropagation(); + const toolbar = document.querySelector('#main-toolbar'); + if (toolbar) { + const { bottom } = toolbar.getBoundingClientRect(); + const { left } = e.currentTarget.getBoundingClientRect(); + this.contextMenuRenderer.render({ + menuPath: MainToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, + anchor: { x: left, y: bottom }, + }); + } + } + + render(): React.ReactNode { + return ( +
+
+
+
); + } + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(FIND_IN_WORKSPACE_ROOT, { + execute: async () => { + const wsRoots = await this.workspaceService.roots; + if (!wsRoots.length) { + await this.commandService.executeCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER.id); + } else if (wsRoots.length === 1) { + const { resource } = wsRoots[0]; + await this.commandService.executeCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER.id, [resource]); + } else { + this.searchPickService.open(); + } + }, + }); + registry.registerCommand(SEARCH_FOR_FILE, { + execute: () => { + this.commandService.executeCommand(quickFileOpen.id); + }, + }); + } + + registerMenus(registry: MenuModelRegistry): void { + registry.registerMenuAction(MainToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, { + commandId: quickCommand.id, + label: 'Find a Command', + order: 'a', + }); + registry.registerMenuAction(MainToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, { + commandId: SEARCH_FOR_FILE.id, + order: 'b', + }); + registry.registerMenuAction(MainToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, { + commandId: SearchInWorkspaceCommands.OPEN_SIW_WIDGET.id, + label: 'Search Entire Workspace For Text', + order: 'c', + }); + registry.registerMenuAction(MainToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, { + commandId: FIND_IN_WORKSPACE_ROOT.id, + order: 'd', + }); + registry.registerMenuAction(MainToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, { + commandId: 'languages.workspace.symbol', + label: 'Search for a Symbol', + order: 'e', + }); + } +} + +export const bindEasySearchToolbarWidget = (bind: interfaces.Bind): void => { + bind(EasySearchToolbarItem).toSelf().inSingletonScope(); + bind(ReactTabBarToolbarContribution).to(EasySearchToolbarItem); + bind(CommandContribution).to(EasySearchToolbarItem); + bind(MenuContribution).to(EasySearchToolbarItem); +}; diff --git a/packages/toolbar/src/browser/font-awesome-icons.ts b/packages/toolbar/src/browser/font-awesome-icons.ts new file mode 100644 index 0000000000000..a9f6a1c0dcd4d --- /dev/null +++ b/packages/toolbar/src/browser/font-awesome-icons.ts @@ -0,0 +1,18 @@ +/******************************************************************************** + * Copyright (C) 2022 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/quotes, max-len */ +export const fontAwesomeIcons = ["fa-glass", "fa-music", "fa-search", "fa-envelope-o", "fa-heart", "fa-star", "fa-star-o", "fa-user", "fa-film", "fa-th-large", "fa-th", "fa-th-list", "fa-check", "fa-remove", "fa-close", "fa-times", "fa-search-plus", "fa-search-minus", "fa-power-off", "fa-signal", "fa-gear", "fa-cog", "fa-trash-o", "fa-home", "fa-file-o", "fa-clock-o", "fa-road", "fa-download", "fa-arrow-circle-o-down", "fa-arrow-circle-o-up", "fa-inbox", "fa-play-circle-o", "fa-rotate-right", "fa-repeat", "fa-refresh", "fa-list-alt", "fa-lock", "fa-flag", "fa-headphones", "fa-volume-off", "fa-volume-down", "fa-volume-up", "fa-qrcode", "fa-barcode", "fa-tag", "fa-tags", "fa-book", "fa-bookmark", "fa-print", "fa-camera", "fa-font", "fa-bold", "fa-italic", "fa-text-height", "fa-text-width", "fa-align-left", "fa-align-center", "fa-align-right", "fa-align-justify", "fa-list", "fa-dedent", "fa-outdent", "fa-indent", "fa-video-camera", "fa-photo", "fa-image", "fa-picture-o", "fa-pencil", "fa-map-marker", "fa-adjust", "fa-tint", "fa-edit", "fa-pencil-square-o", "fa-share-square-o", "fa-check-square-o", "fa-arrows", "fa-step-backward", "fa-fast-backward", "fa-backward", "fa-play", "fa-pause", "fa-stop", "fa-forward", "fa-fast-forward", "fa-step-forward", "fa-eject", "fa-chevron-left", "fa-chevron-right", "fa-plus-circle", "fa-minus-circle", "fa-times-circle", "fa-check-circle", "fa-question-circle", "fa-info-circle", "fa-crosshairs", "fa-times-circle-o", "fa-check-circle-o", "fa-ban", "fa-arrow-left", "fa-arrow-right", "fa-arrow-up", "fa-arrow-down", "fa-mail-forward", "fa-share", "fa-expand", "fa-compress", "fa-plus", "fa-minus", "fa-asterisk", "fa-exclamation-circle", "fa-gift", "fa-leaf", "fa-fire", "fa-eye", "fa-eye-slash", "fa-warning", "fa-exclamation-triangle", "fa-plane", "fa-calendar", "fa-random", "fa-comment", "fa-magnet", "fa-chevron-up", "fa-chevron-down", "fa-retweet", "fa-shopping-cart", "fa-folder", "fa-folder-open", "fa-arrows-v", "fa-arrows-h", "fa-bar-chart-o", "fa-bar-chart", "fa-twitter-square", "fa-facebook-square", "fa-camera-retro", "fa-key", "fa-gears", "fa-cogs", "fa-comments", "fa-thumbs-o-up", "fa-thumbs-o-down", "fa-star-half", "fa-heart-o", "fa-sign-out", "fa-linkedin-square", "fa-thumb-tack", "fa-external-link", "fa-sign-in", "fa-trophy", "fa-github-square", "fa-upload", "fa-lemon-o", "fa-phone", "fa-square-o", "fa-bookmark-o", "fa-phone-square", "fa-twitter", "fa-facebook-f", "fa-facebook", "fa-github", "fa-unlock", "fa-credit-card", "fa-feed", "fa-rss", "fa-hdd-o", "fa-bullhorn", "fa-bell", "fa-certificate", "fa-hand-o-right", "fa-hand-o-left", "fa-hand-o-up", "fa-hand-o-down", "fa-arrow-circle-left", "fa-arrow-circle-right", "fa-arrow-circle-up", "fa-arrow-circle-down", "fa-globe", "fa-wrench", "fa-tasks", "fa-filter", "fa-briefcase", "fa-arrows-alt", "fa-group", "fa-users", "fa-chain", "fa-link", "fa-cloud", "fa-flask", "fa-cut", "fa-scissors", "fa-copy", "fa-files-o", "fa-paperclip", "fa-save", "fa-floppy-o", "fa-square", "fa-navicon", "fa-reorder", "fa-bars", "fa-list-ul", "fa-list-ol", "fa-strikethrough", "fa-underline", "fa-table", "fa-magic", "fa-truck", "fa-pinterest", "fa-pinterest-square", "fa-google-plus-square", "fa-google-plus", "fa-money", "fa-caret-down", "fa-caret-up", "fa-caret-left", "fa-caret-right", "fa-columns", "fa-unsorted", "fa-sort", "fa-sort-down", "fa-sort-desc", "fa-sort-up", "fa-sort-asc", "fa-envelope", "fa-linkedin", "fa-rotate-left", "fa-undo", "fa-legal", "fa-gavel", "fa-dashboard", "fa-tachometer", "fa-comment-o", "fa-comments-o", "fa-flash", "fa-bolt", "fa-sitemap", "fa-umbrella", "fa-paste", "fa-clipboard", "fa-lightbulb-o", "fa-exchange", "fa-cloud-download", "fa-cloud-upload", "fa-user-md", "fa-stethoscope", "fa-suitcase", "fa-bell-o", "fa-coffee", "fa-cutlery", "fa-file-text-o", "fa-building-o", "fa-hospital-o", "fa-ambulance", "fa-medkit", "fa-fighter-jet", "fa-beer", "fa-h-square", "fa-plus-square", "fa-angle-double-left", "fa-angle-double-right", "fa-angle-double-up", "fa-angle-double-down", "fa-angle-left", "fa-angle-right", "fa-angle-up", "fa-angle-down", "fa-desktop", "fa-laptop", "fa-tablet", "fa-mobile-phone", "fa-mobile", "fa-circle-o", "fa-quote-left", "fa-quote-right", "fa-spinner", "fa-circle", "fa-mail-reply", "fa-reply", "fa-github-alt", "fa-folder-o", "fa-folder-open-o", "fa-smile-o", "fa-frown-o", "fa-meh-o", "fa-gamepad", "fa-keyboard-o", "fa-flag-o", "fa-flag-checkered", "fa-terminal", "fa-code", "fa-mail-reply-all", "fa-reply-all", "fa-star-half-empty", "fa-star-half-full", "fa-star-half-o", "fa-location-arrow", "fa-crop", "fa-code-fork", "fa-unlink", "fa-chain-broken", "fa-question", "fa-info", "fa-exclamation", "fa-superscript", "fa-subscript", "fa-eraser", "fa-puzzle-piece", "fa-microphone", "fa-microphone-slash", "fa-shield", "fa-calendar-o", "fa-fire-extinguisher", "fa-rocket", "fa-maxcdn", "fa-chevron-circle-left", "fa-chevron-circle-right", "fa-chevron-circle-up", "fa-chevron-circle-down", "fa-html5", "fa-css3", "fa-anchor", "fa-unlock-alt", "fa-bullseye", "fa-ellipsis-h", "fa-ellipsis-v", "fa-rss-square", "fa-play-circle", "fa-ticket", "fa-minus-square", "fa-minus-square-o", "fa-level-up", "fa-level-down", "fa-check-square", "fa-pencil-square", "fa-external-link-square", "fa-share-square", "fa-compass", "fa-toggle-down", "fa-caret-square-o-down", "fa-toggle-up", "fa-caret-square-o-up", "fa-toggle-right", "fa-caret-square-o-right", "fa-euro", "fa-eur", "fa-gbp", "fa-dollar", "fa-usd", "fa-rupee", "fa-inr", "fa-cny", "fa-rmb", "fa-yen", "fa-jpy", "fa-ruble", "fa-rouble", "fa-rub", "fa-won", "fa-krw", "fa-bitcoin", "fa-btc", "fa-file", "fa-file-text", "fa-sort-alpha-asc", "fa-sort-alpha-desc", "fa-sort-amount-asc", "fa-sort-amount-desc", "fa-sort-numeric-asc", "fa-sort-numeric-desc", "fa-thumbs-up", "fa-thumbs-down", "fa-youtube-square", "fa-youtube", "fa-xing", "fa-xing-square", "fa-youtube-play", "fa-dropbox", "fa-stack-overflow", "fa-instagram", "fa-flickr", "fa-adn", "fa-bitbucket", "fa-bitbucket-square", "fa-tumblr", "fa-tumblr-square", "fa-long-arrow-down", "fa-long-arrow-up", "fa-long-arrow-left", "fa-long-arrow-right", "fa-apple", "fa-windows", "fa-android", "fa-linux", "fa-dribbble", "fa-skype", "fa-foursquare", "fa-trello", "fa-female", "fa-male", "fa-gittip", "fa-gratipay", "fa-sun-o", "fa-moon-o", "fa-archive", "fa-bug", "fa-vk", "fa-weibo", "fa-renren", "fa-pagelines", "fa-stack-exchange", "fa-arrow-circle-o-right", "fa-arrow-circle-o-left", "fa-toggle-left", "fa-caret-square-o-left", "fa-dot-circle-o", "fa-wheelchair", "fa-vimeo-square", "fa-turkish-lira", "fa-try", "fa-plus-square-o", "fa-space-shuttle", "fa-slack", "fa-envelope-square", "fa-wordpress", "fa-openid", "fa-institution", "fa-bank", "fa-university", "fa-mortar-board", "fa-graduation-cap", "fa-yahoo", "fa-google", "fa-reddit", "fa-reddit-square", "fa-stumbleupon-circle", "fa-stumbleupon", "fa-delicious", "fa-digg", "fa-pied-piper-pp", "fa-pied-piper-alt", "fa-drupal", "fa-joomla", "fa-language", "fa-fax", "fa-building", "fa-child", "fa-paw", "fa-spoon", "fa-cube", "fa-cubes", "fa-behance", "fa-behance-square", "fa-steam", "fa-steam-square", "fa-recycle", "fa-automobile", "fa-car", "fa-cab", "fa-taxi", "fa-tree", "fa-spotify", "fa-deviantart", "fa-soundcloud", "fa-database", "fa-file-pdf-o", "fa-file-word-o", "fa-file-excel-o", "fa-file-powerpoint-o", "fa-file-photo-o", "fa-file-picture-o", "fa-file-image-o", "fa-file-zip-o", "fa-file-archive-o", "fa-file-sound-o", "fa-file-audio-o", "fa-file-movie-o", "fa-file-video-o", "fa-file-code-o", "fa-vine", "fa-codepen", "fa-jsfiddle", "fa-life-bouy", "fa-life-buoy", "fa-life-saver", "fa-support", "fa-life-ring", "fa-circle-o-notch", "fa-ra", "fa-resistance", "fa-rebel", "fa-ge", "fa-empire", "fa-git-square", "fa-git", "fa-y-combinator-square", "fa-yc-square", "fa-hacker-news", "fa-tencent-weibo", "fa-qq", "fa-wechat", "fa-weixin", "fa-send", "fa-paper-plane", "fa-send-o", "fa-paper-plane-o", "fa-history", "fa-circle-thin", "fa-header", "fa-paragraph", "fa-sliders", "fa-share-alt", "fa-share-alt-square", "fa-bomb", "fa-soccer-ball-o", "fa-futbol-o", "fa-tty", "fa-binoculars", "fa-plug", "fa-slideshare", "fa-twitch", "fa-yelp", "fa-newspaper-o", "fa-wifi", "fa-calculator", "fa-paypal", "fa-google-wallet", "fa-cc-visa", "fa-cc-mastercard", "fa-cc-discover", "fa-cc-amex", "fa-cc-paypal", "fa-cc-stripe", "fa-bell-slash", "fa-bell-slash-o", "fa-trash", "fa-copyright", "fa-at", "fa-eyedropper", "fa-paint-brush", "fa-birthday-cake", "fa-area-chart", "fa-pie-chart", "fa-line-chart", "fa-lastfm", "fa-lastfm-square", "fa-toggle-off", "fa-toggle-on", "fa-bicycle", "fa-bus", "fa-ioxhost", "fa-angellist", "fa-cc", "fa-shekel", "fa-sheqel", "fa-ils", "fa-meanpath", "fa-buysellads", "fa-connectdevelop", "fa-dashcube", "fa-forumbee", "fa-leanpub", "fa-sellsy", "fa-shirtsinbulk", "fa-simplybuilt", "fa-skyatlas", "fa-cart-plus", "fa-cart-arrow-down", "fa-diamond", "fa-ship", "fa-user-secret", "fa-motorcycle", "fa-street-view", "fa-heartbeat", "fa-venus", "fa-mars", "fa-mercury", "fa-intersex", "fa-transgender", "fa-transgender-alt", "fa-venus-double", "fa-mars-double", "fa-venus-mars", "fa-mars-stroke", "fa-mars-stroke-v", "fa-mars-stroke-h", "fa-neuter", "fa-genderless", "fa-facebook-official", "fa-pinterest-p", "fa-whatsapp", "fa-server", "fa-user-plus", "fa-user-times", "fa-hotel", "fa-bed", "fa-viacoin", "fa-train", "fa-subway", "fa-medium", "fa-yc", "fa-y-combinator", "fa-optin-monster", "fa-opencart", "fa-expeditedssl", "fa-battery-4", "fa-battery", "fa-battery-full", "fa-battery-3", "fa-battery-three-quarters", "fa-battery-2", "fa-battery-half", "fa-battery-1", "fa-battery-quarter", "fa-battery-0", "fa-battery-empty", "fa-mouse-pointer", "fa-i-cursor", "fa-object-group", "fa-object-ungroup", "fa-sticky-note", "fa-sticky-note-o", "fa-cc-jcb", "fa-cc-diners-club", "fa-clone", "fa-balance-scale", "fa-hourglass-o", "fa-hourglass-1", "fa-hourglass-start", "fa-hourglass-2", "fa-hourglass-half", "fa-hourglass-3", "fa-hourglass-end", "fa-hourglass", "fa-hand-grab-o", "fa-hand-rock-o", "fa-hand-stop-o", "fa-hand-paper-o", "fa-hand-scissors-o", "fa-hand-lizard-o", "fa-hand-spock-o", "fa-hand-pointer-o", "fa-hand-peace-o", "fa-trademark", "fa-registered", "fa-creative-commons", "fa-gg", "fa-gg-circle", "fa-tripadvisor", "fa-odnoklassniki", "fa-odnoklassniki-square", "fa-get-pocket", "fa-wikipedia-w", "fa-safari", "fa-chrome", "fa-firefox", "fa-opera", "fa-internet-explorer", "fa-tv", "fa-television", "fa-contao", "fa-500px", "fa-amazon", "fa-calendar-plus-o", "fa-calendar-minus-o", "fa-calendar-times-o", "fa-calendar-check-o", "fa-industry", "fa-map-pin", "fa-map-signs", "fa-map-o", "fa-map", "fa-commenting", "fa-commenting-o", "fa-houzz", "fa-vimeo", "fa-black-tie", "fa-fonticons", "fa-reddit-alien", "fa-edge", "fa-credit-card-alt", "fa-codiepie", "fa-modx", "fa-fort-awesome", "fa-usb", "fa-product-hunt", "fa-mixcloud", "fa-scribd", "fa-pause-circle", "fa-pause-circle-o", "fa-stop-circle", "fa-stop-circle-o", "fa-shopping-bag", "fa-shopping-basket", "fa-hashtag", "fa-bluetooth", "fa-bluetooth-b", "fa-percent", "fa-gitlab", "fa-wpbeginner", "fa-wpforms", "fa-envira", "fa-universal-access", "fa-wheelchair-alt", "fa-question-circle-o", "fa-blind", "fa-audio-description", "fa-volume-control-phone", "fa-braille", "fa-assistive-listening-systems", "fa-asl-interpreting", "fa-american-sign-language-interpreting", "fa-deafness", "fa-hard-of-hearing", "fa-deaf", "fa-glide", "fa-glide-g", "fa-signing", "fa-sign-language", "fa-low-vision", "fa-viadeo", "fa-viadeo-square", "fa-snapchat", "fa-snapchat-ghost", "fa-snapchat-square", "fa-pied-piper", "fa-first-order", "fa-yoast", "fa-themeisle", "fa-google-plus-circle", "fa-google-plus-official", "fa-fa", "fa-font-awesome", "fa-handshake-o", "fa-envelope-open", "fa-envelope-open-o", "fa-linode", "fa-address-book", "fa-address-book-o", "fa-vcard", "fa-address-card", "fa-vcard-o", "fa-address-card-o", "fa-user-circle", "fa-user-circle-o", "fa-user-o", "fa-id-badge", "fa-drivers-license", "fa-id-card", "fa-drivers-license-o", "fa-id-card-o", "fa-quora", "fa-free-code-camp", "fa-telegram", "fa-thermometer-4", "fa-thermometer", "fa-thermometer-full", "fa-thermometer-3", "fa-thermometer-three-quarters", "fa-thermometer-2", "fa-thermometer-half", "fa-thermometer-1", "fa-thermometer-quarter", "fa-thermometer-0", "fa-thermometer-empty", "fa-shower", "fa-bathtub", "fa-s15", "fa-bath", "fa-podcast", "fa-window-maximize", "fa-window-minimize", "fa-window-restore", "fa-times-rectangle", "fa-window-close", "fa-times-rectangle-o", "fa-window-close-o", "fa-bandcamp", "fa-grav", "fa-etsy", "fa-imdb", "fa-ravelry", "fa-eercast", "fa-microchip", "fa-snowflake-o", "fa-superpowers", "fa-wpexplorer", "fa-meetup"]; diff --git a/packages/toolbar/src/browser/main-toolbar-command-contribution.ts b/packages/toolbar/src/browser/main-toolbar-command-contribution.ts new file mode 100644 index 0000000000000..aacb9fce80c4c --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar-command-contribution.ts @@ -0,0 +1,215 @@ +/******************************************************************************** + * Copyright (C) 2022 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 { + bindContributionProvider, + CommandContribution, + CommandRegistry, + CommandService, + InMemoryResources, + MenuContribution, + MenuModelRegistry, +} from '@theia/core'; +import { + CommonMenus, + createPreferenceProxy, + KeybindingContribution, + KeybindingRegistry, + PreferenceContribution, + PreferenceScope, + PreferenceService, + QuickInputService, + Widget, +} from '@theia/core/lib/browser'; +import { injectable, inject, interfaces, Container } from '@theia/core/shared/inversify'; +import { EditorManager } from '@theia/editor/lib/browser'; +import { bindEasySearchToolbarWidget } from './easy-search-toolbar-item'; +import { MainToolbarImpl } from './main-toolbar'; +import { bindToolbarIconDialog } from './main-toolbar-icon-selector-dialog'; +import { + ReactTabBarToolbarContribution, + ToolbarItemPosition, + MainToolbarFactory, + MainToolbar, + LateInjector, + lateInjector, +} from './main-toolbar-interfaces'; +import { MainToolbarCommandQuickInputService } from './main-toolbar-command-quick-input-service'; +import { MainToolbarStorageProvider } from './main-toolbar-storage-provider'; +import { MainToolbarController } from './main-toolbar-controller'; +import { SearchInWorkspaceQuickInputService } from './search-in-workspace-root-quick-input-service'; +import { MainToolbarPreferencesSchema, MainToolbarPreferences, TOOLBAR_ENABLE_PREFERENCE_ID } from './main-toolbar-preference-contribution'; +import { MainToolbarDefaults, MainToolbarDefaultsFactory } from './main-toolbar-defaults'; +import { MainToolbarCommands, MainToolbarMenus, UserToolbarURI, USER_TOOLBAR_URI } from './main-toolbar-constants'; +import { JsonSchemaContribution, JsonSchemaRegisterContext } from '@theia/core/lib/browser/json-schema-store'; +import { toolbarConfigurationSchema, toolbarSchemaId } from './main-toolbar-preference-schema'; +import URI from '@theia/core/lib/common/uri'; + +@injectable() +export class MainToolbarContribution implements CommandContribution, KeybindingContribution, MenuContribution, JsonSchemaContribution { + @inject(MainToolbarController) protected readonly model: MainToolbarController; + @inject(QuickInputService) protected readonly quickInputService: QuickInputService; + @inject(MainToolbarCommandQuickInputService) protected toolbarCommandPickService: MainToolbarCommandQuickInputService; + @inject(CommandService) protected readonly commandService: CommandService; + @inject(EditorManager) protected readonly editorManager: EditorManager; + @inject(PreferenceService) protected readonly preferenceService: PreferenceService; + @inject(MainToolbarController) protected readonly toolbarModel: MainToolbarController; + @inject(InMemoryResources) protected readonly inMemoryResources: InMemoryResources; + protected readonly schemaURI = new URI(toolbarSchemaId); + + registerSchemas(context: JsonSchemaRegisterContext): void { + this.inMemoryResources.add(this.schemaURI, JSON.stringify(toolbarConfigurationSchema)); + context.registerSchema({ + fileMatch: ['toolbar.json'], + url: this.schemaURI.toString(), + }); + } + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(MainToolbarCommands.CUSTOMIZE_TOOLBAR, { + execute: () => this.model.openOrCreateJSONFile(true), + }); + registry.registerCommand(MainToolbarCommands.RESET_TOOLBAR, { + execute: () => this.model.clearAll(), + }); + registry.registerCommand(MainToolbarCommands.TOGGLE_MAIN_TOOLBAR, { + execute: () => { + const isVisible = this.preferenceService.get(TOOLBAR_ENABLE_PREFERENCE_ID); + this.preferenceService.set(TOOLBAR_ENABLE_PREFERENCE_ID, !isVisible, PreferenceScope.User); + }, + }); + + registry.registerCommand(MainToolbarCommands.REMOVE_COMMAND_FROM_TOOLBAR, { + execute: async (_widget, position: ToolbarItemPosition | undefined, id?: string) => position && this.model.removeItem(position, id), + isVisible: (...args) => this.isToolbarWidget(args[0]), + }); + registry.registerCommand(MainToolbarCommands.INSERT_GROUP_LEFT, { + execute: async (_widget: Widget, position: ToolbarItemPosition | undefined) => position && this.model.insertGroup(position, 'left'), + isVisible: (widget: Widget, position: ToolbarItemPosition | undefined) => { + if (position) { + const { alignment, groupIndex, itemIndex } = position; + const owningGroupLength = this.toolbarModel.toolbarItems.items[alignment][groupIndex].length; + return this.isToolbarWidget(widget) && (owningGroupLength > 1) && (itemIndex > 0); + } + return false; + }, + }); + registry.registerCommand(MainToolbarCommands.INSERT_GROUP_RIGHT, { + execute: async (_widget: Widget, position: ToolbarItemPosition | undefined) => position && this.model.insertGroup(position, 'right'), + isVisible: (widget: Widget, position: ToolbarItemPosition | undefined) => { + if (position) { + const { alignment, groupIndex, itemIndex } = position; + const owningGroupLength = this.toolbarModel.toolbarItems.items[alignment][groupIndex].length; + const isNotLastItem = itemIndex < (owningGroupLength - 1); + return this.isToolbarWidget(widget) && owningGroupLength > 1 && isNotLastItem; + } + return false; + }, + }); + registry.registerCommand(MainToolbarCommands.ADD_COMMAND_TO_TOOLBAR, { + execute: () => this.toolbarCommandPickService.openIconDialog(), + }); + } + + protected isToolbarWidget(arg: unknown): boolean { + return arg instanceof MainToolbarImpl; + } + + registerKeybindings(keys: KeybindingRegistry): void { + keys.registerKeybinding({ + command: MainToolbarCommands.TOGGLE_MAIN_TOOLBAR.id, + keybinding: 'alt+t', + }); + } + + registerMenus(registry: MenuModelRegistry): void { + registry.registerMenuAction(CommonMenus.VIEW_LAYOUT, { + commandId: MainToolbarCommands.TOGGLE_MAIN_TOOLBAR.id, + order: 'z', + }); + + registry.registerMenuAction(MainToolbarMenus.TOOLBAR_ITEM_CONTEXT_MENU, { + commandId: MainToolbarCommands.ADD_COMMAND_TO_TOOLBAR.id, + order: 'a', + }); + registry.registerMenuAction(MainToolbarMenus.TOOLBAR_ITEM_CONTEXT_MENU, { + commandId: MainToolbarCommands.INSERT_GROUP_LEFT.id, + order: 'b', + }); + registry.registerMenuAction(MainToolbarMenus.TOOLBAR_ITEM_CONTEXT_MENU, { + commandId: MainToolbarCommands.INSERT_GROUP_RIGHT.id, + order: 'c', + }); + registry.registerMenuAction(MainToolbarMenus.TOOLBAR_ITEM_CONTEXT_MENU, { + commandId: MainToolbarCommands.REMOVE_COMMAND_FROM_TOOLBAR.id, + order: 'd', + }); + + registry.registerMenuAction(MainToolbarMenus.MAIN_TOOLBAR_BACKGROUND_CONTEXT_MENU, { + commandId: MainToolbarCommands.ADD_COMMAND_TO_TOOLBAR.id, + order: 'a', + }); + registry.registerMenuAction(MainToolbarMenus.MAIN_TOOLBAR_BACKGROUND_CONTEXT_MENU, { + commandId: MainToolbarCommands.CUSTOMIZE_TOOLBAR.id, + order: 'b', + }); + registry.registerMenuAction(MainToolbarMenus.MAIN_TOOLBAR_BACKGROUND_CONTEXT_MENU, { + commandId: MainToolbarCommands.TOGGLE_MAIN_TOOLBAR.id, + order: 'c', + }); + registry.registerMenuAction(MainToolbarMenus.MAIN_TOOLBAR_BACKGROUND_CONTEXT_MENU, { + commandId: MainToolbarCommands.RESET_TOOLBAR.id, + order: 'd', + }); + } +} + +export function bindMainToolbar(bind: interfaces.Bind): void { + bind(MainToolbarFactory).toFactory(({ container }) => (): MainToolbar => { + const child = new Container({ defaultScope: 'Singleton' }); + child.parent = container; + child.bind(MainToolbar).to(MainToolbarImpl); + return child.get(MainToolbar); + }); + bind(MainToolbarContribution).toSelf().inSingletonScope(); + bind(CommandContribution).to(MainToolbarContribution); + bind(MenuContribution).toService(MainToolbarContribution); + bind(KeybindingContribution).toService(MainToolbarContribution); + bind(JsonSchemaContribution).toService(MainToolbarContribution); + + bind(MainToolbarCommandQuickInputService).toSelf().inSingletonScope(); + bind(SearchInWorkspaceQuickInputService).toSelf().inSingletonScope(); + + bindToolbarIconDialog(bind); + bind(MainToolbarDefaultsFactory).toConstantValue(MainToolbarDefaults); + bind(MainToolbarPreferences).toDynamicValue(({ container }) => { + const preferences = container.get(PreferenceService); + return createPreferenceProxy(preferences, MainToolbarPreferencesSchema); + }).inSingletonScope(); + bind(PreferenceContribution).toConstantValue({ + schema: MainToolbarPreferencesSchema, + }); + + bind(UserToolbarURI).toConstantValue(USER_TOOLBAR_URI); + + bind(MainToolbarController).toSelf().inSingletonScope(); + bind(MainToolbarStorageProvider).toSelf().inSingletonScope(); + bindContributionProvider(bind, ReactTabBarToolbarContribution); + bindEasySearchToolbarWidget(bind); + bind(LateInjector).toFactory( + (context: interfaces.Context) => (id: interfaces.ServiceIdentifier): T => lateInjector(context.container, id), + ); +} diff --git a/packages/toolbar/src/browser/main-toolbar-command-quick-input-service.ts b/packages/toolbar/src/browser/main-toolbar-command-quick-input-service.ts new file mode 100644 index 0000000000000..96622c11473e1 --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar-command-quick-input-service.ts @@ -0,0 +1,77 @@ +/******************************************************************************** + * Copyright (C) 2022 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 { Command, CommandRegistry, CommandService } from '@theia/core'; +import { QuickCommandService, QuickInputService, QuickPickItem } from '@theia/core/lib/browser'; +import { injectable, inject } from '@theia/core/shared/inversify'; +import { MainToolbarIconDialogFactory } from './main-toolbar-icon-selector-dialog'; +import { ToolbarAlignment, ToolbarAlignmentString } from './main-toolbar-interfaces'; +import { MainToolbarController } from './main-toolbar-controller'; + +@injectable() +export class MainToolbarCommandQuickInputService { + @inject(CommandService) protected readonly commandService: CommandService; + @inject(QuickInputService) protected readonly quickInputService: QuickInputService; + @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; + @inject(QuickCommandService) protected readonly quickCommandService: QuickCommandService; + @inject(MainToolbarController) protected readonly model: MainToolbarController; + @inject(MainToolbarIconDialogFactory) protected readonly iconDialogFactory: MainToolbarIconDialogFactory; + + protected quickPickItems: QuickPickItem[] = []; + + protected iconClass: string | undefined; + protected commandToAdd: Command | undefined; + + protected columnQuickPickItems: QuickPickItem[] = [ToolbarAlignment.LEFT, ToolbarAlignment.CENTER, ToolbarAlignment.RIGHT] + .map(column => ({ + label: `${column.toUpperCase()} Column`, + id: column, + })); + + openIconDialog(): void { + this.quickPickItems = this.generateCommandsList(); + this.quickInputService.showQuickPick(this.quickPickItems, { + placeholder: 'Find a command to add to the toolbar', + }); + } + + protected openColumnQP(): Promise { + return this.quickInputService.showQuickPick(this.columnQuickPickItems, { + placeholder: 'Where would you like the command added?', + }); + } + + protected generateCommandsList(): QuickPickItem[] { + const { recent, other } = this.quickCommandService['getCommands'](); + return [...recent, ...other].map(command => { + const formattedItem = this.quickCommandService.toItem(command) as QuickPickItem; + return { + ...formattedItem, + alwaysShow: true, + execute: async (): Promise => { + const iconDialog = this.iconDialogFactory(command); + const iconClass = await iconDialog.open(); + if (iconClass) { + const { id } = await this.openColumnQP(); + if (ToolbarAlignmentString.is(id)) { + this.model.addItem({ ...command, iconClass }, id); + } + } + }, + }; + }); + } +} diff --git a/packages/toolbar/src/browser/main-toolbar-constants.ts b/packages/toolbar/src/browser/main-toolbar-constants.ts new file mode 100644 index 0000000000000..137ff51f6a6a5 --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar-constants.ts @@ -0,0 +1,73 @@ +/******************************************************************************** + * Copyright (C) 2022 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 { MenuPath } from '@theia/core'; +import URI from '@theia/core/lib/common/uri'; +import { UserStorageUri } from '@theia/userstorage/lib/browser'; + +export namespace MainToolbarCommands { + export const TOGGLE_MAIN_TOOLBAR = { + id: 'main.toolbar.view.toggle', + category: 'View', + label: 'Toggle Main Toolbar', + }; + export const REMOVE_COMMAND_FROM_TOOLBAR = { + id: 'main.toolbar.remove.command', + category: 'Edit', + label: 'Remove Command From Toolbar', + }; + export const INSERT_GROUP_LEFT = { + id: 'main.toolbar.insert.group.left', + category: 'Edit', + label: 'Insert Group Separator (Left)', + }; + export const INSERT_GROUP_RIGHT = { + id: 'main.toolbar.insert.group.right', + category: 'Edit', + label: 'Insert Group Separator (Right)', + }; + export const ADD_COMMAND_TO_TOOLBAR = { + id: 'main.toolbar.add.command', + category: 'Edit', + label: 'Add Command to Toolbar', + }; + export const RESET_TOOLBAR = { + id: 'main.toolbar.restore.defaults', + category: 'Toolbar', + label: 'Restore Toolbar Defaults', + }; + export const CUSTOMIZE_TOOLBAR = { + id: 'main.toolbar.customize.toolbar', + category: 'Toolbar', + label: 'Customize Toolbar (Open JSON)', + }; +} + +export const UserToolbarURI = Symbol('UserToolbarURI'); +export const USER_TOOLBAR_URI = new URI().withScheme(UserStorageUri.scheme).withPath('/user/toolbar.json'); +export namespace MainToolbarMenus { + export const TOOLBAR_ITEM_CONTEXT_MENU: MenuPath = ['mainToolbar:toolbarItemContextMenu']; + export const MAIN_TOOLBAR_BACKGROUND_CONTEXT_MENU: MenuPath = ['mainToolbar:backgroundContextMenu']; + export const SEARCH_WIDGET_DROPDOWN_MENU: MenuPath = ['searchToolbar:dropdown']; +} + +export type ReactInteraction = React.MouseEvent | React.KeyboardEvent; +export namespace ReactKeyboardEvent { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export function is(obj: any): obj is React.KeyboardEvent { + return typeof obj === 'object' && 'key' in obj; + } +} diff --git a/packages/toolbar/src/browser/main-toolbar-controller.ts b/packages/toolbar/src/browser/main-toolbar-controller.ts new file mode 100644 index 0000000000000..e4856731ad108 --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar-controller.ts @@ -0,0 +1,230 @@ +/******************************************************************************** + * Copyright (C) 2022 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 { Command, ContributionProvider, deepClone, Emitter, MaybePromise, MessageService, Prioritizeable } from '@theia/core'; +import { Widget } from '@theia/core/lib/browser'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { injectable, inject, postConstruct, named } from '@theia/core/shared/inversify'; +import { MainToolbarDefaultsFactory } from './main-toolbar-defaults'; +import { + DeflatedMainToolbarTreeSchema, + ReactTabBarToolbarContribution, + MainToolbarTreeSchema, + ValidMainToolbarItem, + ToolbarAlignment, + ToolbarItemPosition, +} from './main-toolbar-interfaces'; +import { MainToolbarStorageProvider, TOOLBAR_BAD_JSON_ERROR_MESSAGE } from './main-toolbar-storage-provider'; + +@injectable() +export class MainToolbarController { + @inject(MainToolbarStorageProvider) protected readonly storageProvider: MainToolbarStorageProvider; + @inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService; + @inject(MessageService) protected readonly messageService: MessageService; + @inject(MainToolbarDefaultsFactory) protected readonly defaultsFactory: () => MainToolbarTreeSchema; + @inject(ContributionProvider) @named(ReactTabBarToolbarContribution) + protected widgetContributions: ContributionProvider; + + protected toolbarModelDidUpdateEmitter = new Emitter(); + readonly onToolbarModelDidUpdate = this.toolbarModelDidUpdateEmitter.event; + + protected toolbarProviderBusyEmitter = new Emitter(); + readonly onToolbarDidChangeBusyState = this.toolbarProviderBusyEmitter.event; + + readonly ready = new Deferred(); + + protected _toolbarItems: MainToolbarTreeSchema; + get toolbarItems(): MainToolbarTreeSchema { + return this._toolbarItems; + } + + set toolbarItems(newTree: MainToolbarTreeSchema) { + this._toolbarItems = newTree; + this.toolbarModelDidUpdateEmitter.fire(); + } + + protected inflateItems(schema: DeflatedMainToolbarTreeSchema): MainToolbarTreeSchema { + const newTree: MainToolbarTreeSchema = { + items: { + [ToolbarAlignment.LEFT]: [], + [ToolbarAlignment.CENTER]: [], + [ToolbarAlignment.RIGHT]: [], + }, + }; + for (const column of Object.keys(schema.items)) { + const currentColumn = schema.items[column as ToolbarAlignment]; + for (const group of currentColumn) { + const newGroup: ValidMainToolbarItem[] = []; + for (const item of group) { + if (item.group === 'contributed') { + const contribution = this.getContributionByID(item.id); + if (contribution) { + newGroup.push(contribution); + } + } else if (TabBarToolbarItem.is(item)) { + newGroup.push({ ...item }); + } + } + if (newGroup.length) { + newTree.items[column as ToolbarAlignment].push(newGroup); + } + } + } + return newTree; + } + + getContributionByID(id: string): ReactTabBarToolbarContribution | undefined { + return this.widgetContributions.getContributions().find(contribution => contribution.id === id); + } + + @postConstruct() + async init(): Promise { + await this.appState.reachedState('ready'); + await this.storageProvider.ready; + this.toolbarItems = await this.resolveToolbarItems(); + this.storageProvider.onToolbarItemsChanged(async () => { + this.toolbarItems = await this.resolveToolbarItems(); + }); + this.ready.resolve(); + this.widgetContributions.getContributions().forEach(contribution => { + if (contribution.onDidChange) { + contribution.onDidChange(() => this.toolbarModelDidUpdateEmitter.fire()); + } + }); + } + + protected async resolveToolbarItems(): Promise { + await this.storageProvider.ready; + if (this.storageProvider.toolbarItems) { + try { + return this.inflateItems(this.storageProvider.toolbarItems); + } catch (e) { + this.messageService.error(TOOLBAR_BAD_JSON_ERROR_MESSAGE); + } + } + return this.getDefaultItemsAndContributions(); + } + + protected async getDefaultItemsAndContributions(): Promise { + let defaultToolbarTree = this.defaultsFactory(); + const toolbarContributions = this.widgetContributions.getContributions(); + const prioritizedItems = await this.sortAndPrioritizeContributions(toolbarContributions); + defaultToolbarTree = this.groupAndAppendContributions(defaultToolbarTree, prioritizedItems); + return defaultToolbarTree; + } + + protected async sortAndPrioritizeContributions( + contributions: ReactTabBarToolbarContribution[], + ): Promise> { + const prioritizedContributionsPromise: Array> = []; + [ToolbarAlignment.LEFT, ToolbarAlignment.CENTER, ToolbarAlignment.RIGHT].forEach(column => { + prioritizedContributionsPromise.push(this.prioritizeContributionsPerColumn(contributions, column)); + }); + return Promise.all(prioritizedContributionsPromise); + } + + protected async prioritizeContributionsPerColumn( + contributions: ReactTabBarToolbarContribution[], + column: ToolbarAlignment, + ): Promise<[ToolbarAlignment, ReactTabBarToolbarContribution[]]> { + const filteredContributions = contributions.filter(contribution => contribution.column === column); + const prioritized = (await Prioritizeable.prioritizeAll(filteredContributions, contribution => contribution.priority)) + .map(p => p.value); + return [column, prioritized]; + } + + protected groupAndAppendContributions( + toolbarDefaults: MainToolbarTreeSchema, + prioritizedItems: Array<[ToolbarAlignment, ReactTabBarToolbarContribution[]]>, + ): MainToolbarTreeSchema { + const toolbarDefaultsCopy = deepClone(toolbarDefaults); + prioritizedItems.forEach(([column, prioritizedContributions]) => { + const currentColumn = toolbarDefaultsCopy.items[column]; + const indexOfLastGroupInColumn = toolbarDefaultsCopy.items[column].length - 1; + let currentGroup = currentColumn[indexOfLastGroupInColumn]; + prioritizedContributions.forEach(contribution => { + if (!contribution.newGroup) { + currentGroup.push(contribution); + } else { + currentGroup = [contribution]; + currentColumn.push(currentGroup); + } + }); + }); + return toolbarDefaultsCopy; + } + + async swapValues( + oldPosition: ToolbarItemPosition, + newPosition: ToolbarItemPosition, + direction: 'location-left' | 'location-right', + ): Promise { + await this.openOrCreateJSONFile(false); + this.toolbarProviderBusyEmitter.fire(true); + const success = this.storageProvider.swapValues(oldPosition, newPosition, direction); + this.toolbarProviderBusyEmitter.fire(false); + return success; + } + + async clearAll(): Promise { + return this.withBusy(async () => this.storageProvider.clearAll()); + } + + async openOrCreateJSONFile(doOpen = false): Promise { + return this.storageProvider.openOrCreateJSONFile(this.toolbarItems, doOpen); + } + + async addItem(command: Command, area: ToolbarAlignment): Promise { + return this.withBusy(async () => { + await this.openOrCreateJSONFile(false); + return this.storageProvider.addItem(command, area); + }); + } + + async removeItem(position: ToolbarItemPosition, id?: string): Promise { + return this.withBusy(async () => { + await this.openOrCreateJSONFile(false); + return this.storageProvider.removeItem(position); + }); + } + + async moveItemToEmptySpace( + draggedItemPosition: ToolbarItemPosition, + column: ToolbarAlignment, + centerPosition?: 'left' | 'right', + ): Promise { + return this.withBusy(async () => { + await this.openOrCreateJSONFile(false); + return this.storageProvider.moveItemToEmptySpace(draggedItemPosition, column, centerPosition); + }); + } + + async insertGroup(position: ToolbarItemPosition, insertDirection: 'left' | 'right'): Promise { + return this.withBusy(async () => { + await this.openOrCreateJSONFile(false); + return this.storageProvider.insertGroup(position, insertDirection); + }); + } + + async withBusy(action: () => MaybePromise): Promise { + this.toolbarProviderBusyEmitter.fire(true); + const toReturn = await action(); + this.toolbarProviderBusyEmitter.fire(false); + return toReturn; + } +} diff --git a/packages/toolbar/src/browser/main-toolbar-defaults.ts b/packages/toolbar/src/browser/main-toolbar-defaults.ts new file mode 100644 index 0000000000000..b738393f13f91 --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar-defaults.ts @@ -0,0 +1,55 @@ +/******************************************************************************** + * Copyright (C) 2022 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 { MainToolbarTreeSchema, ToolbarAlignment } from './main-toolbar-interfaces'; + +/* eslint-disable max-lines-per-function */ +export const MainToolbarDefaultsFactory = Symbol('MainToolbarDefaultsFactory'); +export const MainToolbarDefaults: () => MainToolbarTreeSchema = () => ({ + items: { + [ToolbarAlignment.LEFT]: [ + [ + { + id: 'textEditor.commands.go.back', + command: 'textEditor.commands.go.back', + icon: 'codicon codicon-arrow-left', + }, + { + id: 'textEditor.commands.go.forward', + command: 'textEditor.commands.go.forward', + icon: 'codicon codicon-arrow-right', + }, + ], + [ + { + id: 'workbench.action.splitEditorRight', + command: 'workbench.action.splitEditor', + icon: 'codicon codicon-split-horizontal', + }, + ], + ], + [ToolbarAlignment.CENTER]: [ + [ + { + id: 'terminal:new', + command: 'terminal:new', + icon: 'codicon codicon-terminal', + }, + ], + ], + [ToolbarAlignment.RIGHT]: [], + }, +}); diff --git a/packages/toolbar/src/browser/main-toolbar-frontend-module.ts b/packages/toolbar/src/browser/main-toolbar-frontend-module.ts new file mode 100644 index 0000000000000..1a8f6bc5dbde4 --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar-frontend-module.ts @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (C) 2022 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 '../../src/browser/style/toolbar-shell-style.css'; +import '../../src/browser/style/easy-search-style.css'; +import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; +import { bindToolbarApplicationShell } from './application-shell-with-toolbar-override'; +import { bindMainToolbar } from './main-toolbar-command-contribution'; + +export default new ContainerModule(( + bind: interfaces.Bind, + unbind: interfaces.Unbind, + _isBound: interfaces.IsBound, + rebind: interfaces.Rebind, +) => { + bindToolbarApplicationShell(bind, rebind, unbind); + bindMainToolbar(bind); +}); diff --git a/packages/toolbar/src/browser/main-toolbar-icon-selector-dialog.tsx b/packages/toolbar/src/browser/main-toolbar-icon-selector-dialog.tsx new file mode 100644 index 0000000000000..60cb1d75f550d --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar-icon-selector-dialog.tsx @@ -0,0 +1,300 @@ +/******************************************************************************** + * Copyright (C) 2022 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 '@theia/core/shared/react'; +import * as ReactDOM from '@theia/core/shared/react-dom'; +import { injectable, interfaces, inject, postConstruct } from '@theia/core/shared/inversify'; +import debounce = require('@theia/core/shared/lodash.debounce'); +import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog'; +import { DEFAULT_SCROLL_OPTIONS, DialogProps, Message } from '@theia/core/lib/browser'; +import { Command, Disposable } from '@theia/core'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import PerfectScrollbar from 'perfect-scrollbar'; +import { FuzzySearch } from '@theia/core/lib/browser/tree/fuzzy-search'; +import { codicons } from './codicons'; +import { fontAwesomeIcons } from './font-awesome-icons'; +import { IconSet } from './main-toolbar-interfaces'; +import { ReactInteraction, ReactKeyboardEvent } from './main-toolbar-constants'; + +export interface MainToolbarIconDialogFactory { + (command: Command): MainToolbarIconSelectorDialog; +} + +export const MainToolbarIconDialogFactory = Symbol('MainToolbarIconDialogFactory'); +export const ToolbarCommand = Symbol('ToolbarCommand'); +export const FontAwesomeIcons = Symbol('FontAwesomeIcons'); +export const CodiconIcons = Symbol('CodiconIcons'); + +const FIFTY_MS = 50; +@injectable() +export class MainToolbarIconSelectorDialog extends ReactDialog { + @inject(ToolbarCommand) protected readonly toolbarCommand: Command; + @inject(FileService) protected readonly fileService: FileService; + @inject(FontAwesomeIcons) protected readonly faIcons: string[]; + @inject(CodiconIcons) protected readonly codiconIcons: string[]; + @inject(FuzzySearch) protected readonly fuzzySearch: FuzzySearch; + + static ID = 'main-toolbar-icon-selector-dialog'; + protected deferredScrollContainer = new Deferred(); + scrollOptions: PerfectScrollbar.Options = { ...DEFAULT_SCROLL_OPTIONS }; + protected filterRef: HTMLInputElement; + + protected selectedIcon: string | undefined; + protected activeIconPrefix: IconSet = IconSet.CODICON; + protected iconSets = new Map(); + protected filteredIcons: string[] = []; + protected doShowFilterPlaceholder = false; + protected debounceHandleSearch = debounce(this.doHandleSearch.bind(this), FIFTY_MS, { trailing: true }); + + constructor( + @inject(DialogProps) protected readonly props: DialogProps, + ) { + super(props); + this.toDispose.push(Disposable.create(() => { + ReactDOM.unmountComponentAtNode(this.controlPanel); + })); + } + + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + ReactDOM.render(this.renderControls(), this.controlPanel); + } + + @postConstruct() + protected init(): void { + this.node.id = MainToolbarIconSelectorDialog.ID; + this.iconSets.set(IconSet.FA, this.faIcons); + this.iconSets.set(IconSet.CODICON, this.codiconIcons); + this.activeIconPrefix = IconSet.CODICON; + const initialIcons = this.iconSets.get(this.activeIconPrefix); + if (initialIcons) { + this.filteredIcons = initialIcons; + } + } + + async getScrollContainer(): Promise { + return this.deferredScrollContainer.promise; + } + + protected assignScrollContainerRef = (element: HTMLDivElement): void => this.doAssignScrollContainerRef(element); + protected doAssignScrollContainerRef(element: HTMLDivElement): void { + this.deferredScrollContainer.resolve(element); + } + + protected assignFilterRef = (element: HTMLInputElement): void => this.doAssignFilterRef(element); + protected doAssignFilterRef(element: HTMLInputElement): void { + this.filterRef = element; + } + + get value(): string | undefined { + return this.selectedIcon; + } + + protected handleSelectOnChange = async (e: React.ChangeEvent): Promise => this.doHandleSelectOnChange(e); + protected async doHandleSelectOnChange(e: React.ChangeEvent): Promise { + const { value } = e.target; + this.activeIconPrefix = value as IconSet; + this.filteredIcons = []; + await this.doHandleSearch(); + this.update(); + } + + protected renderIconSelectorOptions(): React.ReactNode { + return ( +
+
+ Icon Set: + {' '} + +
+
+ +
+
+ ); + } + + protected renderIconGrid(): React.ReactNode { + return ( +
+
+ {!this.doShowFilterPlaceholder ? this.filteredIcons?.map(icon => ( +
+
+
+ )) + :
The search returned no results
} +
+
+ ); + } + + protected render(): React.ReactNode { + return ( + <> + {this.renderIconSelectorOptions()} + {this.renderIconGrid()} + + ); + } + + protected async doHandleSearch(): Promise { + const query = this.filterRef.value; + const pattern = query; + const items = this.iconSets.get(this.activeIconPrefix); + if (items) { + if (pattern.length) { + const transform = (item: string): string => item; + const filterResults = await this.fuzzySearch.filter({ pattern, items, transform }); + this.filteredIcons = filterResults.map(result => result.item); + if (!this.filteredIcons.length) { + this.doShowFilterPlaceholder = true; + } else { + this.doShowFilterPlaceholder = false; + } + } else { + this.doShowFilterPlaceholder = false; + this.filteredIcons = items; + } + this.update(); + } + } + + protected handleOnIconClick = (e: ReactInteraction): void => this.doHandleOnIconClick(e); + protected doHandleOnIconClick(e: ReactInteraction): void { + e.currentTarget.classList.add('selected'); + if (ReactKeyboardEvent.is(e) && e.key !== 'Enter') { + return; + } + const iconId = e.currentTarget.getAttribute('data-id'); + if (iconId) { + this.selectedIcon = iconId; + this.update(); + } + } + + protected handleOnIconBlur = (e: React.FocusEvent): void => this.doHandleOnIconBlur(e); + protected doHandleOnIconBlur(e: React.FocusEvent): void { + e.currentTarget.classList.remove('selected'); + } + + protected doAccept = (e: ReactInteraction): void => { + const dataId = e.currentTarget.getAttribute('data-id'); + if (dataId === 'default-accept') { + this.selectedIcon = this.toolbarCommand.iconClass; + } + this.accept(); + }; + + protected doClose = (): void => { + this.selectedIcon = undefined; + this.close(); + }; + + protected renderControls(): React.ReactElement { + return ( +
+
+ {this.toolbarCommand.iconClass + && ( + + )} +
+
+ + + +
+
+ ); + } + + dispose(): void { + super.dispose(); + } +} + +export const ICON_DIALOG_WIDTH = 600; +export const ICON_DIALOG_PADDING = 24; + +export const bindToolbarIconDialog = (bind: interfaces.Bind): void => { + bind(MainToolbarIconDialogFactory).toFactory(ctx => (command: Command): MainToolbarIconSelectorDialog => { + const child = ctx.container.createChild(); + child.bind(DialogProps).toConstantValue({ + title: `Select an Icon for "${command.label}"` ?? 'Select an Icon', + maxWidth: ICON_DIALOG_WIDTH + ICON_DIALOG_PADDING, + }); + child.bind(FontAwesomeIcons).toConstantValue(fontAwesomeIcons); + child.bind(CodiconIcons).toConstantValue(codicons); + child.bind(ToolbarCommand).toConstantValue(command); + child.bind(FuzzySearch).toSelf().inSingletonScope(); + child.bind(MainToolbarIconSelectorDialog).toSelf().inSingletonScope(); + return child.get(MainToolbarIconSelectorDialog); + }); +}; diff --git a/packages/toolbar/src/browser/main-toolbar-interfaces.ts b/packages/toolbar/src/browser/main-toolbar-interfaces.ts new file mode 100644 index 0000000000000..f609f263f737a --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar-interfaces.ts @@ -0,0 +1,90 @@ +/******************************************************************************** + * Copyright (C) 2022 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 '@theia/core/shared/inversify'; +import { DockPanelRenderer, DockPanelRendererFactory } from '@theia/core/lib/browser'; +import { ReactTabBarToolbarItem, TabBarToolbar, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; + +export enum ToolbarAlignment { + LEFT = 'left', + CENTER = 'center', + RIGHT = 'right' +} + +export interface MainToolbarTreeSchema { + items: { + [key in ToolbarAlignment]: ValidMainToolbarItem[][]; + }; +} + +export interface DeflatedMainToolbarTreeSchema { + items: { + [key in ToolbarAlignment]: ValidMainToolbarItemDeflated[][]; + }; +} +export namespace ToolbarAlignmentString { + export const is = (obj: unknown): obj is ToolbarAlignment => obj === ToolbarAlignment.LEFT + || obj === ToolbarAlignment.CENTER + || obj === ToolbarAlignment.RIGHT; +} +export namespace DeflatedMainToolbarTreeSchema { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export const is = (obj: any): obj is DeflatedMainToolbarTreeSchema => !!obj && 'items' in obj + && 'left' in obj.items + && 'center' in obj.items + && 'right' in obj.items; +} + +export interface MainToolbarContributionProperties { + column: ToolbarAlignment; + priority: number; + newGroup: boolean; + toJSON(): { id: string; group: string }; +} + +export type ReactTabBarToolbarContribution = ReactTabBarToolbarItem & MainToolbarContributionProperties; + +export const ReactTabBarToolbarContribution = Symbol('ReactTabBarToolbarContribution'); + +export const MainToolbar = Symbol('MainToolbar'); +export const MainToolbarFactory = Symbol('MainToolbarFactory'); +export type MainToolbar = TabBarToolbar; +export interface DockPanelRendererFactoryWithToolbar extends DockPanelRendererFactory { + (): DockPanelRenderer; + (toolbar: boolean): MainToolbar; +} + +export type ValidMainToolbarItem = ReactTabBarToolbarContribution | TabBarToolbarItem; +export type ValidMainToolbarItemDeflated = { id: string; group: 'contributed' } | TabBarToolbarItem; + +export const LateInjector = Symbol('LateInjector'); + +export const lateInjector = ( + context: interfaces.Container, + serviceIdentifier: interfaces.ServiceIdentifier, +): T => context.get(serviceIdentifier); + +export interface ToolbarItemPosition { + alignment: ToolbarAlignment; + groupIndex: number; + itemIndex: number; +} + +export enum IconSet { + FA = 'fa', + CODICON = 'codicon' +} + diff --git a/packages/toolbar/src/browser/main-toolbar-preference-contribution.ts b/packages/toolbar/src/browser/main-toolbar-preference-contribution.ts new file mode 100644 index 0000000000000..eced638d6c323 --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar-preference-contribution.ts @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (C) 2022 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 { PreferenceSchema, PreferenceProxy, PreferenceScope } from '@theia/core/lib/browser'; + +export const TOOLBAR_ENABLE_PREFERENCE_ID = 'mainToolbar.showToolbar'; + +export const MainToolbarPreferencesSchema: PreferenceSchema = { + type: 'object', + properties: { + [TOOLBAR_ENABLE_PREFERENCE_ID]: { + 'type': 'boolean', + 'description': 'Show main toolbar', + 'default': false, + 'scope': PreferenceScope.Workspace, + }, + }, +}; + +class MainToolbarPreferencesContribution { + [TOOLBAR_ENABLE_PREFERENCE_ID]: boolean; +} + +export const MainToolbarPreferences = Symbol('MainToolbarPreferences'); +export type MainToolbarPreferences = PreferenceProxy; diff --git a/packages/toolbar/src/browser/main-toolbar-preference-schema.ts b/packages/toolbar/src/browser/main-toolbar-preference-schema.ts new file mode 100644 index 0000000000000..fc37f31f20029 --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar-preference-schema.ts @@ -0,0 +1,73 @@ +/******************************************************************************** + * Copyright (C) 2022 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 { IJSONSchema } from '@theia/core/lib/common/json-schema'; +import * as Ajv from '@theia/core/shared/ajv'; + +const toolbarColumnGroup: IJSONSchema = { + 'type': 'array', + 'description': 'Array of subgroups for right toolbar column', + 'items': { + 'type': 'array', + 'description': 'Grouping', + 'items': { + 'type': 'object', + 'properties': { + 'id': { 'type': 'string' }, + 'command': { 'type': 'string' }, + 'icon': { 'type': 'string' }, + 'tooltip': { 'type': 'string' }, + 'group': { 'enum': ['contributed'] } + }, + 'required': [ + 'id', + ], + 'additionalProperties': false, + } + } +}; + +export const toolbarSchemaId = 'vscode://schemas/toolbar'; +export const toolbarConfigurationSchema: IJSONSchema = { + // '$schema': 'https://json-schema.org/draft/2019-09/schema', + '$id': 'vscode://schemas/indexing-grid', + 'type': 'object', + 'title': 'Toolbar', + 'properties': { + 'items': { + 'type': 'object', + 'properties': { + 'left': toolbarColumnGroup, + 'center': toolbarColumnGroup, + 'right': toolbarColumnGroup, + }, + 'required': [ + 'left', + 'center', + 'right' + ], + 'additionalProperties': false, + } + }, + 'required': [ + 'items' + ] +}; + +const validator = new Ajv().compile(toolbarConfigurationSchema); +export function isToolbarPreferences(candidate: unknown): boolean { + return Boolean(validator(candidate)); +} diff --git a/packages/toolbar/src/browser/main-toolbar-storage-provider.ts b/packages/toolbar/src/browser/main-toolbar-storage-provider.ts new file mode 100644 index 0000000000000..ac425da568844 --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar-storage-provider.ts @@ -0,0 +1,348 @@ +/******************************************************************************** + * Copyright (C) 2022 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 jsoncParser from 'jsonc-parser'; +import { Command, deepClone, Disposable, DisposableCollection, Emitter, MessageService } from '@theia/core'; +import { injectable, postConstruct, inject, interfaces } from '@theia/core/shared/inversify'; +import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; +import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { EditorManager } from '@theia/editor/lib/browser'; +import { Widget } from '@theia/core/lib/browser'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import URI from '@theia/core/lib/common/uri'; +import { + DeflatedMainToolbarTreeSchema, + MainToolbarTreeSchema, + ValidMainToolbarItem, + ValidMainToolbarItemDeflated, + ToolbarAlignment, + ToolbarItemPosition, + LateInjector, +} from './main-toolbar-interfaces'; +import { UserToolbarURI } from './main-toolbar-constants'; + +export const TOOLBAR_BAD_JSON_ERROR_MESSAGE = 'There was an error reading your toolbar.json file. Please check if it is corrupt' + + ' by right-clicking the toolbar and selecting "Customize Toolbar". You can also reset it to its defaults by selecting' + + ' "Restore Toolbar Defaults"'; +@injectable() +export class MainToolbarStorageProvider implements Disposable { + @inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService; + @inject(MonacoTextModelService) protected readonly textModelService: MonacoTextModelService; + @inject(FileService) protected readonly fileService: FileService; + @inject(MessageService) protected readonly messageService: MessageService; + @inject(LateInjector) + protected lateInjector: (id: interfaces.ServiceIdentifier) => T; + + @inject(UserToolbarURI) protected readonly USER_TOOLBAR_URI: URI; + + get ready(): Promise { + return this._ready.promise; + } + + protected readonly _ready = new Deferred(); + + // Injecting this directly causes a circular dependency, so we're using a custom utility + // to inject this after the application has started up + protected monacoWorkspace: MonacoWorkspace; + protected editorManager: EditorManager; + protected model: MonacoEditorModel | undefined; + protected toDispose = new DisposableCollection(); + protected toolbarItemsUpdatedEmitter = new Emitter(); + readonly onToolbarItemsChanged = this.toolbarItemsUpdatedEmitter.event; + toolbarItems: DeflatedMainToolbarTreeSchema | undefined; + + @postConstruct() + async init(): Promise { + const reference = await this.textModelService.createModelReference(this.USER_TOOLBAR_URI); + this.model = reference.object; + this.toDispose.push(reference); + this.toDispose.push(Disposable.create(() => this.model = undefined)); + this.readConfiguration(); + if (this.model) { + this.toDispose.push(this.model.onDidChangeContent(() => this.readConfiguration())); + this.toDispose.push(this.model.onDirtyChanged(() => this.readConfiguration())); + this.toDispose.push(this.model.onDidChangeValid(() => this.readConfiguration())); + } + this.toDispose.push(this.toolbarItemsUpdatedEmitter); + await this.appState.reachedState('ready'); + this.monacoWorkspace = this.lateInjector(MonacoWorkspace); + this.editorManager = this.lateInjector(EditorManager); + this._ready.resolve(); + } + + protected readConfiguration(): void { + if (!this.model || this.model.dirty) { + return; + } + try { + if (this.model.valid) { + const content = this.model.getText(); + this.toolbarItems = this.parseContent(content); + } else { + this.toolbarItems = undefined; + } + this.toolbarItemsUpdatedEmitter.fire(); + } catch (e) { + console.error(`Failed to load toolbar config from '${this.USER_TOOLBAR_URI}'.`, e); + } + } + + async removeItem(position: ToolbarItemPosition): Promise { + if (this.toolbarItems) { + const { alignment, groupIndex, itemIndex } = position; + const modifiedConfiguration = deepClone(this.toolbarItems); + modifiedConfiguration.items[alignment][groupIndex].splice(itemIndex, 1); + const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration); + return this.writeToFile([], sanitizedConfiguration); + } + return false; + } + + async addItem(command: Command, alignment: ToolbarAlignment): Promise { + if (this.toolbarItems) { + const itemFromCommand: ValidMainToolbarItem = { + id: command.id, + command: command.id, + icon: command.iconClass, + }; + const groupIndex = this.toolbarItems?.items[alignment].length; + if (groupIndex) { + const lastItemIndex = this.toolbarItems?.items[alignment][groupIndex - 1].length; + const modifiedConfiguration = deepClone(this.toolbarItems); + modifiedConfiguration.items[alignment][groupIndex - 1].push(itemFromCommand); + return !!lastItemIndex && this.writeToFile([], modifiedConfiguration); + } + return this.addItemToEmptyColumn(itemFromCommand, alignment); + } + return false; + } + + async swapValues( + oldPosition: ToolbarItemPosition, + newPosition: ToolbarItemPosition, + direction: 'location-left' | 'location-right', + ): Promise { + if (this.toolbarItems) { + const { alignment, groupIndex, itemIndex } = oldPosition; + const draggedItem = this.toolbarItems?.items[alignment][groupIndex][itemIndex]; + const newItemIndex = direction === 'location-right' ? newPosition.itemIndex + 1 : newPosition.itemIndex; + const modifiedConfiguration = deepClone(this.toolbarItems); + if (newPosition.alignment === oldPosition.alignment && newPosition.groupIndex === oldPosition.groupIndex) { + modifiedConfiguration.items[newPosition.alignment][newPosition.groupIndex].splice(newItemIndex, 0, draggedItem); + if (newPosition.itemIndex > oldPosition.itemIndex) { + modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex, 1); + } else { + modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex + 1, 1); + } + } else { + modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex, 1); + modifiedConfiguration.items[newPosition.alignment][newPosition.groupIndex].splice(newItemIndex, 0, draggedItem); + } + const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration); + return this.writeToFile([], sanitizedConfiguration); + } + return false; + } + + async addItemToEmptyColumn(item: ValidMainToolbarItemDeflated, alignment: ToolbarAlignment): Promise { + if (this.toolbarItems) { + const modifiedConfiguration = deepClone(this.toolbarItems); + modifiedConfiguration.items[alignment].push([item]); + return this.writeToFile([], modifiedConfiguration); + } + return false; + } + + async moveItemToEmptySpace( + oldPosition: ToolbarItemPosition, + newAlignment: ToolbarAlignment, + centerPosition?: 'left' | 'right', + ): Promise { + const { alignment: oldAlignment, itemIndex: oldItemIndex } = oldPosition; + let oldGroupIndex = oldPosition.groupIndex; + if (this.toolbarItems) { + const draggedItem = this.toolbarItems.items[oldAlignment][oldGroupIndex][oldItemIndex]; + const newGroupIndex = this.toolbarItems.items[oldAlignment].length; + const modifiedConfiguration = deepClone(this.toolbarItems); + if (newAlignment === ToolbarAlignment.LEFT) { + modifiedConfiguration.items[newAlignment].push([draggedItem]); + } else if (newAlignment === ToolbarAlignment.CENTER) { + if (centerPosition === 'left') { + modifiedConfiguration.items[newAlignment].unshift([draggedItem]); + if (newAlignment === oldAlignment) { + oldGroupIndex = oldGroupIndex + 1; + } + } else if (centerPosition === 'right') { + modifiedConfiguration.items[newAlignment].splice(newGroupIndex + 1, 0, [draggedItem]); + } + } else if (newAlignment === ToolbarAlignment.RIGHT) { + modifiedConfiguration.items[newAlignment].unshift([draggedItem]); + if (newAlignment === oldAlignment) { + oldGroupIndex = oldGroupIndex + 1; + } + } + modifiedConfiguration.items[oldAlignment][oldGroupIndex].splice(oldItemIndex, 1); + const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration); + return this.writeToFile([], sanitizedConfiguration); + } + return false; + } + + async insertGroup(position: ToolbarItemPosition, insertDirection: 'left' | 'right'): Promise { + if (this.toolbarItems) { + const { alignment, groupIndex, itemIndex } = position; + const modifiedConfiguration = deepClone(this.toolbarItems); + const originalColumn = modifiedConfiguration.items[alignment]; + if (originalColumn) { + const existingGroup = originalColumn[groupIndex]; + const existingGroupLength = existingGroup.length; + let poppedGroup: ValidMainToolbarItemDeflated[] = []; + let numItemsToRemove: number; + if (insertDirection === 'left' && itemIndex !== 0) { + numItemsToRemove = existingGroupLength - itemIndex; + poppedGroup = existingGroup.splice(itemIndex, numItemsToRemove); + originalColumn.splice(groupIndex, 1, existingGroup, poppedGroup); + } else if (insertDirection === 'right' && itemIndex !== existingGroupLength - 1) { + numItemsToRemove = itemIndex + 1; + poppedGroup = existingGroup.splice(0, numItemsToRemove); + originalColumn.splice(groupIndex, 1, poppedGroup, existingGroup); + } + const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration); + return this.writeToFile([], sanitizedConfiguration); + } + } + return false; + } + + protected removeEmptyGroupsFromToolbar( + toolbarItems: DeflatedMainToolbarTreeSchema | undefined, + ): DeflatedMainToolbarTreeSchema | undefined { + if (toolbarItems) { + const modifiedConfiguration = deepClone(toolbarItems); + const columns = [ToolbarAlignment.LEFT, ToolbarAlignment.CENTER, ToolbarAlignment.RIGHT]; + columns.forEach(column => { + const groups = toolbarItems.items[column]; + groups.forEach((group, index) => { + if (group.length === 0) { + modifiedConfiguration.items[column].splice(index, 1); + } + }); + }); + return modifiedConfiguration; + } + return undefined; + } + + async clearAll(): Promise { + if (this.model) { + const textModel = this.model.textEditorModel; + await this.monacoWorkspace.applyBackgroundEdit(this.model, [ + { + range: textModel.getFullModelRange(), + // eslint-disable-next-line no-null/no-null + text: null, + forceMoveMarkers: false, + }, + ]); + } + this.toolbarItemsUpdatedEmitter.fire(); + return true; + } + + protected async writeToFile(path: jsoncParser.JSONPath, value: unknown, insertion = false): Promise { + if (this.model) { + try { + const content = this.model.getText().trim(); + const textModel = this.model.textEditorModel; + const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = []; + const { insertSpaces, tabSize, defaultEOL } = textModel.getOptions(); + for (const edit of jsoncParser.modify(content, path, value, { + isArrayInsertion: insertion, + formattingOptions: { + insertSpaces, + tabSize, + eol: defaultEOL === monaco.editor.DefaultEndOfLine.LF ? '\n' : '\r\n', + }, + })) { + const start = textModel.getPositionAt(edit.offset); + const end = textModel.getPositionAt(edit.offset + edit.length); + editOperations.push({ + range: monaco.Range.fromPositions(start, end), + // eslint-disable-next-line no-null/no-null + text: edit.content || null, + forceMoveMarkers: false, + }); + } + await this.monacoWorkspace.applyBackgroundEdit(this.model, editOperations); + await this.model.save(); + return true; + } catch (e) { + const message = `Failed to update the value of '${path.join('.')}' in '${this.USER_TOOLBAR_URI}'.`; + this.messageService.error(TOOLBAR_BAD_JSON_ERROR_MESSAGE); + console.error(`${message}`, e); + return false; + } + } + return false; + } + + protected parseContent(fileContent: string): DeflatedMainToolbarTreeSchema | undefined { + const rawConfig = this.parse(fileContent); + if (!DeflatedMainToolbarTreeSchema.is(rawConfig)) { + return undefined; + } + return rawConfig; + } + + protected parse(fileContent: string): MainToolbarTreeSchema | undefined { + let strippedContent = fileContent.trim(); + if (!strippedContent) { + return undefined; + } + strippedContent = jsoncParser.stripComments(strippedContent); + return jsoncParser.parse(strippedContent); + } + + async openOrCreateJSONFile(state: MainToolbarTreeSchema, doOpen = false): Promise { + const fileExists = await this.fileService.exists(this.USER_TOOLBAR_URI); + let doWriteStateToFile = false; + if (fileExists) { + const fileContent = await this.fileService.read(this.USER_TOOLBAR_URI); + if (fileContent.value.trim() === '') { + doWriteStateToFile = true; + } + } else { + await this.fileService.create(this.USER_TOOLBAR_URI); + doWriteStateToFile = true; + } + if (doWriteStateToFile) { + await this.writeToFile([], state); + } + this.readConfiguration(); + if (doOpen) { + const widget = await this.editorManager.open(this.USER_TOOLBAR_URI); + return widget; + } + return undefined; + } + + dispose(): void { + this.toDispose.dispose(); + } +} diff --git a/packages/toolbar/src/browser/main-toolbar.tsx b/packages/toolbar/src/browser/main-toolbar.tsx new file mode 100644 index 0000000000000..1945dae0af696 --- /dev/null +++ b/packages/toolbar/src/browser/main-toolbar.tsx @@ -0,0 +1,422 @@ +/******************************************************************************** + * Copyright (C) 2022 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 '@theia/core/shared/react'; +import { Anchor, ContextMenuAccess, KeybindingRegistry, PreferenceService, Widget, WidgetManager } from '@theia/core/lib/browser'; +import { LabelIcon } from '@theia/core/lib/browser/label-parser'; +import { TabBarToolbar, TabBarToolbarFactory, TabBarToolbarItem, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; +import { MenuPath, ProgressService } from '@theia/core'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { ProgressBarFactory } from '@theia/core/lib/browser/progress-bar-factory'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { + ValidMainToolbarItem, + ToolbarAlignment, + ToolbarAlignmentString, + ToolbarItemPosition, +} from './main-toolbar-interfaces'; +import { MainToolbarController } from './main-toolbar-controller'; +import { MainToolbarMenus } from './main-toolbar-constants'; + +const TOOLBAR_BACKGROUND_DATA_ID = 'main-toolbar-wrapper'; +export const TOOLBAR_PROGRESSBAR_ID = 'main-toolbar-progress'; +@injectable() +export class MainToolbarImpl extends TabBarToolbar { + @inject(TabBarToolbarFactory) protected readonly tabbarToolbarFactory: TabBarToolbarFactory; + @inject(TabBarToolbarRegistry) protected tabBarToolBarRegistry: TabBarToolbarRegistry; + @inject(WidgetManager) protected readonly widgetManager: WidgetManager; + @inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService; + @inject(MainToolbarController) protected readonly model: MainToolbarController; + @inject(PreferenceService) protected readonly preferenceService: PreferenceService; + @inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry; + @inject(ProgressBarFactory) protected readonly progressFactory: ProgressBarFactory; + @inject(ProgressService) protected readonly progressService: ProgressService; + + protected currentlyDraggedItem: HTMLDivElement | undefined; + protected draggedStartingPosition: ToolbarItemPosition | undefined; + protected deferredRef = new Deferred(); + protected isBusyDeferred = new Deferred(); + + @postConstruct() + async init(): Promise { + this.hide(); + await this.model.ready.promise; + + this.updateInlineItems(); + this.update(); + this.model.onToolbarModelDidUpdate(() => { + this.updateInlineItems(); + this.update(); + }); + this.model.onToolbarDidChangeBusyState(isBusy => { + if (isBusy) { + this.isBusyDeferred = new Deferred(); + this.progressService.withProgress('test', TOOLBAR_PROGRESSBAR_ID, async () => this.isBusyDeferred.promise); + } else { + this.isBusyDeferred.resolve(); + } + }); + + await this.deferredRef.promise; + this.progressFactory({ container: this.node, insertMode: 'append', locationId: TOOLBAR_PROGRESSBAR_ID }); + } + + protected updateInlineItems(): void { + this.inline.clear(); + const { items } = this.model.toolbarItems; + for (const column of Object.keys(items)) { + for (const group of items[column as ToolbarAlignment]) { + for (const item of group) { + this.inline.set(item.id, item); + } + } + } + } + + protected handleContextMenu = (e: React.MouseEvent): ContextMenuAccess => this.doHandleContextMenu(e); + protected doHandleContextMenu(event: React.MouseEvent): ContextMenuAccess { + event.preventDefault(); + event.stopPropagation(); + const contextMenuArgs = this.getContextMenuArgs(event); + const { menuPath, anchor } = this.getMenuDetailsForClick(event); + return this.contextMenuRenderer.render({ + args: contextMenuArgs, + menuPath, + anchor, + }); + } + + protected getMenuDetailsForClick(event: React.MouseEvent): { menuPath: MenuPath; anchor: Anchor } { + const clickId = event.currentTarget.getAttribute('data-id'); + let menuPath: MenuPath; + let anchor: Anchor; + if (clickId === TOOLBAR_BACKGROUND_DATA_ID) { + menuPath = MainToolbarMenus.MAIN_TOOLBAR_BACKGROUND_CONTEXT_MENU; + const { clientX, clientY } = event; + anchor = { x: clientX, y: clientY }; + } else { + menuPath = MainToolbarMenus.TOOLBAR_ITEM_CONTEXT_MENU; + const { left, bottom } = event.currentTarget.getBoundingClientRect(); + anchor = { x: left, y: bottom }; + } + return { menuPath, anchor }; + } + + protected getContextMenuArgs(event: React.MouseEvent): Array { + const args: Array = [this]; + // data-position is the stringified position of a given toolbar item, this allows + // the model to be aware of start/stop positions during drag & drop and CRUD operations + const position = event.currentTarget.getAttribute('data-position'); + const id = event.currentTarget.getAttribute('data-id'); + if (position) { + args.push(JSON.parse(position)); + } else if (id) { + args.push(id); + } + return args; + } + + protected renderGroupsInColumn(groups: ValidMainToolbarItem[][], alignment: ToolbarAlignment): React.ReactNode { + const nodes: React.ReactNodeArray = []; + groups.forEach((group, groupIndex) => { + if (nodes.length && group.length) { + nodes.push(
); + } + group.forEach((item, itemIndex) => { + const position = { alignment, groupIndex, itemIndex }; + nodes.push(this.renderItemWithDraggableWrapper(item, position)); + }); + }); + return nodes; + } + + protected assignRef = (element: HTMLDivElement): void => this.doAssignRef(element); + protected doAssignRef(element: HTMLDivElement): void { + this.deferredRef.resolve(element); + } + + protected render(): React.ReactNode { + const leftGroups = this.model.toolbarItems?.items[ToolbarAlignment.LEFT]; + const centerGroups = this.model.toolbarItems?.items[ToolbarAlignment.CENTER]; + const rightGroups = this.model.toolbarItems?.items[ToolbarAlignment.RIGHT]; + return ( +
+ {this.renderColumnWrapper(ToolbarAlignment.LEFT, leftGroups)} + {this.renderColumnWrapper(ToolbarAlignment.CENTER, centerGroups)} + {this.renderColumnWrapper(ToolbarAlignment.RIGHT, rightGroups)} +
+ ); + } + + protected renderColumnWrapper(alignment: ToolbarAlignment, columnGroup: ValidMainToolbarItem[][]): React.ReactNode { + let children: React.ReactNode; + if (alignment === ToolbarAlignment.LEFT) { + children = ( + <> + {this.renderGroupsInColumn(columnGroup, alignment)} + {this.renderColumnSpace(alignment)} + + ); + } else if (alignment === ToolbarAlignment.CENTER) { + const isCenterColumnEmpty = !columnGroup.length; + if (isCenterColumnEmpty) { + children = this.renderColumnSpace(alignment, 'left'); + } else { + children = ( + <> + {this.renderColumnSpace(alignment, 'left')} + {this.renderGroupsInColumn(columnGroup, alignment)} + {this.renderColumnSpace(alignment, 'right')} + + ); + } + } else if (alignment === ToolbarAlignment.RIGHT) { + children = ( + <> + {this.renderColumnSpace(alignment)} + {this.renderGroupsInColumn(columnGroup, alignment)} + + ); + } + return ( +
+ {children} +
); + } + + protected renderColumnSpace(alignment: ToolbarAlignment, position?: 'left' | 'right'): React.ReactNode { + return ( +
+ ); + } + + protected renderItemWithDraggableWrapper(item: ValidMainToolbarItem, position: ToolbarItemPosition): React.ReactNode { + const stringifiedPosition = JSON.stringify(position); + let toolbarItemClassNames = ''; + let renderBody: React.ReactNode; + if (TabBarToolbarItem.is(item)) { + const command = this.commands.getCommand(item.command); + toolbarItemClassNames = this.getToolbarItemClassNames(command?.id); + renderBody = this.renderItem(item); + } else { + const contribution = this.model.getContributionByID(item.id); + if (contribution) { + renderBody = contribution.render(); + } + } + return ( +
+ {renderBody} +
+
+ ); + } + + protected renderItem( + item: TabBarToolbarItem, + ): React.ReactNode { + const classNames = []; + if (item.text) { + for (const labelPart of this.labelParser.parse(item.text)) { + if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) { + const className = `fa fa-${labelPart.name}${labelPart.animation ? ' fa-' + labelPart.animation : ''}`; + classNames.push(...className.split(' ')); + } + } + } + const command = this.commands.getCommand(item.command); + const iconClass = (typeof item.icon === 'function' && item.icon()) || item.icon || command?.iconClass; + if (iconClass) { + classNames.push(iconClass); + } + let itemTooltip = ''; + if (item.tooltip) { + itemTooltip = item.tooltip; + } else if (command?.label) { + itemTooltip = command.label; + } + const keybindingString = this.resolveKeybindingForCommand(command?.id); + itemTooltip = `${itemTooltip}${keybindingString}`; + + return ( +
+ ); + } + + protected resolveKeybindingForCommand(commandID: string | undefined): string { + if (!commandID) { + return ''; + } + const keybindings = this.keybindingRegistry.getKeybindingsForCommand(commandID); + if (keybindings.length > 0) { + const binding = keybindings[0]; + const bindingKeySequence = this.keybindingRegistry.resolveKeybinding(binding); + const keyCode = bindingKeySequence[0]; + return ` (${this.keybindingRegistry.acceleratorForKeyCode(keyCode, '+')})`; + } + return ''; + } + + protected handleOnDragStart = (e: React.DragEvent): void => this.doHandleOnDragStart(e); + protected doHandleOnDragStart(e: React.DragEvent): void { + const draggedElement = e.currentTarget; + draggedElement.classList.add('dragging'); + e.dataTransfer.setDragImage(draggedElement, 0, 0); + const position = JSON.parse(e.currentTarget.getAttribute('data-position') ?? ''); + this.currentlyDraggedItem = e.currentTarget; + this.draggedStartingPosition = position; + } + + protected handleOnDragEnter = (e: React.DragEvent): void => this.doHandleItemOnDragEnter(e); + protected doHandleItemOnDragEnter(e: React.DragEvent): void { + e.preventDefault(); + e.stopPropagation(); + const targetItemDOMElement = e.currentTarget; + const targetItemHoverOverlay = targetItemDOMElement.querySelector('.hover-overlay'); + const targetItemId = e.currentTarget.getAttribute('data-id'); + if (targetItemDOMElement.classList.contains('empty-column-space')) { + targetItemDOMElement.classList.add('drag-over'); + } else if (targetItemDOMElement.classList.contains('main-toolbar-item') && targetItemHoverOverlay) { + const { clientX } = e; + const { left, right } = e.currentTarget.getBoundingClientRect(); + const targetMiddleX = (left + right) / 2; + if (targetItemId !== this.currentlyDraggedItem?.getAttribute('data-id')) { + targetItemHoverOverlay.classList.add('drag-over'); + if (clientX <= targetMiddleX) { + targetItemHoverOverlay.classList.add('location-left'); + targetItemHoverOverlay.classList.remove('location-right'); + } else { + targetItemHoverOverlay.classList.add('location-right'); + targetItemHoverOverlay.classList.remove('location-left'); + } + } + } + } + + protected handleOnDragLeave = (e: React.DragEvent): void => this.doHandleOnDragLeave(e); + protected doHandleOnDragLeave(e: React.DragEvent): void { + e.preventDefault(); + e.stopPropagation(); + const targetItemDOMElement = e.currentTarget; + const targetItemHoverOverlay = targetItemDOMElement.querySelector('.hover-overlay'); + if (targetItemDOMElement.classList.contains('empty-column-space')) { + targetItemDOMElement.classList.remove('drag-over'); + } else if (targetItemHoverOverlay && targetItemDOMElement.classList.contains('main-toolbar-item')) { + targetItemHoverOverlay?.classList.remove('drag-over', 'location-left', 'location-right'); + } + } + + protected handleOnDrop = (e: React.DragEvent): void => this.doHandleOnDrop(e); + protected doHandleOnDrop(e: React.DragEvent): void { + e.preventDefault(); + e.stopPropagation(); + const targetItemDOMElement = e.currentTarget; + const targetItemHoverOverlay = targetItemDOMElement.querySelector('.hover-overlay'); + if (targetItemDOMElement.classList.contains('empty-column-space')) { + this.handleDropInEmptySpace(targetItemDOMElement); + targetItemDOMElement.classList.remove('drag-over'); + } else if (targetItemHoverOverlay && targetItemDOMElement.classList.contains('main-toolbar-item')) { + this.handleDropInExistingGroup(targetItemDOMElement); + targetItemHoverOverlay.classList.remove('drag-over', 'location-left', 'location-right'); + } + this.currentlyDraggedItem = undefined; + this.draggedStartingPosition = undefined; + } + + protected handleDropInExistingGroup(element: EventTarget & HTMLDivElement): void { + const position = element.getAttribute('data-position'); + const targetDirection = element.querySelector('.hover-overlay')?.classList.toString() + .split(' ') + .find(className => className.includes('location')); + const dropPosition = JSON.parse(position ?? ''); + if (this.currentlyDraggedItem && targetDirection + && this.draggedStartingPosition && !this.arePositionsEquivalent(this.draggedStartingPosition, dropPosition)) { + this.model.swapValues( + this.draggedStartingPosition, + dropPosition, + targetDirection as 'location-left' | 'location-right', + ); + } + } + + protected handleDropInEmptySpace(element: EventTarget & HTMLDivElement): void { + const column = element.getAttribute('data-column'); + if (ToolbarAlignmentString.is(column) && this.draggedStartingPosition) { + if (column === ToolbarAlignment.CENTER) { + const centerPosition = element.getAttribute('data-center-position'); + this.model.moveItemToEmptySpace(this.draggedStartingPosition, column, centerPosition as 'left' | 'right'); + } else { + this.model.moveItemToEmptySpace(this.draggedStartingPosition, column); + } + } + } + + protected arePositionsEquivalent(start: ToolbarItemPosition, end: ToolbarItemPosition): boolean { + return start.alignment === end.alignment + && start.groupIndex === end.groupIndex + && start.itemIndex === end.itemIndex; + } + + protected handleOnDragEnd = (e: React.DragEvent): void => this.doHandleOnDragEnd(e); + protected doHandleOnDragEnd(e: React.DragEvent): void { + e.preventDefault(); + e.stopPropagation(); + this.currentlyDraggedItem = undefined; + this.draggedStartingPosition = undefined; + e.currentTarget.classList.remove('dragging'); + } +} diff --git a/packages/toolbar/src/browser/package.spec.ts b/packages/toolbar/src/browser/package.spec.ts new file mode 100644 index 0000000000000..f8509b7b59498 --- /dev/null +++ b/packages/toolbar/src/browser/package.spec.ts @@ -0,0 +1,19 @@ +/******************************************************************************** + * Copyright (C) 2022 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 + ********************************************************************************/ + +describe('timeline package', () => { + it('supports code coverage statistics', () => true); +}); diff --git a/packages/toolbar/src/browser/search-in-workspace-root-quick-input-service.ts b/packages/toolbar/src/browser/search-in-workspace-root-quick-input-service.ts new file mode 100644 index 0000000000000..234d03e012e7b --- /dev/null +++ b/packages/toolbar/src/browser/search-in-workspace-root-quick-input-service.ts @@ -0,0 +1,48 @@ +/******************************************************************************** + * Copyright (C) 2022 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 '@theia/core/shared/inversify'; +import { LabelProvider, QuickInputService, QuickPickItem } from '@theia/core/lib/browser'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { CommandService } from '@theia/core'; +import { SearchInWorkspaceCommands } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution'; + +@injectable() +export class SearchInWorkspaceQuickInputService { + @inject(QuickInputService) protected readonly quickInputService: QuickInputService; + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + @inject(CommandService) protected readonly commandService: CommandService; + protected quickPickItems: QuickPickItem[] = []; + + open(): void { + this.quickPickItems = this.createWorkspaceList(); + this.quickInputService.showQuickPick(this.quickPickItems, { + placeholder: 'Workspace root to search', + }); + } + + protected createWorkspaceList(): QuickPickItem[] { + const roots = this.workspaceService.tryGetRoots(); + return roots.map(root => { + const uri = root.resource; + return { + label: this.labelProvider.getName(uri), + execute: (): Promise => this.commandService.executeCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER.id, [uri]), + }; + }); + } +} diff --git a/packages/toolbar/src/browser/style/easy-search-style.css b/packages/toolbar/src/browser/style/easy-search-style.css new file mode 100644 index 0000000000000..ed98113a5d1a6 --- /dev/null +++ b/packages/toolbar/src/browser/style/easy-search-style.css @@ -0,0 +1,39 @@ +/******************************************************************************** + * Copyright (C) 2022 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 + ********************************************************************************/ + + +#easy-search-toolbar-widget { + position: relative; +} + +#easy-search-toolbar-widget:focus, +#easy-search-toolbar-widget .icon-wrapper:focus, +#easy-search-toolbar-widget .codicon-search:focus { + outline: none; +} + +#easy-search-toolbar-widget #easy-search-item-icon.codicon-search { + position: relative; + top: 2px; + font-size: 20px; +} + +#easy-search-toolbar-widget .icon-wrapper .codicon-triangle-down { + position: absolute; + font-size: 11px; + top: 14px; + right: -1px; +} diff --git a/packages/toolbar/src/browser/style/toolbar-shell-style.css b/packages/toolbar/src/browser/style/toolbar-shell-style.css new file mode 100644 index 0000000000000..85ddb42764817 --- /dev/null +++ b/packages/toolbar/src/browser/style/toolbar-shell-style.css @@ -0,0 +1,237 @@ +/******************************************************************************** + * Copyright (C) 2022 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 + ********************************************************************************/ + + + +#main-toolbar { + --toolbarHeight: calc(var(--theia-private-menubar-height) - 3px); + --toolbar-item-padding: 5px; + --dropzone-background: rgb(0, 0, 0, 0.3); + + min-height: var(--toolbarHeight); + color: var(--theia-activityBar-foreground); + background: var(--theia-activityBar-background); + box-shadow: 0px 4px 2px -2px var(--theia-widget-shadow); + padding: 2px 4px; + display: flex; + flex-direction: column; +} + +.main-toolbar-wrapper { + display: flex; + flex-direction: row; + width: 100%; + overflow: hidden; + margin-top: 1px; +} + +.main-toolbar-wrapper .toolbar-column { + display: flex; + flex: 1; +} + +.main-toolbar-wrapper .left { + justify-content: flex-start; +} + +.main-toolbar-wrapper .center { + justify-content: center; +} + +.main-toolbar-wrapper .right { + justify-content: flex-end; +} + +.main-toolbar-wrapper:focus { + outline: none; +} + +#main-toolbar .main-toolbar-item { + margin: 0; + padding: 0 var(--toolbar-item-padding); + box-sizing: border-box; + position: relative; + background: unset; + cursor: pointer; +} + +#main-toolbar .main-toolbar-item#workbench\.action\.splitEditorRight { + opacity: 1; +} + +#main-toolbar .empty-column-space { + flex-grow: 1; +} + +#main-toolbar .main-toolbar-item .codicon { + font-size: 20px; +} + +#main-toolbar .main-toolbar-item.enabled:hover:not(.dragging):not(.active) { + transform: scale(1.1); +} + +#main-toolbar .main-toolbar-item .hover-overlay { + position: absolute; + pointer-events: none; + height: 100%; + width: 100%; + left: 0; + top: 0; +} + +#main-toolbar .main-toolbar-item .hover-overlay.drag-over { + background-color: var(--theia-activityBar-foreground); + opacity: 0.3; +} + +#main-toolbar .main-toolbar-item .hover-overlay.location-left { + width: 25%; +} + +#main-toolbar .main-toolbar-item .hover-overlay.location-right { + width: 25%; + left: 75%; + right: 0; +} + +#main-toolbar .main-toolbar-item.dragging { + opacity: 0.3; +} + +#main-toolbar .main-toolbar-item:focus { + outline: none; +} + +#main-toolbar .item:focus, +#main-toolbar .item div:focus { + outline: none; +} + +#main-toolbar .separator { + width: 1px; + background-color: var(--theia-activityBar-foreground); + opacity: 0.8; + margin: 0 5px; +} + +.toolbar-column { + display: flex; +} + +.toolbar-column.left { + margin-right: var(--toolbar-item-padding); +} + +.toolbar-column.right { + margin-left: var(--toolbar-item-padding); +} + +.toolbar-column.empty { + min-width: 60px; +} + +.empty-column-space.drag-over { + background-color: var(--theia-activityBar-foreground); + opacity: 0.3; + border-radius: 2px; +} + +#main-toolbar-icon-selector-dialog .dialogBlock { + max-height: 75%; + width: 600px; +} + +#main-toolbar-icon-selector-dialog .dialogContent { + overflow: hidden; + display: block; +} + +#main-toolbar-icon-selector-dialog .dialogContent .icon-selector-options { + display: flex; +} + +#main-toolbar-icon-selector-dialog .dialogContent .icon-wrapper:focus { + box-shadow: unset; + outline: solid 1px var(--theia-focusBorder); +} + +#main-toolbar-icon-selector-dialog .dialogControl { + padding-top: var(--theia-ui-padding); +} + +.main-toolbar-icon-dialog-content.grid { + --grid-size: 28px; + + display: grid; + grid-template-columns: repeat(20, var(--grid-size)); + grid-template-rows: var(--grid-size); + grid-auto-rows: var(--grid-size); +} + +.main-toolbar-icon-dialog-content .icon-wrapper { + height: 100%; + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.main-toolbar-icon-dialog-content .search-placeholder { + text-align: center; + margin-top: var(--theia-ui-padding); + font-size: 1.2em; +} + +.main-toolbar-icon-controls { + display: flex; + width: 100%; + justify-content: space-between; +} + +.icon-selector-options { + justify-content: space-between; +} + +.icon-selector-options .icon-filter-input { + height: 18px; +} + +.main-toolbar-icon-select { + margin-bottom: var(--theia-ui-padding); +} + +.main-toolbar-icon-controls .main-toolbar-default-icon { + margin-left: var(--theia-ui-padding); + font-size: 16px; +} + +.main-toolbar-icon-dialog-content .icon-wrapper.selected { + background-color: var(--theia-list-activeSelectionBackground); +} + +.main-toolbar-icon-dialog-content .fa, +.main-toolbar-icon-dialog-content .codicon { + font-size: 20px; +} + +.main-toolbar-scroll-container { + height: 375px; + position: relative; + padding: 0 var(--theia-ui-padding); + border: 1px solid var(--theia-editorWidget-border); + background-color: var(--theia-dropdown-background); +} diff --git a/packages/toolbar/tsconfig.json b/packages/toolbar/tsconfig.json new file mode 100644 index 0000000000000..b973ddbc673a2 --- /dev/null +++ b/packages/toolbar/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [] +} diff --git a/tsconfig.json b/tsconfig.json index aa7ba77f9ac52..b9997bb80a9d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -150,6 +150,9 @@ { "path": "packages/timeline" }, + { + "path": "packages/toolbar" + }, { "path": "packages/typehierarchy" },