From 33408322658632cdd78ef6b6616b71bc8b40130d Mon Sep 17 00:00:00 2001 From: Jan Bicker Date: Mon, 18 Mar 2019 15:12:00 +0000 Subject: [PATCH] Implemented Toolbar support for sidepanels and changed sidepanel tabs. Every sidepanel has an header now where the title and a toolbar is shown (if tools are registered for that widget). As an example the control buttons of search in workspace extension are moved to the toolbar. Additionally the tabs of sidebars don't show labels anymore but icons. Fixes theia-ide/theia#3864 Signed-off-by: Jan Bicker --- CHANGELOG.md | 2 + .../browser/frontend-application-module.ts | 4 +- .../src/browser/shell/side-panel-handler.ts | 33 ++++++- .../src/browser/shell/side-panel-toolbar.ts | 88 +++++++++++++++++ .../src/browser/shell/tab-bar-toolbar.tsx | 28 +++++- packages/core/src/browser/shell/tab-bars.ts | 1 + packages/core/src/browser/style/index.css | 2 +- packages/core/src/browser/style/sidepanel.css | 98 +++++++++++++++---- packages/core/src/browser/style/tabs.css | 36 ++++++- .../style/variables-bright.useable.css | 5 + .../browser/style/variables-dark.useable.css | 5 + .../debug/src/browser/style/debug-dark.svg | 4 + packages/debug/src/browser/style/index.css | 5 +- .../debug/src/browser/view/debug-widget.ts | 2 +- .../src/browser/extension-widget.tsx | 2 +- .../src/browser/style/extension-sidebar.css | 5 +- .../src/browser/style/extensions.svg | 4 + packages/git/src/browser/git-widget.tsx | 2 +- packages/git/src/browser/style/git.svg | 4 + packages/git/src/browser/style/index.css | 5 +- .../src/browser/navigator-contribution.ts | 2 +- .../src/browser/navigator-widget.tsx | 4 +- .../navigator/src/browser/style/files.svg | 4 + .../navigator/src/browser/style/index.css | 5 +- .../src/main/browser/view/view-registry.ts | 1 + ...arch-in-workspace-frontend-contribution.ts | 72 +++++++++++++- .../search-in-workspace-frontend-module.ts | 2 + .../browser/search-in-workspace-widget.tsx | 97 +++++++++--------- .../src/browser/styles/index.css | 35 ++----- .../src/browser/styles/search.svg | 6 ++ 30 files changed, 438 insertions(+), 125 deletions(-) create mode 100644 packages/core/src/browser/shell/side-panel-toolbar.ts create mode 100644 packages/debug/src/browser/style/debug-dark.svg create mode 100644 packages/extension-manager/src/browser/style/extensions.svg create mode 100644 packages/git/src/browser/style/git.svg create mode 100644 packages/navigator/src/browser/style/files.svg create mode 100644 packages/search-in-workspace/src/browser/styles/search.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 5019dea70dbce..9cc81ef6baa7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Breaking changes: - [plugin] support multiple windows per a backend [#4509](https://github.com/theia-ide/theia/issues/4509) - Some plugin bindings are scoped per a connection now. Clients, who contribute/rebind these bindings, will need to scope them per a connection as well. - [quick-open] disable separate fuzzy matching by default [#4549](https://github.com/theia-ide/theia/pull/4549) +- [shell] support toolbars in side bars [#4600](https://github.com/theia-ide/theia/pull/4600) + - In side bars a widget title is rendered as an icon. ## v0.4.0 - [application-manager] added support for pre-load HTML templates diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 052e5f74e7e91..eeff9e95a0a4e 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -44,7 +44,9 @@ import { LocalStorageService, StorageService } from './storage-service'; import { WidgetFactory, WidgetManager } from './widget-manager'; import { ApplicationShell, ApplicationShellOptions, DockPanelRenderer, TabBarRenderer, - TabBarRendererFactory, ShellLayoutRestorer, SidePanelHandler, SidePanelHandlerFactory, SplitPositionHandler, DockPanelRendererFactory + TabBarRendererFactory, ShellLayoutRestorer, + SidePanelHandler, SidePanelHandlerFactory, + SplitPositionHandler, DockPanelRendererFactory } from './shell'; import { StatusBar, StatusBarImpl } from './status-bar/status-bar'; import { LabelParser } from './label-parser'; diff --git a/packages/core/src/browser/shell/side-panel-handler.ts b/packages/core/src/browser/shell/side-panel-handler.ts index 29bd26ad3530a..9449c8c6b7db8 100644 --- a/packages/core/src/browser/shell/side-panel-handler.ts +++ b/packages/core/src/browser/shell/side-panel-handler.ts @@ -24,6 +24,8 @@ import { TabBarRendererFactory, TabBarRenderer, SHELL_TABBAR_CONTEXT_MENU, SideT import { SplitPositionHandler, SplitPositionOptions } from './split-panels'; import { FrontendApplicationStateService } from '../frontend-application-state'; import { TheiaDockPanel } from './theia-dock-panel'; +import { SidePanelToolbar } from './side-panel-toolbar'; +import { TabBarToolbarRegistry, TabBarToolbarFactory, TabBarToolbar } from './tab-bar-toolbar'; /** The class name added to the left and right area panels. */ export const LEFT_RIGHT_AREA_CLASS = 'theia-app-sides'; @@ -56,6 +58,10 @@ export class SidePanelHandler { * tab bar itself remains visible as long as there is at least one widget. */ tabBar: SideTabBar; + /** + * A tool bar, which displays a title and widget specific command buttons. + */ + toolBar: SidePanelToolbar; /** * The widget container is a dock panel in `single-document` mode, which means that the panel * cannot be split. @@ -85,6 +91,8 @@ export class SidePanelHandler { */ protected options: SidePanel.Options; + @inject(TabBarToolbarRegistry) protected tabBarToolBarRegistry: TabBarToolbarRegistry; + @inject(TabBarToolbarFactory) protected tabBarToolBarFactory: () => TabBarToolbar; @inject(TabBarRendererFactory) protected tabBarRendererFactory: () => TabBarRenderer; @inject(SplitPositionHandler) protected splitPositionHandler: SplitPositionHandler; @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService; @@ -96,6 +104,7 @@ export class SidePanelHandler { this.side = side; this.options = options; this.tabBar = this.createSideBar(); + this.toolBar = this.createToolbar(); this.dockPanel = this.createSidePanel(); this.container = this.createContainer(); @@ -152,7 +161,19 @@ export class SidePanelHandler { return sidePanel; } + protected createToolbar(): SidePanelToolbar { + const toolbar = new SidePanelToolbar(this.tabBarToolBarRegistry, this.tabBarToolBarFactory, this.side); + return toolbar; + } + protected createContainer(): Panel { + const contentBox = new BoxLayout({ direction: 'top-to-bottom', spacing: 0 }); + BoxPanel.setStretch(this.toolBar, 0); + contentBox.addWidget(this.toolBar); + BoxPanel.setStretch(this.dockPanel, 1); + contentBox.addWidget(this.dockPanel); + const contentPanel = new BoxPanel({layout: contentBox}); + const side = this.side; let direction: BoxLayout.Direction; switch (side) { @@ -165,12 +186,12 @@ export class SidePanelHandler { default: throw new Error('Illegal argument: ' + side); } - const boxLayout = new BoxLayout({ direction, spacing: 0 }); + const containerLayout = new BoxLayout({ direction, spacing: 0 }); BoxPanel.setStretch(this.tabBar, 0); - boxLayout.addWidget(this.tabBar); - BoxPanel.setStretch(this.dockPanel, 1); - boxLayout.addWidget(this.dockPanel); - const boxPanel = new BoxPanel({ layout: boxLayout }); + containerLayout.addWidget(this.tabBar); + BoxPanel.setStretch(contentPanel, 1); + containerLayout.addWidget(contentPanel); + const boxPanel = new BoxPanel({ layout: containerLayout }); boxPanel.id = 'theia-' + side + '-content-panel'; return boxPanel; } @@ -327,6 +348,8 @@ export class SidePanelHandler { const hideDockPanel = currentTitle === null; let relativeSizes: number[] | undefined; + this.toolBar.toolbarTitle = currentTitle || undefined; + if (hideDockPanel) { container.addClass(COLLAPSED_CLASS); if (this.state.expansion === SidePanel.ExpansionState.expanded && !this.state.empty) { diff --git a/packages/core/src/browser/shell/side-panel-toolbar.ts b/packages/core/src/browser/shell/side-panel-toolbar.ts new file mode 100644 index 0000000000000..8a920b41c8bd6 --- /dev/null +++ b/packages/core/src/browser/shell/side-panel-toolbar.ts @@ -0,0 +1,88 @@ +/******************************************************************************** + * Copyright (C) 2019 TypeFox 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 { Widget, Title } from '@phosphor/widgets'; +import { TabBarToolbar, TabBarToolbarRegistry } from './tab-bar-toolbar'; +import { Message } from '@phosphor/messaging'; + +export class SidePanelToolbar extends Widget { + + protected titleContainer: HTMLElement | undefined; + private _toolbarTitle: Title | undefined; + protected toolbar: TabBarToolbar | undefined; + + constructor( + protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry, + protected readonly tabBarToolbarFactory: () => TabBarToolbar, + protected readonly side: 'left' | 'right') { + super(); + this.init(); + this.tabBarToolbarRegistry.onDidChange(() => this.update()); + } + + protected onAfterAttach(msg: Message): void { + if (this.toolbar) { + if (this.toolbar.isAttached) { + Widget.detach(this.toolbar); + } + Widget.attach(this.toolbar, this.node); + } + super.onAfterAttach(msg); + } + + protected onBeforeDetach(msg: Message): void { + if (this.titleContainer) { + this.node.removeChild(this.titleContainer); + } + if (this.toolbar && this.toolbar.isAttached) { + Widget.detach(this.toolbar); + } + super.onBeforeDetach(msg); + } + + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + this.updateToolbar(); + } + + protected updateToolbar(): void { + if (!this.toolbar) { + return; + } + const current = this._toolbarTitle; + const widget = current && current.owner || undefined; + const items = widget ? this.tabBarToolbarRegistry.visibleItems(widget) : []; + this.toolbar.updateItems(items, widget); + } + + protected init(): void { + this.titleContainer = document.createElement('div'); + this.titleContainer.classList.add('theia-sidepanel-title'); + this.node.appendChild(this.titleContainer); + this.node.classList.add('theia-sidepanel-toolbar'); + this.node.classList.add(`theia-${this.side}-side-panel`); + this.toolbar = this.tabBarToolbarFactory(); + this.update(); + } + + set toolbarTitle(title: Title | undefined) { + if (this.titleContainer && title) { + this._toolbarTitle = title; + this.titleContainer.innerHTML = this._toolbarTitle.label; + this.update(); + } + } +} diff --git a/packages/core/src/browser/shell/tab-bar-toolbar.tsx b/packages/core/src/browser/shell/tab-bar-toolbar.tsx index 2c411ec389621..46f3136cf3b53 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar.tsx +++ b/packages/core/src/browser/shell/tab-bar-toolbar.tsx @@ -23,6 +23,9 @@ import { FrontendApplicationContribution } from '../frontend-application'; import { CommandRegistry, CommandService } from '../../common/command'; import { Disposable } from '../../common/disposable'; import { ContextKeyService } from '../context-key-service'; +import { Event, Emitter } from '../../common/event'; + +import debounce = require('lodash.debounce'); /** * Factory for instantiating tab-bar toolbars. @@ -80,15 +83,21 @@ export class TabBarToolbar extends ReactWidget { } } const command = this.commands.getCommand(item.command); - const iconClass = command && command.iconClass; - if (iconClass) { - classNames.push(iconClass); + if (command) { + const iconClass = command.iconClass; + if (iconClass) { + classNames.push(iconClass); + } } - return
+ return
{innerText}
; } + protected commandIsEnabled(command: string): boolean { + return this.commands.isEnabled(command, this.current); + } + protected executeCommand = (e: React.MouseEvent) => { const item = this.items.get(e.currentTarget.id); if (item) { @@ -174,6 +183,8 @@ export interface TabBarToolbarItem { */ readonly when?: string; + readonly onDidChange?: Event; + } export namespace TabBarToolbarItem { @@ -227,6 +238,11 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { @named(TabBarToolbarContribution) protected readonly contributionProvider: ContributionProvider; + protected readonly onDidChangeEmitter = new Emitter(); + readonly onDidChange: Event = this.onDidChangeEmitter.event; + // debounce in order to avoid to fire more than once in the same tick + protected fireOnDidChange = debounce(() => this.onDidChangeEmitter.fire(undefined), 0); + onStart(): void { const contributions = this.contributionProvider.getContributions(); for (const contribution of contributions) { @@ -245,6 +261,10 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { throw new Error(`A toolbar item is already registered with the '${id}' ID.`); } this.items.set(id, item); + this.fireOnDidChange(); + if (item.onDidChange) { + item.onDidChange(() => this.fireOnDidChange()); + } } /** diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index 63bb8eed1cedb..17a08570bfd16 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -305,6 +305,7 @@ export class ToolbarAwareTabBar extends ScrollableTabBar { super(options); this.rewireDOM(); + this.tabBarToolbarRegistry.onDidChange(() => this.update()); } /** diff --git a/packages/core/src/browser/style/index.css b/packages/core/src/browser/style/index.css index b6c0e9c932bef..21d3f9a31482c 100644 --- a/packages/core/src/browser/style/index.css +++ b/packages/core/src/browser/style/index.css @@ -59,7 +59,7 @@ body { } .theia-icon { - width: 18px; + width: 32px; height: 18px; margin: 5px; margin-left: 8px; diff --git a/packages/core/src/browser/style/sidepanel.css b/packages/core/src/browser/style/sidepanel.css index 32ee05c42dd82..83f4538363645 100644 --- a/packages/core/src/browser/style/sidepanel.css +++ b/packages/core/src/browser/style/sidepanel.css @@ -19,7 +19,8 @@ |----------------------------------------------------------------------------*/ :root { - --theia-private-sidebar-tab-width: 32px; + --theia-private-sidebar-tab-width: 50px; + --theia-private-sidebar-tab-height: 32px; --theia-private-sidebar-scrollbar-rail-width: 7px; --theia-private-sidebar-scrollbar-width: 5px; } @@ -31,10 +32,10 @@ .p-TabBar.theia-app-sides { display: block; - color: var(--theia-ui-font-color1); + color: var(--theia-tab-font-color); background: var(--theia-layout-color2); font-size: var(--theia-ui-font-size1); - min-width: var(--theia-private-sidebar-tab-width); + min-width: var(--theia-private-sidebar-tab-width); max-width: var(--theia-private-sidebar-tab-width); } @@ -44,9 +45,12 @@ .p-TabBar.theia-app-sides .p-TabBar-tab { position: relative; - padding: 12px 8px; + padding: 11px 10px; background: var(--theia-layout-color2); flex-direction: column; + justify-content: center; + align-items: center; + min-height: var(--theia-private-sidebar-tab-height); } .p-TabBar.theia-app-left .p-TabBar-tab { @@ -58,8 +62,12 @@ } .p-TabBar.theia-app-sides .p-TabBar-tab.p-mod-current { - color: var(--theia-ui-font-color0); - background: var(--theia-layout-color0); + color: var(--theia-tab-font-color); + opacity: 1.0; + background: none; + min-height: var(--theia-private-sidebar-tab-height); + height: var(--theia-private-sidebar-tab-height); + border-top: none; } .p-TabBar.theia-app-left .p-TabBar-tab.p-mod-current.theia-mod-active { @@ -72,25 +80,40 @@ border-top-color: transparent; } -.p-TabBar.theia-app-sides .p-TabBar-tab:hover { - background: var(--theia-accent-color3); -} - -.p-TabBar.theia-app-sides .p-TabBar-tabIcon, +.p-TabBar.theia-app-sides .p-TabBar-tabLabel, .p-TabBar.theia-app-sides .p-TabBar-tabCloseIcon { display: none; } -.p-TabBar.theia-app-sides .p-TabBar-tabLabel { - position: absolute; - min-height: var(--theia-private-sidebar-tab-width); - max-height: var(--theia-private-sidebar-tab-width); - align-items: flex-start; +.p-TabBar.theia-app-sides .p-TabBar-tabIcon { + width: 30px; + height: 28px; + background: var(--theia-tab-icon-color); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; } -.p-TabBar.theia-app-sides .p-TabBar-tabIcon { - transform: scale(1.5); - max-height: 15px; +.p-TabBar.theia-app-sides .file-icon.p-TabBar-tabIcon:hover, +.p-TabBar.theia-app-sides .p-mod-current .file-icon.p-TabBar-tabIcon, +.p-TabBar.theia-app-sides .fa.p-TabBar-tabIcon:hover, +.p-TabBar.theia-app-sides .p-mod-current .fa.p-TabBar-tabIcon { + opacity: 1.0; + background: none; +} + +.p-TabBar.theia-app-sides .p-TabBar-tabIcon:hover, +.p-TabBar.theia-app-sides .p-mod-current .p-TabBar-tabIcon { + background: var(--theia-active-tab-icon-color); +} + +.p-TabBar.theia-app-sides .fa.p-TabBar-tabIcon { + display: flex; + align-items: center; + justify-content: center; + font-size: 23px; + opacity: 0.6; + text-align: center; + background: none; } .p-TabBar.theia-app-left .p-TabBar-tabLabel { @@ -173,3 +196,40 @@ .p-TabBar.theia-app-right > .theia-TabBar-hidden-content .p-TabBar-tabLabel { transform: none; } + +/*----------------------------------------------------------------------------- +| Sidepanel Toolbar +|----------------------------------------------------------------------------*/ + +.theia-sidepanel-toolbar { + min-height: 30px; + display: flex; + padding-left: 5px; + align-items: center; +} + +.theia-sidepanel-toolbar.theia-left-side-panel { + border-right: var(--theia-panel-border-width) solid var(--theia-border-color1); +} + +.theia-sidepanel-toolbar.theia-right-side-panel { + border-left: var(--theia-panel-border-width) solid var(--theia-border-color1); +} + +.theia-sidepanel-toolbar .theia-sidepanel-title { + color: var(--theia-ui-font-color2); + flex: 1; + margin-left: 14px; + text-transform: uppercase; + font-size: 10px; +} + +.theia-sidepanel-toolbar .p-TabBar-toolbar .item { + color: var(--theia-ui-font-color1); +} + +.theia-sidepanel-toolbar .p-TabBar-toolbar .item > div{ + height: 18px; + width: 18px; + background-repeat: no-repeat; +} diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css index 54d1953de9a0c..98dbf8f0dc5ef 100644 --- a/packages/core/src/browser/style/tabs.css +++ b/packages/core/src/browser/style/tabs.css @@ -90,10 +90,29 @@ } .p-TabBar .p-TabBar-tabIcon { - width: 15px; - line-height: 1.7; + width: 15px; + line-height: 1.7; + font-size: 12px; + text-align: center; + background-repeat: no-repeat; +} + +.p-TabBar.theia-app-centers .p-TabBar-tabIcon { + background-size: 13px; + background-position-y: 3px; + background: var(--theia-active-tab-icon-color); + -webkit-mask-repeat: no-repeat; + -webkit-mask-size: 13px; + -webkit-mask-position-y: 4px; + mask-repeat: no-repeat; + mask-size: 13px; + mask-position: 4px 0; } +.p-TabBar .file-icon.p-TabBar-tabIcon, +.p-TabBar .fa.p-TabBar-tabIcon { + background: none; +} .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon { padding-top: 6px; @@ -207,8 +226,17 @@ display: flex; align-items: center; margin-left: 8px; /* `padding` + `margin-right` from the container toolbar */ + opacity: 0.25; + cursor: default; } -.p-TabBar-toolbar .item:hover { - cursor: pointer; +.p-TabBar-toolbar .item.enabled { + opacity: 1.0; + cursor: pointer; +} + +.p-TabBar-toolbar .item > div{ + height: 18px; + width: 18px; + background-repeat: no-repeat; } diff --git a/packages/core/src/browser/style/variables-bright.useable.css b/packages/core/src/browser/style/variables-bright.useable.css index 8e32bdccaed9d..13a92bcd3b4cd 100644 --- a/packages/core/src/browser/style/variables-bright.useable.css +++ b/packages/core/src/browser/style/variables-bright.useable.css @@ -78,6 +78,11 @@ is not optimized for dense, information rich UIs. --theia-terminal-font-family: monospace; --theia-ui-padding: 6px; + /* Tab Icon Colors */ + --theia-active-tab-icon-color: rgb(0, 0, 0, 1.0); + --theia-tab-icon-color: rgb(0, 0, 0, 0.6); + --theia-tab-font-color: #000; + /* Main layout colors (bright to dark) ------------------------------------ */ diff --git a/packages/core/src/browser/style/variables-dark.useable.css b/packages/core/src/browser/style/variables-dark.useable.css index 63d9ffb8c46ec..f3b9d3a60703b 100644 --- a/packages/core/src/browser/style/variables-dark.useable.css +++ b/packages/core/src/browser/style/variables-dark.useable.css @@ -78,6 +78,11 @@ is not optimized for dense, information rich UIs. --theia-terminal-font-family: monospace; --theia-ui-padding: 6px; + /* Tab Colors */ + --theia-active-tab-icon-color: rgb(255, 255, 255, 1.0); + --theia-tab-icon-color: rgb(255, 255, 255, 0.6); + --theia-tab-font-color: #FFF; + /* Main layout colors (dark to bright) ------------------------------------ */ diff --git a/packages/debug/src/browser/style/debug-dark.svg b/packages/debug/src/browser/style/debug-dark.svg new file mode 100644 index 0000000000000..5c4141704fbfb --- /dev/null +++ b/packages/debug/src/browser/style/debug-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/debug/src/browser/style/index.css b/packages/debug/src/browser/style/index.css index d4303c3d41ec7..00dbf0767dfcd 100644 --- a/packages/debug/src/browser/style/index.css +++ b/packages/debug/src/browser/style/index.css @@ -152,8 +152,9 @@ opacity: 1; } -.debug-tab-icon::before { - content: "\f188" +.debug-tab-icon { + -webkit-mask: url('debug-dark.svg') 50%; + mask: url('debug-dark.svg') 50%; } /** Console */ diff --git a/packages/debug/src/browser/view/debug-widget.ts b/packages/debug/src/browser/view/debug-widget.ts index 1a527c6452cb2..6e7b07fb5b728 100644 --- a/packages/debug/src/browser/view/debug-widget.ts +++ b/packages/debug/src/browser/view/debug-widget.ts @@ -55,7 +55,7 @@ export class DebugWidget extends BaseWidget implements ApplicationShell.Trackabl this.title.label = DebugWidget.LABEL; this.title.caption = DebugWidget.LABEL; this.title.closable = true; - this.title.iconClass = 'fa debug-tab-icon'; + this.title.iconClass = 'debug-tab-icon'; this.addClass('theia-debug-container'); this.toDispose.pushAll([ this.toolbar, diff --git a/packages/extension-manager/src/browser/extension-widget.tsx b/packages/extension-manager/src/browser/extension-widget.tsx index e7dc37afb9e39..024901072fddd 100644 --- a/packages/extension-manager/src/browser/extension-widget.tsx +++ b/packages/extension-manager/src/browser/extension-widget.tsx @@ -41,7 +41,7 @@ export class ExtensionWidget extends ReactWidget { this.id = 'extensions'; this.title.label = 'Extensions'; this.title.caption = 'Extensions'; - this.title.iconClass = 'fa extensions-tab-icon'; + this.title.iconClass = 'extensions-tab-icon'; this.addClass('theia-extensions'); this.update(); diff --git a/packages/extension-manager/src/browser/style/extension-sidebar.css b/packages/extension-manager/src/browser/style/extension-sidebar.css index 4121d246f54cf..5876c65044226 100644 --- a/packages/extension-manager/src/browser/style/extension-sidebar.css +++ b/packages/extension-manager/src/browser/style/extension-sidebar.css @@ -94,6 +94,7 @@ flex: 3; } -.extensions-tab-icon::before { - content: "\f12e" +.extensions-tab-icon { + -webkit-mask: url('extensions.svg'); + mask: url('extensions.svg'); } diff --git a/packages/extension-manager/src/browser/style/extensions.svg b/packages/extension-manager/src/browser/style/extensions.svg new file mode 100644 index 0000000000000..b7cd62ad53907 --- /dev/null +++ b/packages/extension-manager/src/browser/style/extensions.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/git/src/browser/git-widget.tsx b/packages/git/src/browser/git-widget.tsx index 2d38acca26380..c8dcd2f571104 100644 --- a/packages/git/src/browser/git-widget.tsx +++ b/packages/git/src/browser/git-widget.tsx @@ -80,7 +80,7 @@ export class GitWidget extends GitDiffWidget implements StatefulWidget { this.id = 'theia-gitContainer'; this.title.label = 'Git'; this.title.caption = 'Git'; - this.title.iconClass = 'fa git-tab-icon'; + this.title.iconClass = 'git-tab-icon'; this.scrollContainer = GitWidget.Styles.CHANGES_CONTAINER; this.addClass('theia-git'); this.node.tabIndex = 0; diff --git a/packages/git/src/browser/style/git.svg b/packages/git/src/browser/style/git.svg new file mode 100644 index 0000000000000..9280ae1dbf89d --- /dev/null +++ b/packages/git/src/browser/style/git.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/git/src/browser/style/index.css b/packages/git/src/browser/style/index.css index 18dfd485bd2f6..abb4d23a52478 100644 --- a/packages/git/src/browser/style/index.css +++ b/packages/git/src/browser/style/index.css @@ -326,8 +326,9 @@ outline: none; } -.git-tab-icon::before { - content: "\f126" +.git-tab-icon { + -webkit-mask: url('git.svg'); + mask: url('git.svg'); } .git-change-count { diff --git a/packages/navigator/src/browser/navigator-contribution.ts b/packages/navigator/src/browser/navigator-contribution.ts index 5d3a5a6c70378..98029a68571d5 100644 --- a/packages/navigator/src/browser/navigator-contribution.ts +++ b/packages/navigator/src/browser/navigator-contribution.ts @@ -91,7 +91,7 @@ export class FileNavigatorContribution extends AbstractViewContribution + + + diff --git a/packages/navigator/src/browser/style/index.css b/packages/navigator/src/browser/style/index.css index f8c6dca6616f1..4b09504a4e0fe 100644 --- a/packages/navigator/src/browser/style/index.css +++ b/packages/navigator/src/browser/style/index.css @@ -45,6 +45,7 @@ width: calc(100% - var(--theia-ui-padding)*4); } -.navigator-tab-icon::before { - content: "\f0c5" +.navigator-tab-icon { + -webkit-mask: url('files.svg'); + mask: url('files.svg'); } diff --git a/packages/plugin-ext/src/main/browser/view/view-registry.ts b/packages/plugin-ext/src/main/browser/view/view-registry.ts index 8821a3750faab..9710f741ed3ab 100644 --- a/packages/plugin-ext/src/main/browser/view/view-registry.ts +++ b/packages/plugin-ext/src/main/browser/view/view-registry.ts @@ -49,6 +49,7 @@ export class ViewRegistry { if (this.containerWidgets.has(viewsContainer.id)) { return; } + const containerWidget = new ViewsContainerWidget(viewsContainer, containerViews); this.containerWidgets.set(viewsContainer.id, containerWidget); diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts index ab420767eb567..70077aae7d054 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts @@ -18,12 +18,14 @@ import { AbstractViewContribution, KeybindingRegistry, LabelProvider, CommonMenu import { SearchInWorkspaceWidget } from './search-in-workspace-widget'; import { injectable, inject, postConstruct } from 'inversify'; import { CommandRegistry, MenuModelRegistry, SelectionService, Command } from '@theia/core'; +import { Widget } from '@theia/core/lib/browser/widgets'; import { NavigatorContextMenu } from '@theia/navigator/lib/browser/navigator-contribution'; import { UriCommandHandler, UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import URI from '@theia/core/lib/common/uri'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { FileSystem } from '@theia/filesystem/lib/common'; import { SearchInWorkspaceContextKeyService } from './search-in-workspace-context-key-service'; +import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; export namespace SearchInWorkspaceCommands { const SEARCH_CATEGORY = 'Search'; @@ -41,10 +43,28 @@ export namespace SearchInWorkspaceCommands { category: SEARCH_CATEGORY, label: 'Find in Folder' }; + export const REFRESH_RESULTS: Command = { + id: 'search-in-workspace.refresh', + category: SEARCH_CATEGORY, + label: 'Refresh', + iconClass: 'refresh' + }; + export const COLLAPSE_ALL: Command = { + id: 'search-in-workspace.collapse-all', + category: SEARCH_CATEGORY, + label: 'Collapse All', + iconClass: 'collapse-all' + }; + export const CLEAR_ALL: Command = { + id: 'search-in-workspace.clear-all', + category: SEARCH_CATEGORY, + label: 'Clear All', + iconClass: 'clear-all' + }; } @injectable() -export class SearchInWorkspaceFrontendContribution extends AbstractViewContribution implements FrontendApplicationContribution { +export class SearchInWorkspaceFrontendContribution extends AbstractViewContribution implements FrontendApplicationContribution, TabBarToolbarContribution { @inject(SelectionService) protected readonly selectionService: SelectionService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @@ -78,7 +98,7 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut await this.openView({ activate: false }); } - registerCommands(commands: CommandRegistry): void { + async registerCommands(commands: CommandRegistry): Promise { super.registerCommands(commands); commands.registerCommand(SearchInWorkspaceCommands.OPEN_SIW_WIDGET, { isEnabled: () => this.workspaceService.tryGetRoots().length > 0, @@ -104,10 +124,33 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut } } }); - const widget: SearchInWorkspaceWidget = await this.openView({ activate: true }); + const widget = await this.openView({ activate: true }); widget.findInFolder(resources); } })); + + commands.registerCommand(SearchInWorkspaceCommands.REFRESH_RESULTS, { + execute: w => this.withWidget(w, widget => widget.refresh()), + isEnabled: w => this.withWidget(w, widget => (widget.hasResultList() || widget.hasSearchTerm()) && this.workspaceService.tryGetRoots().length > 0), + isVisible: w => this.withWidget(w, () => true) + }); + commands.registerCommand(SearchInWorkspaceCommands.COLLAPSE_ALL, { + execute: w => this.withWidget(w, widget => widget.collapseAll()), + isEnabled: w => this.withWidget(w, widget => widget.hasResultList()), + isVisible: w => this.withWidget(w, () => true) + }); + commands.registerCommand(SearchInWorkspaceCommands.CLEAR_ALL, { + execute: w => this.withWidget(w, widget => widget.clear()), + isEnabled: w => this.withWidget(w, widget => widget.hasResultList()), + isVisible: w => this.withWidget(w, () => true) + }); + } + + protected withWidget(widget: Widget | undefined = this.tryGetWidget(), fn: (widget: SearchInWorkspaceWidget) => T): T | false { + if (widget instanceof SearchInWorkspaceWidget && widget.id === SearchInWorkspaceWidget.ID) { + return fn(widget); + } + return false; } registerKeybindings(keybindings: KeybindingRegistry): void { @@ -128,6 +171,29 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut }); } + async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise { + const widget = await this.widget; + const onDidChange = widget.onDidUpdate; + toolbarRegistry.registerItem({ + id: SearchInWorkspaceCommands.REFRESH_RESULTS.id, + command: SearchInWorkspaceCommands.REFRESH_RESULTS.id, + priority: 0, + onDidChange + }); + toolbarRegistry.registerItem({ + id: SearchInWorkspaceCommands.CLEAR_ALL.id, + command: SearchInWorkspaceCommands.CLEAR_ALL.id, + priority: 1, + onDidChange + }); + toolbarRegistry.registerItem({ + id: SearchInWorkspaceCommands.COLLAPSE_ALL.id, + command: SearchInWorkspaceCommands.COLLAPSE_ALL.id, + priority: 2, + onDidChange + }); + } + protected newUriAwareCommandHandler(handler: UriCommandHandler): UriAwareCommandHandler { return new UriAwareCommandHandler(this.selectionService, handler); } diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts index 33fa6f6a28ccd..1261d4565b679 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts @@ -26,6 +26,7 @@ import { SearchInWorkspaceResultTreeWidget } from './search-in-workspace-result- import { SearchInWorkspaceFrontendContribution } from './search-in-workspace-frontend-contribution'; import { InMemoryTextResourceResolver } from './in-memory-text-resource'; import { SearchInWorkspaceContextKeyService } from './search-in-workspace-context-key-service'; +import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; export default new ContainerModule(bind => { bind(SearchInWorkspaceContextKeyService).toSelf().inSingletonScope(); @@ -39,6 +40,7 @@ export default new ContainerModule(bind => { bindViewContribution(bind, SearchInWorkspaceFrontendContribution); bind(FrontendApplicationContribution).toService(SearchInWorkspaceFrontendContribution); + bind(TabBarToolbarContribution).toService(SearchInWorkspaceFrontendContribution); // The object that gets notified of search results. bind(SearchInWorkspaceClientImpl).toSelf().inSingletonScope(); diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx index 2fe771015766e..0222d44c54d9b 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx +++ b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx @@ -20,7 +20,7 @@ import { SearchInWorkspaceResultTreeWidget } from './search-in-workspace-result- import { SearchInWorkspaceOptions } from '../common/search-in-workspace-interface'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { Disposable } from '@theia/core/lib/common'; +import { Event, Emitter, Disposable } from '@theia/core/lib/common'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { SearchInWorkspaceContextKeyService } from './search-in-workspace-context-key-service'; @@ -72,6 +72,9 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge protected searchFormContainer: HTMLElement; protected resultContainer: HTMLElement; + protected readonly onDidUpdateEmitter = new Emitter(); + readonly onDidUpdate: Event = this.onDidUpdateEmitter.event; + @inject(SearchInWorkspaceResultTreeWidget) protected readonly resultTreeWidget: SearchInWorkspaceResultTreeWidget; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @@ -83,7 +86,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge this.id = SearchInWorkspaceWidget.ID; this.title.label = SearchInWorkspaceWidget.LABEL; this.title.caption = SearchInWorkspaceWidget.LABEL; - this.title.iconClass = 'fa search-in-workspace-tab-icon'; + this.title.iconClass = 'search-in-workspace-tab-icon'; this.contentNode = document.createElement('div'); this.contentNode.classList.add('t-siw-search-container'); @@ -180,6 +183,47 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge this.update(); } + hasResultList(): boolean { + return this.hasResults; + } + + hasSearchTerm(): boolean { + return this.searchTerm !== ''; + } + + refresh(): void { + this.resultTreeWidget.search(this.searchTerm, this.searchInWorkspaceOptions); + this.update(); + } + + collapseAll(): void { + this.resultTreeWidget.collapseAll(); + this.update(); + } + + clear(): void { + this.searchTerm = ''; + this.replaceTerm = ''; + this.searchInWorkspaceOptions.include = []; + this.searchInWorkspaceOptions.exclude = []; + this.includeIgnoredState.enabled = false; + this.matchCaseState.enabled = false; + this.wholeWordState.enabled = false; + this.regExpState.enabled = false; + const search = document.getElementById('search-input-field'); + const replace = document.getElementById('replace-input-field'); + const include = document.getElementById('include-glob-field'); + const exclude = document.getElementById('exclude-glob-field'); + if (search && replace && include && exclude) { + (search as HTMLInputElement).value = ''; + (replace as HTMLInputElement).value = ''; + (include as HTMLInputElement).value = ''; + (exclude as HTMLInputElement).value = ''; + } + this.resultTreeWidget.search(this.searchTerm, this.searchInWorkspaceOptions); + this.update(); + } + protected onAfterAttach(msg: Message): void { super.onAfterAttach(msg); ReactDOM.render({this.renderSearchHeader()}{this.renderSearchInfo()}, this.searchFormContainer); @@ -192,6 +236,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge protected onUpdateRequest(msg: Message): void { super.onUpdateRequest(msg); ReactDOM.render({this.renderSearchHeader()}{this.renderSearchInfo()}, this.searchFormContainer); + this.onDidUpdateEmitter.fire(undefined); } protected onResize(msg: Widget.ResizeMessage): void { @@ -224,55 +269,9 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge } protected renderSearchHeader(): React.ReactNode { - const controlButtons = this.renderControlButtons(); const searchAndReplaceContainer = this.renderSearchAndReplace(); const searchDetails = this.renderSearchDetails(); - return
{controlButtons}{searchAndReplaceContainer}{searchDetails}
; - } - - protected refresh = () => { - this.resultTreeWidget.search(this.searchTerm, this.searchInWorkspaceOptions); - this.update(); - } - - protected collapseAll = () => { - this.resultTreeWidget.collapseAll(); - this.update(); - } - - protected clear = () => { - this.searchTerm = ''; - this.replaceTerm = ''; - this.searchInWorkspaceOptions.include = []; - this.searchInWorkspaceOptions.exclude = []; - this.includeIgnoredState.enabled = false; - this.matchCaseState.enabled = false; - this.wholeWordState.enabled = false; - this.regExpState.enabled = false; - const search = document.getElementById('search-input-field'); - const replace = document.getElementById('replace-input-field'); - const include = document.getElementById('include-glob-field'); - const exclude = document.getElementById('exclude-glob-field'); - if (search && replace && include && exclude) { - (search as HTMLInputElement).value = ''; - (replace as HTMLInputElement).value = ''; - (include as HTMLInputElement).value = ''; - (exclude as HTMLInputElement).value = ''; - } - this.resultTreeWidget.search(this.searchTerm, this.searchInWorkspaceOptions); - this.update(); - } - - protected renderControlButtons(): React.ReactNode { - const refreshButton = this.renderControlButton(`refresh${(this.hasResults || this.searchTerm !== '') && this.workspaceService.tryGetRoots().length > 0 - ? ' enabled' : ''}`, 'Refresh', this.refresh); - const collapseAllButton = this.renderControlButton(`collapse-all${this.hasResults ? ' enabled' : ''}`, 'Collapse All', this.collapseAll); - const clearButton = this.renderControlButton(`clear-all${this.hasResults ? ' enabled' : ''}`, 'Clear', this.clear); - return
{refreshButton}{collapseAllButton}{clearButton}
; - } - - protected renderControlButton(btnClass: string, title: string, clickHandler: () => void): React.ReactNode { - return ; + return
{searchAndReplaceContainer}{searchDetails}
; } protected renderSearchAndReplace(): React.ReactNode { diff --git a/packages/search-in-workspace/src/browser/styles/index.css b/packages/search-in-workspace/src/browser/styles/index.css index 855d7c193c5c9..7866b486231a0 100644 --- a/packages/search-in-workspace/src/browser/styles/index.css +++ b/packages/search-in-workspace/src/browser/styles/index.css @@ -21,7 +21,7 @@ .t-siw-search-container { color: var(--theia-ui-font-color1); - padding: 5px; + padding: 1px 5px; display: flex; flex-direction: column; height: 100%; @@ -59,16 +59,16 @@ margin-bottom: 5px; } -.t-siw-search-container .searchHeader .controls .refresh { - background: var(--theia-icon-refresh); +.p-TabBar-toolbar .item .refresh { + background: var(--theia-icon-refresh) no-repeat; } -.t-siw-search-container .searchHeader .controls .collapse-all { - background: var(--theia-icon-collapse-all); +.p-TabBar-toolbar .item .collapse-all { + background: var(--theia-icon-collapse-all) no-repeat; } -.t-siw-search-container .searchHeader .controls .clear-all { - background: var(--theia-icon-clear); +.p-TabBar-toolbar .item .clear-all { + background: var(--theia-icon-clear) no-repeat; } .t-siw-search-container .searchHeader .search-field-container { @@ -181,26 +181,10 @@ justify-content: center; } -.t-siw-search-container .searchHeader .controls .btn{ - margin-left: 3px; - opacity: 0.25; - width: 18px -} - -.t-siw-search-container .searchHeader .controls .btn.enabled{ - opacity: 0.7; - cursor: pointer; -} - -.t-siw-search-container .searchHeader .controls .btn.enabled:hover{ - opacity: 1; -} - .t-siw-search-container .searchHeader .search-details .button-container { height: 5px; } - .t-siw-search-container .searchHeader .search-details .button-container .btn{ cursor: pointer; } @@ -388,8 +372,9 @@ opacity: 0.5; } -.search-in-workspace-tab-icon::before { - content: "\f002" +.search-in-workspace-tab-icon { + -webkit-mask: url('search.svg'); + mask: url('search.svg'); } .highlighted-count-container { diff --git a/packages/search-in-workspace/src/browser/styles/search.svg b/packages/search-in-workspace/src/browser/styles/search.svg new file mode 100644 index 0000000000000..5b8c2af051798 --- /dev/null +++ b/packages/search-in-workspace/src/browser/styles/search.svg @@ -0,0 +1,6 @@ + + + + + +