提交 ea1e57b3 编写于 作者: C Christof Marti

Multi-select list (#45589)

上级 4ba0ecc3
......@@ -33,6 +33,7 @@ export interface IPickOpenEntry {
run?: (context: IEntryRunContext) => void;
action?: IAction;
payload?: any;
selected?: boolean;
}
export interface IPickOpenItem {
......@@ -84,6 +85,8 @@ export interface IPickOptions {
* a context key to set when this picker is active
*/
contextKey?: string;
multiSelect?: boolean;
}
export interface IInputOptions {
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { TPromise } from 'vs/base/common/winjs.base';
import { IPickOptions, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen';
import { CancellationToken } from 'vs/base/common/cancellation';
export const IQuickInputService = createDecorator<IQuickInputService>('quickInputService');
export interface IQuickInputService {
_serviceBrand: any;
pick<T extends IPickOpenEntry>(picks: TPromise<T[]>, options?: IPickOptions, token?: CancellationToken): TPromise<T[]>;
}
......@@ -1554,12 +1554,22 @@ declare module 'vscode' {
*/
ignoreFocusOut?: boolean;
multiSelect?: boolean;
/**
* An optional function that is invoked whenever an item is selected.
*/
onDidSelectItem?(item: QuickPickItem | string): any;
}
export interface MultiSelectQuickPickItem extends QuickPickItem {
selected?: boolean;
}
export interface MultiSelectQuickPickOptions extends QuickPickOptions {
multiSelect: true;
}
/**
* Options to configure the behaviour of the [workspace folder](#WorkspaceFolder) pick UI.
*/
......@@ -5069,6 +5079,7 @@ declare module 'vscode' {
* @param token A token that can be used to signal cancellation.
* @return A promise that resolves to the selection or `undefined`.
*/
export function showQuickPick(items: string[] | Thenable<string[]>, options: MultiSelectQuickPickOptions, token?: CancellationToken): Thenable<string[] | undefined>;
export function showQuickPick(items: string[] | Thenable<string[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<string | undefined>;
/**
......@@ -5079,6 +5090,7 @@ declare module 'vscode' {
* @param token A token that can be used to signal cancellation.
* @return A promise that resolves to the selected item or `undefined`.
*/
export function showQuickPick<T extends MultiSelectQuickPickItem>(items: T[] | Thenable<T[]>, options: MultiSelectQuickPickOptions, token?: CancellationToken): Thenable<T[] | undefined>;
export function showQuickPick<T extends QuickPickItem>(items: T[] | Thenable<T[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<T | undefined>;
/**
......
......@@ -7,6 +7,7 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { asWinJsPromise } from 'vs/base/common/async';
import { IQuickOpenService, IPickOptions, IInputOptions } from 'vs/platform/quickOpen/common/quickOpen';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { InputBoxOptions } from 'vscode';
import { ExtHostContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, MyQuickPickItems, MainContext, IExtHostContext } from '../node/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
......@@ -23,7 +24,8 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
constructor(
extHostContext: IExtHostContext,
@IQuickOpenService quickOpenService: IQuickOpenService
@IQuickOpenService quickOpenService: IQuickOpenService,
@IQuickInputService private _quickInputService: IQuickInputService
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostQuickOpen);
this._quickOpenService = quickOpenService;
......@@ -32,7 +34,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
public dispose(): void {
}
$show(options: IPickOptions): TPromise<number> {
$show(options: IPickOptions): TPromise<number | number[]> {
const myToken = ++this._token;
......@@ -50,12 +52,25 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
};
});
return asWinJsPromise(token => this._quickOpenService.pick(this._contents, options, token)).then(item => {
if (item) {
return item.handle;
return asWinJsPromise<number | number[]>(token => {
if (options.multiSelect) {
return this._quickInputService.pick(this._contents, options, token)
.then(items => {
if (items) {
return items.map(item => item.handle);
}
return undefined;
});
} else {
return this._quickOpenService.pick(this._contents, options, token)
.then(item => {
if (item) {
return item.handle;
}
return undefined;
});
}
return undefined;
}, undefined, progress => {
}).then(undefined, undefined, progress => {
if (progress) {
this._proxy.$onItemSelected((<MyQuickPickItems>progress).handle);
}
......
......@@ -366,7 +366,7 @@ export function createApiFactory(
showErrorMessage(message, first, ...rest) {
return extHostMessageService.showMessage(extension, Severity.Error, message, first, rest);
},
showQuickPick(items: any, options: vscode.QuickPickOptions, token?: vscode.CancellationToken) {
showQuickPick(items: any, options: vscode.QuickPickOptions, token?: vscode.CancellationToken): any {
return extHostQuickOpen.showQuickPick(items, options, token);
},
showWorkspaceFolderPick(options: vscode.WorkspaceFolderPickOptions) {
......
......@@ -327,7 +327,7 @@ export interface MyQuickPickItems extends IPickOpenEntry {
handle: number;
}
export interface MainThreadQuickOpenShape extends IDisposable {
$show(options: IPickOptions): TPromise<number>;
$show(options: IPickOptions): TPromise<number | number[]>;
$setItems(items: MyQuickPickItems[]): TPromise<any>;
$setError(error: Error): TPromise<any>;
$input(options: vscode.InputBoxOptions, validateInput: boolean): TPromise<string>;
......
......@@ -7,7 +7,7 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { wireCancellationToken, asWinJsPromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { QuickPickOptions, QuickPickItem, InputBoxOptions, WorkspaceFolderPickOptions, WorkspaceFolder } from 'vscode';
import { QuickPickOptions, QuickPickItem, InputBoxOptions, WorkspaceFolderPickOptions, WorkspaceFolder, MultiSelectQuickPickItem, MultiSelectQuickPickOptions } from 'vscode';
import { MainContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, MyQuickPickItems, IMainContext } from './extHost.protocol';
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands';
......@@ -29,9 +29,10 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape {
this._commands = commands;
}
showQuickPick(itemsOrItemsPromise: MultiSelectQuickPickItem[] | Thenable<MultiSelectQuickPickItem[]>, options: MultiSelectQuickPickOptions, token?: CancellationToken): Thenable<MultiSelectQuickPickItem[] | undefined>;
showQuickPick(itemsOrItemsPromise: string[] | Thenable<string[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<string | undefined>;
showQuickPick(itemsOrItemsPromise: QuickPickItem[] | Thenable<QuickPickItem[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<QuickPickItem | undefined>;
showQuickPick(itemsOrItemsPromise: Item[] | Thenable<Item[]>, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Thenable<Item | undefined> {
showQuickPick(itemsOrItemsPromise: Item[] | Thenable<Item[]>, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Thenable<Item | Item[] | undefined> {
// clear state from last invocation
this._onDidSelectItem = undefined;
......@@ -43,7 +44,8 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape {
placeHolder: options && options.placeHolder,
matchOnDescription: options && options.matchOnDescription,
matchOnDetail: options && options.matchOnDetail,
ignoreFocusLost: options && options.ignoreFocusOut
ignoreFocusLost: options && options.ignoreFocusOut,
multiSelect: options && options.multiSelect
});
const promise = TPromise.any(<TPromise<number | Item[]>[]>[quickPickWidget, itemsPromise]).then(values => {
......@@ -60,6 +62,7 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape {
let label: string;
let description: string;
let detail: string;
let selected: boolean;
if (typeof item === 'string') {
label = item;
......@@ -67,12 +70,14 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape {
label = item.label;
description = item.description;
detail = item.detail;
selected = options && options.multiSelect ? (<MultiSelectQuickPickItem>item).selected : undefined;
}
pickItems.push({
label,
description,
handle,
detail
detail,
selected
});
}
......@@ -89,6 +94,8 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape {
return quickPickWidget.then(handle => {
if (typeof handle === 'number') {
return items[handle];
} else if (Array.isArray(handle)) {
return handle.map(h => items[h]);
}
return undefined;
});
......@@ -98,7 +105,7 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape {
return TPromise.wrapError(err);
});
});
return wireCancellationToken<Item>(token, promise, true);
return wireCancellationToken<Item | Item[]>(token, promise, true);
}
$onItemSelected(handle: number): void {
......
......@@ -9,6 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import * as errors from 'vs/base/common/errors';
import { Part } from 'vs/workbench/browser/part';
import { QuickOpenController } from 'vs/workbench/browser/parts/quickopen/quickOpenController';
import { QuickInputService } from 'vs/workbench/browser/parts/quickinput/quickInput';
import { Sash, ISashEvent, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation } from 'vs/base/browser/ui/sash/sash';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IPartService, Position, ILayoutOptions, Parts } from 'vs/workbench/services/part/common/partService';
......@@ -67,6 +68,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal
private panel: Part;
private statusbar: Part;
private quickopen: QuickOpenController;
private quickInput: QuickInputService;
private notificationsCenter: NotificationsCenter;
private notificationsToasts: NotificationsToasts;
private toUnbind: IDisposable[];
......@@ -97,6 +99,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal
statusbar: Part
},
quickopen: QuickOpenController,
quickInput: QuickInputService,
notificationsCenter: NotificationsCenter,
notificationsToasts: NotificationsToasts,
@IStorageService private storageService: IStorageService,
......@@ -116,6 +119,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal
this.panel = parts.panel;
this.statusbar = parts.statusbar;
this.quickopen = quickopen;
this.quickInput = quickInput;
this.notificationsCenter = notificationsCenter;
this.notificationsToasts = notificationsToasts;
this.toUnbind = [];
......@@ -651,6 +655,9 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal
// Quick open
this.quickopen.layout(this.workbenchSize);
// Quick input
this.quickInput.layout(this.workbenchSize);
// Notifications
this.notificationsCenter.layout(this.workbenchSize);
this.notificationsToasts.layout(this.workbenchSize);
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.quick-input-widget {
position: absolute;
width: 600px;
z-index: 2000;
padding-bottom: 6px;
left: 50%;
margin-left: -300px;
}
.quick-input-actions {
padding: 3px;
}
.quick-input-actions button {
border: 0;
padding: 0px 4px 1px 4px;
float: right;
margin-left: 4px;
}
.quick-input-actions button:focus {
outline:0;
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./quickInput';
import { Component } from 'vs/workbench/common/component';
import { IQuickInputService } from 'vs/platform/quickInput/common/quickInput';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { Dimension } from 'vs/base/browser/builder';
import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
import * as dom from 'vs/base/browser/dom';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { registerThemingParticipant, ITheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService';
import { buttonBackground, buttonForeground, contrastBorder, buttonHoverBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme';
import { IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen';
import { TPromise } from 'vs/base/common/winjs.base';
import { CancellationToken } from 'vs/base/common/cancellation';
const $ = dom.$;
export interface ISelectedElement {
item: object;
label: string;
selected: boolean;
}
interface ISelectedElementTemplateData {
element: HTMLElement;
name: HTMLElement;
checkbox: HTMLInputElement;
context: ISelectedElement;
toDispose: IDisposable[];
}
class SelectedElementRenderer implements IRenderer<ISelectedElement, ISelectedElementTemplateData> {
static readonly ID = 'selectedelement';
get templateId() {
return SelectedElementRenderer.ID;
}
renderTemplate(container: HTMLElement): ISelectedElementTemplateData {
const data: ISelectedElementTemplateData = Object.create(null);
data.element = dom.append(container, $('.selected_element'));
data.checkbox = <HTMLInputElement>$('input');
data.checkbox.type = 'checkbox';
data.toDispose = [];
data.toDispose.push(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => data.context.selected = !data.context.selected));
dom.append(data.element, data.checkbox);
data.name = dom.append(data.element, $('span.label'));
return data;
}
renderElement(element: ISelectedElement, index: number, data: ISelectedElementTemplateData): void {
data.context = element;
data.name.textContent = element.label;
data.element.title = data.name.textContent;
data.checkbox.checked = element.selected;
}
disposeTemplate(templateData: ISelectedElementTemplateData): void {
dispose(templateData.toDispose);
}
}
class SelectedElementDelegate implements IDelegate<ISelectedElement> {
getHeight(element: ISelectedElement): number {
return 22;
}
getTemplateId(element: ISelectedElement): string {
return SelectedElementRenderer.ID;
}
}
export class QuickInputService extends Component implements IQuickInputService {
public _serviceBrand: any;
private static readonly ID = 'workbench.component.quickinput';
private static readonly MAX_WIDTH = 600; // Max total width of quick open widget
// private static readonly MAX_ITEMS_HEIGHT = 20 * 22; // Max height of item list below input field
private layoutDimensions: Dimension;
private container: HTMLElement;
private list: WorkbenchList<ISelectedElement>;
private elements: ISelectedElement[] = [];
private resolve: (value?: object[] | Thenable<object[]>) => void;
constructor(
@IInstantiationService private instantiationService: IInstantiationService,
@IPartService private partService: IPartService,
@IThemeService themeService: IThemeService
) {
super(QuickInputService.ID, themeService);
}
private create() {
if (this.container) {
return;
}
const workbench = document.getElementById(this.partService.getWorkbenchElementId());
this.container = dom.append(workbench, $('.quick-input-widget'));
this.container.style.display = 'none';
const listContainer = dom.append(this.container, $('.quick-input-list'));
const delegate = new SelectedElementDelegate();
this.list = this.instantiationService.createInstance(WorkbenchList, listContainer, delegate, [new SelectedElementRenderer()], {
identityProvider: element => element.label,
multipleSelectionSupport: false
}) as WorkbenchList<ISelectedElement>;
const buttonContainer = dom.append(this.container, $('.quick-input-actions'));
const cancel = dom.append(buttonContainer, $('button'));
cancel.textContent = 'Cancel'; // TODO
this.toUnbind.push(dom.addDisposableListener(cancel, dom.EventType.CLICK, e => this.close(false)));
const ok = dom.append(buttonContainer, $('button'));
ok.textContent = 'OK'; // TODO
this.toUnbind.push(dom.addDisposableListener(ok, dom.EventType.CLICK, e => this.close(true)));
this.toUnbind.push(dom.addDisposableListener(this.container, 'focusout', (e: FocusEvent) => {
for (let element = <Element>e.relatedTarget; element; element = element.parentElement) {
if (element === this.container) {
return;
}
}
this.close(false);
}));
}
private close(ok: boolean) {
if (ok) {
this.resolve(this.elements.filter(e => e.selected).map(e => e.item));
} else {
this.resolve();
}
this.container.style.display = 'none';
}
async pick<T extends IPickOpenEntry>(picks: TPromise<T[]>, options?: IPickOptions, token?: CancellationToken): TPromise<T[]> {
this.create();
// TODO: Progress indication.
this.elements = (await picks).map(item => ({
item,
label: item.label,
selected: !!item.selected
}));
this.list.splice(0, this.list.length, this.elements);
this.container.style.display = null;
this.updateLayout();
this.list.focusFirst();
this.list.domFocus();
return new TPromise<T[]>(resolve => this.resolve = resolve);
}
public layout(dimension: Dimension): void {
this.layoutDimensions = dimension;
this.updateLayout();
}
private updateLayout() {
if (this.layoutDimensions && this.container) {
const titlebarOffset = this.partService.getTitleBarOffset();
this.container.style.top = `${titlebarOffset}px`;
const style = this.container.style;
const width = Math.min(this.layoutDimensions.width * 0.62 /* golden cut */, QuickInputService.MAX_WIDTH);
style.width = width + 'px';
style.marginLeft = '-' + (width / 2) + 'px';
this.list.layout();
}
}
}
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const sideBarBackground = theme.getColor(SIDE_BAR_BACKGROUND);
const sideBarForeground = theme.getColor(SIDE_BAR_FOREGROUND);
const contrastBorderColor = theme.getColor(contrastBorder);
const widgetShadowColor = theme.getColor(widgetShadow);
collector.addRule(`.quick-input-widget {
${sideBarBackground ? `background-color: ${sideBarBackground};` : ''}
${sideBarForeground ? `color: ${sideBarForeground};` : ''}
${contrastBorderColor ? `border: 1px solid ${contrastBorderColor};` : ''}
${widgetShadowColor ? `box-shadow: 0 5px 8px ${widgetShadowColor};` : ''}
}`);
const buttonBackgroundColor = theme.getColor(buttonBackground);
const buttonForegroundColor = theme.getColor(buttonForeground);
collector.addRule(`.quick-input-actions button {
${buttonBackgroundColor ? `background-color: ${buttonBackgroundColor};` : ''}
${buttonForegroundColor ? `color: ${buttonForegroundColor};` : ''}
${contrastBorderColor ? `border: 1px solid ${contrastBorderColor};` : ''}
}`);
const buttonHoverBackgroundColor = theme.getColor(buttonHoverBackground);
if (buttonHoverBackgroundColor) {
collector.addRule(`.quick-input-actions button:hover { background-color: ${buttonHoverBackgroundColor}; }`);
}
});
......@@ -35,6 +35,8 @@ import { WorkbenchLayout } from 'vs/workbench/browser/layout';
import { IActionBarRegistry, Extensions as ActionBarExtensions } from 'vs/workbench/browser/actions';
import { PanelRegistry, Extensions as PanelExtensions } from 'vs/workbench/browser/panel';
import { QuickOpenController } from 'vs/workbench/browser/parts/quickopen/quickOpenController';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { QuickInputService } from 'vs/workbench/browser/parts/quickinput/quickInput';
import { getServices } from 'vs/platform/instantiation/common/extensions';
import { Position, Parts, IPartService, ILayoutOptions, Dimension } from 'vs/workbench/services/part/common/partService';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
......@@ -199,6 +201,7 @@ export class Workbench implements IPartService {
private editorPart: EditorPart;
private statusbarPart: StatusbarPart;
private quickOpen: QuickOpenController;
private quickInput: QuickInputService;
private notificationsCenter: NotificationsCenter;
private notificationsToasts: NotificationsToasts;
private workbenchLayout: WorkbenchLayout;
......@@ -622,6 +625,11 @@ export class Workbench implements IPartService {
this.toUnbind.push({ dispose: () => this.quickOpen.shutdown() });
serviceCollection.set(IQuickOpenService, this.quickOpen);
// Quick input service
this.quickInput = this.instantiationService.createInstance(QuickInputService);
this.toUnbind.push({ dispose: () => this.quickInput.shutdown() });
serviceCollection.set(IQuickInputService, this.quickInput);
// Contributed services
const contributedServices = getServices();
for (let contributedService of contributedServices) {
......@@ -1154,6 +1162,7 @@ export class Workbench implements IPartService {
statusbar: this.statusbarPart, // Statusbar
},
this.quickOpen, // Quickopen
this.quickInput, // QuickInput
this.notificationsCenter, // Notifications Center
this.notificationsToasts // Notifications Toasts
);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册