Skip to content

Commit

Permalink
Adopt input box for port naming (#85766)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexr00 authored Nov 29, 2019
1 parent f9b6372 commit 113bced
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 40 deletions.
5 changes: 5 additions & 0 deletions src/vs/workbench/browser/parts/views/media/views.css
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@
padding-left: 3px;
}

.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .monaco-inputbox {
line-height: normal;
flex: 1;
}

.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel {
flex: 1;
text-overflow: ellipsis;
Expand Down
7 changes: 7 additions & 0 deletions src/vs/workbench/common/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,10 @@ export interface ITreeViewDataProvider {
getChildren(element?: ITreeItem): Promise<ITreeItem[]>;

}

export interface IEditableData {
validationMessage: (value: string) => string | null;
placeholder?: string | null;
startingValue?: string | null;
onFinish: (value: string, success: boolean) => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSou
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration, IExplorerService, IEditableData } from 'vs/workbench/contrib/files/common/files';
import { IFilesConfiguration, IExplorerService } from 'vs/workbench/contrib/files/common/files';
import { dirname, joinPath, isEqualOrParent, basename, hasToIgnoreCase, distinctParents } from 'vs/base/common/resources';
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { localize } from 'vs/nls';
Expand Down Expand Up @@ -54,6 +54,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { ILabelService } from 'vs/platform/label/common/label';
import { isNumber } from 'vs/base/common/types';
import { domEvent } from 'vs/base/browser/event';
import { IEditableData } from 'vs/workbench/common/views';

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

Expand Down
3 changes: 2 additions & 1 deletion src/vs/workbench/contrib/files/common/explorerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { Event, Emitter } from 'vs/base/common/event';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IExplorerService, IEditableData, IFilesConfiguration, SortOrder, SortOrderConfiguration, IContextProvider } from 'vs/workbench/contrib/files/common/files';
import { IExplorerService, IFilesConfiguration, SortOrder, SortOrderConfiguration, IContextProvider } from 'vs/workbench/contrib/files/common/files';
import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel';
import { URI } from 'vs/base/common/uri';
import { FileOperationEvent, FileOperation, IFileStat, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files';
Expand All @@ -18,6 +18,7 @@ import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/co
import { IExpression } from 'vs/base/common/glob';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditableData } from 'vs/workbench/common/views';

function getFileEventsExcludes(configurationService: IConfigurationService, root?: URI): IExpression {
const scope = root ? { resource: root } : undefined;
Expand Down
7 changes: 1 addition & 6 deletions src/vs/workbench/contrib/files/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { IModeService, ILanguageSelection } from 'vs/editor/common/services/mode
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys';
import { Registry } from 'vs/platform/registry/common/platform';
import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer } from 'vs/workbench/common/views';
import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer, IEditableData } from 'vs/workbench/common/views';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
Expand All @@ -35,11 +35,6 @@ export const VIEWLET_ID = 'workbench.view.explorer';
*/
export const VIEW_CONTAINER: ViewContainer = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID);

export interface IEditableData {
validationMessage: (value: string) => string | null;
onFinish: (value: string, success: boolean) => void;
}

export interface IExplorerService {
_serviceBrand: undefined;
readonly roots: ExplorerItem[];
Expand Down
162 changes: 132 additions & 30 deletions src/vs/workbench/contrib/remote/browser/tunnelView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import 'vs/css!./media/tunnelView';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { IViewDescriptor } from 'vs/workbench/common/views';
import { IViewDescriptor, IEditableData } from 'vs/workbench/common/views';
import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IContextKeyService, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
Expand All @@ -20,7 +20,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { Disposable, IDisposable, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose } from 'vs/base/common/lifecycle';
import { ViewletPane, IViewletPaneOptions } from 'vs/workbench/browser/parts/views/paneViewlet';
import { ActionBar, ActionViewItem, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
Expand All @@ -31,6 +31,12 @@ import { IRemoteExplorerService, TunnelModel } from 'vs/workbench/services/remot
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { URI } from 'vs/workbench/workbench.web.api';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { once } from 'vs/base/common/functional';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';

class TunnelTreeVirtualDelegate implements IListVirtualDelegate<ITunnelItem> {
getHeight(element: ITunnelItem): number {
Expand Down Expand Up @@ -142,7 +148,10 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
private readonly viewId: string,
@IMenuService private readonly menuService: IMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IInstantiationService private readonly instantiationService: IInstantiationService
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService
) {
super();
}
Expand Down Expand Up @@ -181,30 +190,99 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
renderElement(element: ITreeNode<ITunnelGroup | ITunnelItem, ITunnelGroup | ITunnelItem>, index: number, templateData: ITunnelTemplateData): void {
templateData.elementDisposable.dispose();
const node = element.element;

// reset
templateData.actionBar.clear();
if (this.isTunnelItem(node)) {
templateData.iconLabel.setLabel(node.label, node.description, { title: node.label + ' - ' + node.description, extraClasses: ['tunnel-view-label'] });
templateData.actionBar.context = node;
const contextKeyService = this.contextKeyService.createScoped();
contextKeyService.createKey('view', this.viewId);
contextKeyService.createKey('tunnelType', node.tunnelType);
contextKeyService.createKey('tunnelCloseable', node.closeable);
const menu = this.menuService.createMenu(MenuId.TunnelInline, contextKeyService);
this._register(menu);
const actions: IAction[] = [];
this._register(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions));
if (actions) {
templateData.actionBar.push(actions, { icon: true, label: false });
if (this._actionRunner) {
templateData.actionBar.actionRunner = this._actionRunner;
}

const editableData = this.remoteExplorerService.getEditableData(node.remote);
if (editableData) {
templateData.iconLabel.element.style.display = 'none';
this.renderInputBox(templateData.container, node, editableData);
} else {
templateData.iconLabel.element.style.display = 'flex';
this.renderTunnel(node, templateData);
}
} else {
templateData.iconLabel.setLabel(node.label);
}
}

private renderTunnel(node: ITunnelItem, templateData: ITunnelTemplateData) {
templateData.iconLabel.setLabel(node.label, node.description, { title: node.label + ' - ' + node.description, extraClasses: ['tunnel-view-label'] });
templateData.actionBar.context = node;
const contextKeyService = this.contextKeyService.createScoped();
contextKeyService.createKey('view', this.viewId);
contextKeyService.createKey('tunnelType', node.tunnelType);
contextKeyService.createKey('tunnelCloseable', node.closeable);
const menu = this.menuService.createMenu(MenuId.TunnelInline, contextKeyService);
this._register(menu);
const actions: IAction[] = [];
this._register(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions));
if (actions) {
templateData.actionBar.push(actions, { icon: true, label: false });
if (this._actionRunner) {
templateData.actionBar.actionRunner = this._actionRunner;
}
}
}

private renderInputBox(container: HTMLElement, item: ITunnelItem, editableData: IEditableData): IDisposable {
const value = editableData.startingValue || '';
const inputBox = new InputBox(container, this.contextViewService, {
ariaLabel: nls.localize('remote.tunnelsView.input', "Press Enter to confirm or Escape to cancel."),
validationOptions: {
validation: (value) => {
const content = editableData.validationMessage(value);
if (!content) {
return null;
}

return {
content,
formatContent: true,
type: MessageType.ERROR
};
}
},
placeholder: editableData.placeholder || ''
});
const styler = attachInputBoxStyler(inputBox, this.themeService);

inputBox.value = value;
inputBox.focus();

const done = once((success: boolean, finishEditing: boolean) => {
inputBox.element.style.display = 'none';
const value = inputBox.value;
dispose(toDispose);
if (finishEditing) {
editableData.onFinish(value, success);
}
});

const toDispose = [
inputBox,
dom.addStandardDisposableListener(inputBox.inputElement, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
if (e.equals(KeyCode.Enter)) {
if (inputBox.validate()) {
done(true, true);
}
} else if (e.equals(KeyCode.Escape)) {
done(false, true);
}
}),
dom.addDisposableListener(inputBox.inputElement, dom.EventType.BLUR, () => {
done(inputBox.isInputValid(), true);
}),
styler
];

return toDisposable(() => {
done(false, false);
});
}

disposeElement(resource: ITreeNode<ITunnelGroup | ITunnelItem, ITunnelGroup | ITunnelItem>, index: number, templateData: ITunnelTemplateData): void {
templateData.elementDisposable.dispose();
}
Expand Down Expand Up @@ -321,7 +399,10 @@ export class TunnelPanel extends ViewletPane {
@IQuickInputService protected quickInputService: IQuickInputService,
@ICommandService protected commandService: ICommandService,
@IMenuService private readonly menuService: IMenuService,
@INotificationService private readonly notificationService: INotificationService
@INotificationService private readonly notificationService: INotificationService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService);
this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService);
Expand Down Expand Up @@ -353,7 +434,7 @@ export class TunnelPanel extends ViewletPane {
dom.addClass(treeContainer, 'file-icon-themable-tree');
dom.addClass(treeContainer, 'show-file-icons');
container.appendChild(treeContainer);
const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService);
const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService);
this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree,
'RemoteTunnels',
treeContainer,
Expand Down Expand Up @@ -382,13 +463,29 @@ export class TunnelPanel extends ViewletPane {
this.tree.updateChildren(undefined, true);
}));

const helpItemNavigator = this._register(new TreeResourceNavigator2(this.tree, { openOnFocus: false, openOnSelection: false }));
const navigator = this._register(new TreeResourceNavigator2(this.tree, { openOnFocus: false, openOnSelection: false }));

this._register(Event.debounce(helpItemNavigator.onDidOpenResource, (last, event) => event, 75, true)(e => {
if (e.element.tunnelType === TunnelType.Add) {
this._register(Event.debounce(navigator.onDidOpenResource, (last, event) => event, 75, true)(e => {
if (e.element && (e.element.tunnelType === TunnelType.Add)) {
this.commandService.executeCommand(ForwardPortAction.ID);
}
}));

this._register(this.remoteExplorerService.onDidChangeEditable(async e => {
const isEditing = !!this.remoteExplorerService.getEditableData(e);

if (!isEditing) {
dom.removeClass(treeContainer, 'highlight');
}

await this.tree.updateChildren(undefined, false);

if (isEditing) {
dom.addClass(treeContainer, 'highlight');
} else {
this.tree.domFocus();
}
}));
}

private get contributedContextMenu(): IMenu {
Expand Down Expand Up @@ -471,12 +568,17 @@ namespace NameTunnelAction {
return async (accessor, arg) => {
if (arg instanceof TunnelItem) {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
const quickInputService = accessor.get(IQuickInputService);
const name = await quickInputService.input({ placeHolder: nls.localize('remote.tunnelView.pickName', 'Port name, or leave blank for no name') });
if (name === undefined) {
return;
}
remoteExplorerService.tunnelModel.name(arg.remote, name);
remoteExplorerService.setEditable(arg.remote, {
onFinish: (value, success) => {
if (success) {
remoteExplorerService.tunnelModel.name(arg.remote, value);
}
remoteExplorerService.setEditable(arg.remote, null);
},
validationMessage: () => null,
placeholder: nls.localize('remote.tunnelsView.namePlaceholder', "Name port"),
startingValue: arg.name
});
}
return;
};
Expand Down
24 changes: 22 additions & 2 deletions src/vs/workbench/services/remote/common/remoteExplorerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/e
import { URI } from 'vs/base/common/uri';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { Disposable } from 'vs/base/common/lifecycle';
import { IEditableData } from 'vs/workbench/common/views';

export const IRemoteExplorerService = createDecorator<IRemoteExplorerService>('remoteExplorerService');
export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType';
Expand Down Expand Up @@ -115,6 +116,9 @@ export interface IRemoteExplorerService {
targetType: string;
readonly helpInformation: HelpInformation[];
readonly tunnelModel: TunnelModel;
onDidChangeEditable: Event<number>;
setEditable(remote: number, data: IEditableData | null): void;
getEditableData(remote: number): IEditableData | undefined;
}

export interface HelpInformation {
Expand Down Expand Up @@ -155,10 +159,13 @@ const remoteHelpExtPoint = ExtensionsRegistry.registerExtensionPoint<HelpInforma
class RemoteExplorerService implements IRemoteExplorerService {
public _serviceBrand: undefined;
private _targetType: string = '';
private _onDidChangeTargetType: Emitter<string> = new Emitter<string>();
public onDidChangeTargetType: Event<string> = this._onDidChangeTargetType.event;
private readonly _onDidChangeTargetType: Emitter<string> = new Emitter<string>();
public readonly onDidChangeTargetType: Event<string> = this._onDidChangeTargetType.event;
private _helpInformation: HelpInformation[] = [];
private _tunnelModel: TunnelModel;
private editable: { remote: number, data: IEditableData } | undefined;
private readonly _onDidChangeEditable: Emitter<number> = new Emitter<number>();
public readonly onDidChangeEditable: Event<number> = this._onDidChangeEditable.event;

constructor(
@IStorageService private readonly storageService: IStorageService,
Expand Down Expand Up @@ -212,6 +219,19 @@ class RemoteExplorerService implements IRemoteExplorerService {
get tunnelModel(): TunnelModel {
return this._tunnelModel;
}

setEditable(remote: number, data: IEditableData | null): void {
if (!data) {
this.editable = undefined;
} else {
this.editable = { remote, data };
}
this._onDidChangeEditable.fire(remote);
}

getEditableData(remote: number): IEditableData | undefined {
return this.editable && this.editable.remote === remote ? this.editable.data : undefined;
}
}

registerSingleton(IRemoteExplorerService, RemoteExplorerService, true);

0 comments on commit 113bced

Please sign in to comment.