diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 9a0a307dd5c..a0981c5c296 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -240,6 +240,9 @@ export class ContributableViewsModel extends Disposable { private _onDidChangeViewState = this._register(new Emitter()); protected readonly onDidChangeViewState: Event = this._onDidChangeViewState.event; + private _onDidChangeActiveViews = this._register(new Emitter()); + readonly onDidChangeActiveViews: Event = this._onDidChangeActiveViews.event; + constructor( container: ViewContainer, viewsService: IViewsService, @@ -469,6 +472,8 @@ export class ContributableViewsModel extends Disposable { if (toAdd.length) { this._onDidAdd.fire(toAdd); } + + this._onDidChangeActiveViews.fire(this.viewDescriptors); } } diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index 239514051a2..aa4e7f432ed 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -23,7 +23,7 @@ import { DefaultPanelDndController } from 'vs/base/browser/ui/splitview/panelvie import { WorkbenchTree, IListService } from 'vs/platform/list/browser/listService'; import { IWorkbenchThemeService, IFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { localize } from 'vs/nls'; @@ -112,7 +112,7 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView })); result.push(...viewToggleActions); - const parentActions = super.getContextMenuActions(); + const parentActions = this.getViewletContextMenuActions(); if (viewToggleActions.length && parentActions.length) { result.push(new Separator()); } @@ -121,6 +121,10 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView return result; } + protected getViewletContextMenuActions() { + return super.getContextMenuActions(); + } + setVisible(visible: boolean): void { super.setVisible(visible); this.panels.filter(view => view.isVisible() !== visible) @@ -264,7 +268,7 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView }); } - private toggleViewVisibility(viewId: string): void { + protected toggleViewVisibility(viewId: string): void { const visible = !this.viewsModel.isVisible(viewId); type ViewsToggleVisibilityClassification = { viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; @@ -321,6 +325,109 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView } } +export abstract class FilterViewContainerViewlet extends ViewContainerViewlet { + private constantViewDescriptors: Map = new Map(); + private allViews: Map> = new Map(); + private filterValue: string; + + protected onDidChangeFilterValue: Emitter = new Emitter(); + + constructor( + viewletId: string, + @IConfigurationService configurationService: IConfigurationService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService, + @IWorkspaceContextService contextService: IWorkspaceContextService + ) { + super(viewletId, `${viewletId}.state`, false, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); + this._register(this.onDidChangeFilterValue.event(newFilterValue => { + this.filterValue = newFilterValue; + this.onFilterChanged(newFilterValue); + })); + + this._register(this.viewsModel.onDidChangeActiveViews((viewDescriptors) => { + viewDescriptors.forEach(descriptor => { + let filterOnValue = this.getFilterOn(descriptor); + if (!filterOnValue) { + return; + } + if (!this.allViews.has(filterOnValue)) { + this.allViews.set(filterOnValue, new Map()); + } + this.allViews.get(filterOnValue)!.set(descriptor.id, descriptor); + if (filterOnValue !== this.filterValue) { + this.viewsModel.setVisible(descriptor.id, false); + } + }); + })); + } + + protected addConstantViewDescriptors(constantViewDescriptors: IViewDescriptor[]) { + constantViewDescriptors.forEach(viewDescriptor => this.constantViewDescriptors.set(viewDescriptor.id, viewDescriptor)); + } + + protected abstract getFilterOn(viewDescriptor: IViewDescriptor): string | undefined; + + private onFilterChanged(newFilterValue: string) { + this.getViewsNotForTarget(newFilterValue).forEach(item => this.viewsModel.setVisible(item.id, false)); + this.getViewsForTarget(newFilterValue).forEach(item => this.viewsModel.setVisible(item.id, true)); + } + + getContextMenuActions(): IAction[] { + const result: IAction[] = []; + let viewToggleActions: IAction[] = Array.from(this.constantViewDescriptors.values()).map(viewDescriptor => ({ + id: `${viewDescriptor.id}.toggleVisibility`, + label: viewDescriptor.name, + checked: this.viewsModel.isVisible(viewDescriptor.id), + enabled: viewDescriptor.canToggleVisibility, + run: () => this.toggleViewVisibility(viewDescriptor.id) + })); + + result.push(...viewToggleActions); + const parentActions = this.getViewletContextMenuActions(); + if (viewToggleActions.length && parentActions.length) { + result.push(new Separator()); + } + + result.push(...parentActions); + return result; + } + + private getViewsForTarget(target: string): IViewDescriptor[] { + return this.allViews.has(target) ? Array.from(this.allViews.get(target)!.values()) : []; + } + + private getViewsNotForTarget(target: string): IViewDescriptor[] { + const iterable = this.allViews.keys(); + let key = iterable.next(); + let views: IViewDescriptor[] = []; + while (!key.done) { + if (key.value !== target) { + views = views.concat(this.getViewsForTarget(key.value)); + } + key = iterable.next(); + } + return views; + } + + onDidAddViews(added: IAddedViewDescriptorRef[]): ViewletPanel[] { + const panels: ViewletPanel[] = super.onDidAddViews(added); + for (let i = 0; i < added.length; i++) { + if (this.constantViewDescriptors.has(added[i].viewDescriptor.id)) { + panels[i].setExpanded(false); + } + } + return panels; + } + + abstract getTitle(): string; +} + export class FileIconThemableWorkbenchTree extends WorkbenchTree { constructor( diff --git a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts new file mode 100644 index 00000000000..f57014a69b5 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; + +import { IActionRunner, IAction, Action } from 'vs/base/common/actions'; +import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { selectBorder } from 'vs/platform/theme/common/colorRegistry'; +import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; +import { IViewDescriptor } from 'vs/workbench/common/views'; +import { startsWith } from 'vs/base/common/strings'; +import { isStringArray } from 'vs/base/common/types'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; + +export interface IRemoteSelectItem extends ISelectOptionItem { + authority: string[]; +} + +export class SwitchRemoteViewItem extends SelectActionViewItem { + + actionRunner!: IActionRunner; + + constructor( + action: IAction, + private readonly optionsItems: IRemoteSelectItem[], + @IThemeService private readonly themeService: IThemeService, + @IContextViewService contextViewService: IContextViewService, + @IRemoteExplorerService remoteExplorerService: IRemoteExplorerService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService + ) { + super(null, action, optionsItems, 0, contextViewService, { ariaLabel: nls.localize('remotes', 'Switch Remote') }); + this._register(attachSelectBoxStyler(this.selectBox, themeService, { + selectBackground: SIDE_BAR_BACKGROUND + })); + + this.setSelectionForConnection(optionsItems, environmentService, remoteExplorerService); + } + + private setSelectionForConnection(optionsItems: IRemoteSelectItem[], environmentService: IWorkbenchEnvironmentService, remoteExplorerService: IRemoteExplorerService) { + // TODO: set from saved state + if (this.optionsItems.length > 0) { + const remoteAuthority = environmentService.configuration.remoteAuthority; + let index = 0; + if (remoteAuthority) { + const actualRemoteAuthority = remoteAuthority.split('+')[0]; + for (let optionIterator = 0; (optionIterator < this.optionsItems.length) && (index === 0); optionIterator++) { + for (let authorityIterator = 0; authorityIterator < optionsItems[optionIterator].authority.length; authorityIterator++) { + if (optionsItems[optionIterator].authority[authorityIterator] === actualRemoteAuthority) { + index = optionIterator; + break; + } + } + } + } + this.select(index); + remoteExplorerService.targetType = optionsItems[index].authority[0]; + } + } + + render(container: HTMLElement) { + super.render(container); + dom.addClass(container, 'switch-remote'); + this._register(attachStylerCallback(this.themeService, { selectBorder }, colors => { + container.style.border = colors.selectBorder ? `1px solid ${colors.selectBorder}` : ''; + })); + } + + protected getActionContext(_: string, index: number): any { + return this.optionsItems[index]; + } + + static createOptionItems(views: IViewDescriptor[]): IRemoteSelectItem[] { + let options: IRemoteSelectItem[] = []; + views.forEach(view => { + if (view.group && startsWith(view.group, 'targets') && view.remoteAuthority) { + options.push({ text: view.name, authority: isStringArray(view.remoteAuthority) ? view.remoteAuthority : [view.remoteAuthority] }); + } + }); + return options; + } +} + +export class SwitchRemoteAction extends Action { + + public static readonly ID = 'remote.explorer.switch'; + public static readonly LABEL = nls.localize('remote.explorer.switch', "Switch Remote"); + + constructor( + id: string, label: string, + @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService + ) { + super(id, label); + } + + public async run(item: IRemoteSelectItem): Promise { + this.remoteExplorerService.targetType = item.authority[0]; + } +} diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 4d17af30bd3..f87cdc9515a 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -16,10 +16,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { FilterViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { VIEWLET_ID, VIEW_CONTAINER } from 'vs/workbench/contrib/remote/common/remote.contribution'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; -import { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IViewDescriptor, IViewsRegistry, Extensions } from 'vs/workbench/common/views'; @@ -47,7 +46,10 @@ import Severity from 'vs/base/common/severity'; import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions'; import { IDisposable } from 'vs/base/common/lifecycle'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { SwitchRemoteViewItem, SwitchRemoteAction } from 'vs/workbench/contrib/remote/browser/explorerViewItems'; +import { Action, IActionViewItem, IAction } from 'vs/base/common/actions'; +import { isStringArray } from 'vs/base/common/types'; +import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService'; interface HelpInformation { extensionDescription: IExtensionDescription; @@ -364,10 +366,9 @@ class HelpPanelDescriptor implements IViewDescriptor { } } - -export class RemoteViewlet extends ViewContainerViewlet implements IViewModel { +export class RemoteViewlet extends FilterViewContainerViewlet implements IViewModel { private helpPanelDescriptor = new HelpPanelDescriptor(this); - + private actions: IAction[] | undefined; helpInformations: HelpInformation[] = []; constructor( @@ -380,10 +381,10 @@ export class RemoteViewlet extends ViewContainerViewlet implements IViewModel { @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, @IExtensionService extensionService: IExtensionService, - @IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService, + @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService ) { - super(VIEWLET_ID, `${VIEWLET_ID}.state`, true, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); - + super(VIEWLET_ID, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); + this.addConstantViewDescriptors([this.helpPanelDescriptor]); remoteHelpExtPoint.setHandler((extensions) => { let helpInformation: HelpInformation[] = []; for (let extension of extensions) { @@ -399,6 +400,34 @@ export class RemoteViewlet extends ViewContainerViewlet implements IViewModel { viewsRegistry.deregisterViews([this.helpPanelDescriptor], VIEW_CONTAINER); } }); + + this._register(this.remoteExplorerService.onDidChangeTargetType(() => { + this.onDidChangeFilterValue.fire(this.remoteExplorerService.targetType); + })); + } + + protected getFilterOn(viewDescriptor: IViewDescriptor): string | undefined { + return isStringArray(viewDescriptor.remoteAuthority) ? viewDescriptor.remoteAuthority[0] : viewDescriptor.remoteAuthority; + } + + public getActionViewItem(action: Action): IActionViewItem | undefined { + if (action.id === SwitchRemoteAction.ID) { + return this.instantiationService.createInstance(SwitchRemoteViewItem, action, SwitchRemoteViewItem.createOptionItems(Registry.as(Extensions.ViewsRegistry).getViews(VIEW_CONTAINER))); + } + + return super.getActionViewItem(action); + } + + public getActions(): IAction[] { + if (!this.actions) { + this.actions = [ + this.instantiationService.createInstance(SwitchRemoteAction, SwitchRemoteAction.ID, SwitchRemoteAction.LABEL), + ]; + this.actions.forEach(a => { + this._register(a); + }); + } + return this.actions; } private _handleRemoteInfoExtensionPoint(extension: IExtensionPointUser, helpInformation: HelpInformation[]) { @@ -419,38 +448,6 @@ export class RemoteViewlet extends ViewContainerViewlet implements IViewModel { }); } - onDidAddViews(added: IAddedViewDescriptorRef[]): ViewletPanel[] { - // too late, already added to the view model - const result = super.onDidAddViews(added); - - const remoteAuthority = this.environmentService.configuration.remoteAuthority; - if (remoteAuthority) { - const actualRemoteAuthority = remoteAuthority.split('+')[0]; - added.forEach((descriptor) => { - const panel = this.getView(descriptor.viewDescriptor.id); - if (!panel) { - return; - } - - const descriptorAuthority = descriptor.viewDescriptor.remoteAuthority; - if (typeof descriptorAuthority === 'undefined') { - panel.setExpanded(true); - } else if (descriptor.viewDescriptor.id === HelpPanel.ID) { - // Do nothing, keep the default behavior for Help - } else { - const descriptorAuthorityArr = Array.isArray(descriptorAuthority) ? descriptorAuthority : [descriptorAuthority]; - if (descriptorAuthorityArr.indexOf(actualRemoteAuthority) >= 0) { - panel.setExpanded(true); - } else { - panel.setExpanded(false); - } - } - }); - } - - return result; - } - getTitle(): string { const title = nls.localize('remote.explorer', "Remote Explorer"); return title; diff --git a/src/vs/workbench/contrib/remote/browser/remoteViewlet.css b/src/vs/workbench/contrib/remote/browser/remoteViewlet.css index 6f6908846c3..d48d027c617 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteViewlet.css +++ b/src/vs/workbench/contrib/remote/browser/remoteViewlet.css @@ -87,3 +87,22 @@ .hc-black .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.issueReporter { background-image: url('help-report-issue-hc.svg') } + +.monaco-workbench .part > .title > .title-actions .switch-remote { + display: flex; + align-items: center; + font-size: 11px; + margin-right: 0.3em; + height: 20px; + flex-shrink: 1; + margin-top: 7px; +} + +.switch-remote > .monaco-select-box { + border: none; + display: block; +} + +.monaco-workbench .part > .title > .title-actions .switch-remote > .monaco-select-box { + padding-left: 3px; +} diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 43671fbda5e..5d12781e767 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -34,7 +34,7 @@ export const VIEW_CONTAINER: ViewContainer = Registry.as('remoteExplorerService'); + +export interface IRemoteExplorerService { + _serviceBrand: undefined; + onDidChangeTargetType: Event; + targetType: string; +} + +class RemoteExplorerService implements IRemoteExplorerService { + public _serviceBrand: undefined; + private _targetType: string = ''; + private _onDidChangeTargetType: Emitter = new Emitter(); + public onDidChangeTargetType: Event = this._onDidChangeTargetType.event; + + set targetType(name: string) { + if (this._targetType !== name) { + const oldTarget = this._targetType; + this._targetType = name; + this._onDidChangeTargetType.fire(oldTarget); + } + } + get targetType(): string { + return this._targetType; + } +} + +registerSingleton(IRemoteExplorerService, RemoteExplorerService, true); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 42cf7aaffd7..ddb19940d86 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -81,6 +81,7 @@ import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/extensions/common/staticExtensions'; import 'vs/workbench/services/userDataSync/common/settingsMergeService'; import 'vs/workbench/services/path/common/remotePathService'; +import 'vs/workbench/services/remote/common/remoteExplorerService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';