From 71a0433fa5ceef064d948b18ad9991f5df990dd5 Mon Sep 17 00:00:00 2001 From: Jan Bicker Date: Mon, 18 Mar 2019 10:34:29 +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 --- .../browser/frontend-application-module.ts | 4 +- .../core/src/browser/icons/debug-dark.svg | 4 + .../src/browser/icons/extensions-dark.svg | 4 + .../core/src/browser/icons/files-dark.svg | 4 + packages/core/src/browser/icons/scm-dark.svg | 4 + .../core/src/browser/icons/search-dark.svg | 6 ++ .../src/browser/shell/side-panel-handler.ts | 33 ++++++- .../src/browser/shell/side-panel-toolbar.ts | 91 ++++++++++++++++++ .../src/browser/shell/tab-bar-toolbar.tsx | 16 +++- packages/core/src/browser/style/sidepanel.css | 63 +++++++++--- packages/core/src/browser/style/tabs.css | 25 ++++- .../style/variables-bright.useable.css | 7 ++ .../browser/style/variables-dark.useable.css | 7 ++ packages/core/src/browser/widgets/widget.ts | 15 ++- packages/debug/src/browser/style/index.css | 4 +- .../src/browser/style/extension-sidebar.css | 4 +- packages/git/src/browser/style/index.css | 4 +- .../navigator/src/browser/style/index.css | 4 +- ...arch-in-workspace-frontend-contribution.ts | 62 +++++++++++- .../search-in-workspace-frontend-module.ts | 2 + .../browser/search-in-workspace-widget.tsx | 96 ++++++++++--------- .../src/browser/styles/index.css | 34 ++----- 22 files changed, 383 insertions(+), 110 deletions(-) create mode 100644 packages/core/src/browser/icons/debug-dark.svg create mode 100644 packages/core/src/browser/icons/extensions-dark.svg create mode 100644 packages/core/src/browser/icons/files-dark.svg create mode 100644 packages/core/src/browser/icons/scm-dark.svg create mode 100644 packages/core/src/browser/icons/search-dark.svg create mode 100644 packages/core/src/browser/shell/side-panel-toolbar.ts diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 4edb14bf7517f..6c6de0a0c68e7 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/icons/debug-dark.svg b/packages/core/src/browser/icons/debug-dark.svg new file mode 100644 index 0000000000000..9e40099593ab8 --- /dev/null +++ b/packages/core/src/browser/icons/debug-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core/src/browser/icons/extensions-dark.svg b/packages/core/src/browser/icons/extensions-dark.svg new file mode 100644 index 0000000000000..4b521cc29960a --- /dev/null +++ b/packages/core/src/browser/icons/extensions-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core/src/browser/icons/files-dark.svg b/packages/core/src/browser/icons/files-dark.svg new file mode 100644 index 0000000000000..6fdff23f05eb3 --- /dev/null +++ b/packages/core/src/browser/icons/files-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core/src/browser/icons/scm-dark.svg b/packages/core/src/browser/icons/scm-dark.svg new file mode 100644 index 0000000000000..a39ce0be7ed88 --- /dev/null +++ b/packages/core/src/browser/icons/scm-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core/src/browser/icons/search-dark.svg b/packages/core/src/browser/icons/search-dark.svg new file mode 100644 index 0000000000000..7307dcaaf1e38 --- /dev/null +++ b/packages/core/src/browser/icons/search-dark.svg @@ -0,0 +1,6 @@ + + + + + + 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..1b07f8eb650e2 --- /dev/null +++ b/packages/core/src/browser/shell/side-panel-toolbar.ts @@ -0,0 +1,91 @@ +/******************************************************************************** + * Copyright (C) 2018 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 "@theia/core/src/browser/shell/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(); + } + + 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) : []; + items.forEach(i => i.onDidChange && i.onDidChange(() => { + this.update(); + })); + 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..4a88800fa9c46 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar.tsx +++ b/packages/core/src/browser/shell/tab-bar-toolbar.tsx @@ -80,15 +80,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) { + return this.commands.isEnabled(command, this.current); + } + protected executeCommand = (e: React.MouseEvent) => { const item = this.items.get(e.currentTarget.id); if (item) { @@ -174,6 +180,8 @@ export interface TabBarToolbarItem { */ readonly when?: string; + onDidChange?: (handler: () => void) => void; + } export namespace TabBarToolbarItem { diff --git a/packages/core/src/browser/style/sidepanel.css b/packages/core/src/browser/style/sidepanel.css index 32ee05c42dd82..69bb889b31d51 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; } @@ -34,7 +35,7 @@ color: var(--theia-ui-font-color1); 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 { @@ -60,6 +64,9 @@ .p-TabBar.theia-app-sides .p-TabBar-tab.p-mod-current { color: var(--theia-ui-font-color0); background: var(--theia-layout-color0); + 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 { @@ -76,21 +83,21 @@ 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 { - transform: scale(1.5); - max-height: 15px; + width: 30px; + font-size: 23px; + text-align: center; + height: 28px; + background-repeat: no-repeat; + display: flex; + align-items: center; + justify-content: center; + background-size: 26px } .p-TabBar.theia-app-left .p-TabBar-tabLabel { @@ -173,3 +180,33 @@ .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; +} + +.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..eea2a58ef3c6f 100644 --- a/packages/core/src/browser/style/tabs.css +++ b/packages/core/src/browser/style/tabs.css @@ -90,8 +90,16 @@ } .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; } @@ -207,8 +215,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..99cc15b12ad3b 100644 --- a/packages/core/src/browser/style/variables-bright.useable.css +++ b/packages/core/src/browser/style/variables-bright.useable.css @@ -182,6 +182,13 @@ is not optimized for dense, information rich UIs. --theia-icon-replace-all: url(../icons/replace-all.svg); --theia-icon-open-file: url(../icons/open-file-bright.svg); + /* Sidebar Icons */ + --theia-icon-debug: url(../icons/debug-dark.svg); + --theia-icon-extensions: url(../icons/extensions-dark.svg); + --theia-icon-files: url(../icons/files-dark.svg); + --theia-icon-scm: url(../icons/scm-dark.svg); + --theia-icon-search: url(../icons/search-dark.svg); + /* Scrollbars */ --theia-scrollbar-width: 6px; --theia-scrollbar-rail-width: 10px; diff --git a/packages/core/src/browser/style/variables-dark.useable.css b/packages/core/src/browser/style/variables-dark.useable.css index 63d9ffb8c46ec..ece260b50002b 100644 --- a/packages/core/src/browser/style/variables-dark.useable.css +++ b/packages/core/src/browser/style/variables-dark.useable.css @@ -182,6 +182,13 @@ is not optimized for dense, information rich UIs. --theia-icon-replace-all: url(../icons/replace-all-inverse.svg); --theia-icon-open-file: url(../icons/open-file-dark.svg); + /* Sidebar Icons */ + --theia-icon-debug: url(../icons/debug-dark.svg); + --theia-icon-extensions: url(../icons/extensions-dark.svg); + --theia-icon-files: url(../icons/files-dark.svg); + --theia-icon-scm: url(../icons/scm-dark.svg); + --theia-icon-search: url(../icons/search-dark.svg); + /* Scrollbars */ --theia-scrollbar-width: 6px; --theia-scrollbar-rail-width: 10px; diff --git a/packages/core/src/browser/widgets/widget.ts b/packages/core/src/browser/widgets/widget.ts index 08819d5ffdd5f..4e64586503d19 100644 --- a/packages/core/src/browser/widgets/widget.ts +++ b/packages/core/src/browser/widgets/widget.ts @@ -14,10 +14,10 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, decorate, unmanaged } from 'inversify'; +import { injectable, decorate, unmanaged, postConstruct } from 'inversify'; import { Widget } from '@phosphor/widgets'; import { Message } from '@phosphor/messaging'; -import { Disposable, DisposableCollection, MaybePromise } from '../../common'; +import { Disposable, DisposableCollection, MaybePromise, Emitter } from '../../common'; import { KeyCode, KeysOrKeyCodes } from '../keys'; import PerfectScrollbar from 'perfect-scrollbar'; @@ -42,6 +42,13 @@ export class BaseWidget extends Widget { protected scrollBar?: PerfectScrollbar; protected scrollOptions?: PerfectScrollbar.Options; + protected updateEmitter = new Emitter(); + + @postConstruct() + protected init(){ + this.toDispose.push(this.updateEmitter); + } + dispose(): void { if (this.isDisposed) { return; @@ -50,6 +57,10 @@ export class BaseWidget extends Widget { this.toDispose.dispose(); } + onUpdate(handler: () => void): void { + + } + protected onCloseRequest(msg: Message): void { super.onCloseRequest(msg); this.dispose(); diff --git a/packages/debug/src/browser/style/index.css b/packages/debug/src/browser/style/index.css index d4303c3d41ec7..13caa1470ecc2 100644 --- a/packages/debug/src/browser/style/index.css +++ b/packages/debug/src/browser/style/index.css @@ -152,8 +152,8 @@ opacity: 1; } -.debug-tab-icon::before { - content: "\f188" +.debug-tab-icon { + background: var(--theia-icon-debug) 50%; } /** Console */ diff --git a/packages/extension-manager/src/browser/style/extension-sidebar.css b/packages/extension-manager/src/browser/style/extension-sidebar.css index 4121d246f54cf..173bb4bb864a8 100644 --- a/packages/extension-manager/src/browser/style/extension-sidebar.css +++ b/packages/extension-manager/src/browser/style/extension-sidebar.css @@ -94,6 +94,6 @@ flex: 3; } -.extensions-tab-icon::before { - content: "\f12e" +.extensions-tab-icon { + background: var(--theia-icon-extensions); } diff --git a/packages/git/src/browser/style/index.css b/packages/git/src/browser/style/index.css index 18dfd485bd2f6..039751fb08a1d 100644 --- a/packages/git/src/browser/style/index.css +++ b/packages/git/src/browser/style/index.css @@ -326,8 +326,8 @@ outline: none; } -.git-tab-icon::before { - content: "\f126" +.git-tab-icon { + background: var(--theia-icon-scm); } .git-change-count { diff --git a/packages/navigator/src/browser/style/index.css b/packages/navigator/src/browser/style/index.css index f8c6dca6616f1..1d6478336c2f4 100644 --- a/packages/navigator/src/browser/style/index.css +++ b/packages/navigator/src/browser/style/index.css @@ -45,6 +45,6 @@ width: calc(100% - var(--theia-ui-padding)*4); } -.navigator-tab-icon::before { - content: "\f0c5" +.navigator-tab-icon { + background: var(--theia-icon-files); } 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..b78d5bb614b44 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 @@ -24,6 +24,7 @@ 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/src/browser/shell/tab-bar-toolbar'; export namespace SearchInWorkspaceCommands { const SEARCH_CATEGORY = 'Search'; @@ -41,10 +42,25 @@ export namespace SearchInWorkspaceCommands { category: SEARCH_CATEGORY, label: 'Find in Folder' }; + export const REFRESH_RESULTS: Command = { + id: 'search-in-workspace.refresh', + label: 'Refresh', + iconClass: 'refresh' + }; + export const COLLAPSE_ALL: Command = { + id: 'search-in-workspace.collapse-all', + label: 'Collapse All', + iconClass: 'collapse-all' + }; + export const CLEAR_ALL: Command = { + id: 'search-in-workspace.clear-all', + 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; @@ -54,6 +70,8 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut @inject(SearchInWorkspaceContextKeyService) protected readonly contextKeyService: SearchInWorkspaceContextKeyService; + protected searchInWorkspaceWidget: SearchInWorkspaceWidget; + constructor() { super({ widgetId: SearchInWorkspaceWidget.ID, @@ -72,6 +90,10 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut this.contextKeyService.searchViewletFocus.set(this.shell.activeWidget instanceof SearchInWorkspaceWidget); updateFocusContextKey(); this.shell.activeChanged.connect(updateFocusContextKey); + this.widget.then(w => { + this.searchInWorkspaceWidget = w; + w.update(); + }); } async initializeLayout(app: FrontendApplication): Promise { @@ -108,6 +130,28 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut widget.findInFolder(resources); } })); + + commands.registerCommand(SearchInWorkspaceCommands.REFRESH_RESULTS, { + execute: async () => this.searchInWorkspaceWidget && this.searchInWorkspaceWidget.refresh(), + isEnabled: () => { + return this.searchInWorkspaceWidget && (this.searchInWorkspaceWidget.hasResultList() || this.searchInWorkspaceWidget.hasSearchTerm()) && this.workspaceService.tryGetRoots().length > 0 + }, + isVisible: (widget: SearchInWorkspaceWidget) => SearchInWorkspaceWidget.ID === widget.id + }); + commands.registerCommand(SearchInWorkspaceCommands.COLLAPSE_ALL, { + execute: () => this.searchInWorkspaceWidget && this.searchInWorkspaceWidget.collapseAll(), + isEnabled: () => { + return this.searchInWorkspaceWidget && this.searchInWorkspaceWidget.hasResultList() + }, + isVisible: (widget: SearchInWorkspaceWidget) => SearchInWorkspaceWidget.ID === widget.id + }); + commands.registerCommand(SearchInWorkspaceCommands.CLEAR_ALL, { + execute: () => this.searchInWorkspaceWidget && this.searchInWorkspaceWidget.clear(), + isEnabled: () => { + return this.searchInWorkspaceWidget && this.searchInWorkspaceWidget.hasResultList() + }, + isVisible: (widget: SearchInWorkspaceWidget) => SearchInWorkspaceWidget.ID === widget.id + }); } registerKeybindings(keybindings: KeybindingRegistry): void { @@ -128,6 +172,22 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut }); } + registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry) { + toolbarRegistry.registerItem({ + id: SearchInWorkspaceCommands.REFRESH_RESULTS.id, + command: SearchInWorkspaceCommands.REFRESH_RESULTS.id, + onDidChange: (handler: ()=>void)=>{this.searchInWorkspaceWidget.onDidUpdate(handler)} + }); + toolbarRegistry.registerItem({ + id: SearchInWorkspaceCommands.COLLAPSE_ALL.id, + command: SearchInWorkspaceCommands.COLLAPSE_ALL.id, + }); + toolbarRegistry.registerItem({ + id: SearchInWorkspaceCommands.CLEAR_ALL.id, + command: SearchInWorkspaceCommands.CLEAR_ALL.id, + }); + } + 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..9e8cce72b36f0 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 @@ -72,6 +72,8 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge protected searchFormContainer: HTMLElement; protected resultContainer: HTMLElement; + protected onDidUpdateHandler: () => void; + @inject(SearchInWorkspaceResultTreeWidget) protected readonly resultTreeWidget: SearchInWorkspaceResultTreeWidget; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @@ -180,6 +182,51 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge this.update(); } + hasResultList(): boolean { + return this.hasResults; + } + + hasSearchTerm(): boolean { + return this.searchTerm !== ''; + } + + refresh() { + this.resultTreeWidget.search(this.searchTerm, this.searchInWorkspaceOptions); + this.update(); + } + + collapseAll() { + this.resultTreeWidget.collapseAll(); + this.update(); + } + + 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(); + } + + onDidUpdate(handler: () => void) { + this.onDidUpdateHandler = handler; + } + protected onAfterAttach(msg: Message): void { super.onAfterAttach(msg); ReactDOM.render({this.renderSearchHeader()}{this.renderSearchInfo()}, this.searchFormContainer); @@ -192,6 +239,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.onDidUpdateHandler && this.onDidUpdateHandler(); } protected onResize(msg: Widget.ResizeMessage): void { @@ -224,55 +272,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..af95e235cd9ce 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,8 @@ opacity: 0.5; } -.search-in-workspace-tab-icon::before { - content: "\f002" +.search-in-workspace-tab-icon { + background: var(--theia-icon-search); } .highlighted-count-container {