diff --git a/CHANGELOG.md b/CHANGELOG.md index 5019dea70..9cc81ef6b 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/examples/browser/test/left-panel/left-panel.ts b/examples/browser/test/left-panel/left-panel.ts index 3349c6377..1e9b9c9de 100644 --- a/examples/browser/test/left-panel/left-panel.ts +++ b/examples/browser/test/left-panel/left-panel.ts @@ -31,7 +31,7 @@ export class LeftPanel { } openCloseTab(tabName: string) { - this.driver.element('.p-TabBar.theia-app-left .p-TabBar-content').click(`div=${tabName}`); + this.driver.element('.p-TabBar.theia-app-left .p-TabBar-content').element(`div=${tabName}`).click('..'); // Wait for animations to finish this.driver.pause(300); } diff --git a/examples/browser/test/left-panel/left-panel.ui-spec.ts b/examples/browser/test/left-panel/left-panel.ui-spec.ts index c79079416..7bca29439 100644 --- a/examples/browser/test/left-panel/left-panel.ui-spec.ts +++ b/examples/browser/test/left-panel/left-panel.ui-spec.ts @@ -33,20 +33,20 @@ before(() => { }); describe('theia left panel', () => { - it("should show 'Files' and 'Git'", () => { - expect(leftPanel.doesTabExist('Files')).to.be.true; + it("should show 'Explorer' and 'Git'", () => { + expect(leftPanel.doesTabExist('Explorer')).to.be.true; expect(leftPanel.doesTabExist('Git')).to.be.true; }); describe('files tab', () => { it('should open/close the files tab', () => { - leftPanel.openCloseTab('Files'); + leftPanel.openCloseTab('Explorer'); expect(leftPanel.isFileTreeVisible()).to.be.true; - expect(leftPanel.isTabActive('Files')).to.be.true; + expect(leftPanel.isTabActive('Explorer')).to.be.true; - leftPanel.openCloseTab('Files'); + leftPanel.openCloseTab('Explorer'); expect(leftPanel.isFileTreeVisible()).to.be.false; - expect(leftPanel.isTabActive('Files')).to.be.false; + expect(leftPanel.isTabActive('Explorer')).to.be.false; }); }); diff --git a/examples/browser/test/right-panel/right-panel.ts b/examples/browser/test/right-panel/right-panel.ts index 6a207ea8e..7a8b2ed96 100644 --- a/examples/browser/test/right-panel/right-panel.ts +++ b/examples/browser/test/right-panel/right-panel.ts @@ -31,7 +31,7 @@ export class RightPanel { } openCloseTab(tabName: string) { - this.driver.element('.p-TabBar.theia-app-right .p-TabBar-content').click(`div=${tabName}`); + this.driver.element('.p-TabBar.theia-app-right .p-TabBar-content').element(`div=${tabName}`).click('..'); // Wait for animations to finish this.driver.pause(300); } diff --git a/examples/browser/test/top-panel/top-panel.ts b/examples/browser/test/top-panel/top-panel.ts index 5cd2bf26a..0d6aa6c09 100644 --- a/examples/browser/test/top-panel/top-panel.ts +++ b/examples/browser/test/top-panel/top-panel.ts @@ -41,7 +41,7 @@ export class TopPanel { toggleFilesView() { this.clickMenuTab('View'); - this.clickSubMenu('Files'); + this.clickSubMenu('Explorer'); } toggleGitView() { diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 052e5f74e..eeff9e95a 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 29bd26ad3..9449c8c6b 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 000000000..8a920b41c --- /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 2c411ec38..46f3136cf 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 e157f967f..11cf06f23 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -309,6 +309,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 b6c0e9c93..21d3f9a31 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 32ee05c42..56138e39c 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,13 @@ .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); + cursor: pointer; } .p-TabBar.theia-app-left .p-TabBar-tab { @@ -58,8 +63,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 +81,42 @@ 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-color: var(--theia-tab-icon-color); + opacity: 0.6; + 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 { + opacity: 1.0; +} + +.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; + color: var(--theia-tab-icon-color); } .p-TabBar.theia-app-left .p-TabBar-tabLabel { @@ -173,3 +199,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-color1); + flex: 1; + margin-left: 14px; + text-transform: uppercase; + font-size: var(--theia-ui-font-size0); +} + +.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 54d1953de..5fb8ac701 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-color: var(--theia-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 321d90e12..8362b1d90 100644 --- a/packages/core/src/browser/style/variables-bright.useable.css +++ b/packages/core/src/browser/style/variables-bright.useable.css @@ -78,6 +78,10 @@ is not optimized for dense, information rich UIs. --theia-terminal-font-family: Inconsolata, "Source Code Pro", Consolas, monospace; --theia-ui-padding: 6px; + /* Tab Icon Colors */ + --theia-tab-icon-color: var(--theia-ui-font-color1); + --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 d1f071319..6988b81dc 100644 --- a/packages/core/src/browser/style/variables-dark.useable.css +++ b/packages/core/src/browser/style/variables-dark.useable.css @@ -78,6 +78,10 @@ is not optimized for dense, information rich UIs. --theia-terminal-font-family: Inconsolata, "Source Code Pro", Consolas, monospace; --theia-ui-padding: 6px; + /* Tab Colors */ + --theia-tab-icon-color: rgb(255, 255, 255); + --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 000000000..5c4141704 --- /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 d4303c3d4..00dbf0767 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 b0170abde..2ea80f3ee 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 d3c16e886..f5fdc8713 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 4121d246f..5876c6504 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 000000000..b7cd62ad5 --- /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 80fc3103b..d7442088c 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 000000000..9280ae1db --- /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 18dfd485b..abb4d23a5 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 55411adca..b5b271a92 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 f8c6dca66..4b09504a4 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 698c97420..7e7a922c7 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 9fc95db2c..3b170bf10 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 @@ -17,13 +17,15 @@ import { AbstractViewContribution, KeybindingRegistry, LabelProvider, CommonMenus, FrontendApplication, FrontendApplicationContribution } from '@devpodio/core/lib/browser'; import { SearchInWorkspaceWidget } from './search-in-workspace-widget'; import { injectable, inject, postConstruct } from 'inversify'; -import { CommandRegistry, MenuModelRegistry, SelectionService, Command } from '@devpodio/core'; -import { NavigatorContextMenu } from '@devpodio/navigator/lib/browser/navigator-contribution'; -import { UriCommandHandler, UriAwareCommandHandler } from '@devpodio/core/lib/common/uri-command-handler'; -import URI from '@devpodio/core/lib/common/uri'; -import { WorkspaceService } from '@devpodio/workspace/lib/browser'; -import { FileSystem } from '@devpodio/filesystem/lib/common'; +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 61bdbbfad..19eb01603 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 b8d00cd69..086862b95 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,8 +20,8 @@ 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 '@devpodio/core/lib/common'; -import { WorkspaceService } from '@devpodio/workspace/lib/browser'; +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'; export interface SearchFieldState { @@ -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 855d7c193..7866b4862 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 000000000..5b8c2af05 --- /dev/null +++ b/packages/search-in-workspace/src/browser/styles/search.svg @@ -0,0 +1,6 @@ + + + + + +