提交 d00ae9bd 编写于 作者: S Sandeep Somavarapu

#55879 Collapse all action for custom views

上级 51a83e46
......@@ -1196,7 +1196,30 @@ declare module 'vscode' {
}
//#endregion
//#region Tree Item Label Highlights
//#region Tree View
/**
* Options for creating a [TreeView](#TreeView]
*/
export interface TreeViewOptions<T> {
/**
* A data provider that provides tree data.
*/
treeDataProvider: TreeDataProvider<T>;
/**
* Whether to show collapse all action or not.
*/
showCollapseAll?: boolean;
}
namespace window {
export function createTreeView<T>(viewId: string, options: TreeViewOptions<T>): TreeView<T>;
}
/**
* Label describing the [Tree item](#TreeItem)
*/
......
......@@ -26,12 +26,13 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews);
}
$registerTreeViewDataProvider(treeViewId: string): void {
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean }): void {
const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService);
this._dataProviders.set(treeViewId, dataProvider);
const viewer = this.getTreeViewer(treeViewId);
if (viewer) {
viewer.dataProvider = dataProvider;
viewer.showCollapseAllAction = !!options.showCollapseAll;
this.registerListeners(treeViewId, viewer);
this._proxy.$setVisible(treeViewId, viewer.visible);
} else {
......
......@@ -59,7 +59,7 @@ import { ExtHostUrls } from 'vs/workbench/api/node/extHostUrls';
import { ExtHostWebviews } from 'vs/workbench/api/node/extHostWebview';
import { ExtHostWindow } from 'vs/workbench/api/node/extHostWindow';
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionDescription, throwProposedApiError, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier';
import * as vscode from 'vscode';
......@@ -67,16 +67,6 @@ export interface IExtensionApiFactory {
(extension: IExtensionDescription): typeof vscode;
}
export function checkProposedApiEnabled(extension: IExtensionDescription): void {
if (!extension.enableProposedApi) {
throwProposedApiError(extension);
}
}
function throwProposedApiError(extension: IExtensionDescription): never {
throw new Error(`[${extension.id}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.id}`);
}
function proposedApiFunction<T>(extension: IExtensionDescription, fn: T): T {
if (extension.enableProposedApi) {
return fn;
......@@ -461,10 +451,10 @@ export function createApiFactory(
return extHostTerminalService.createTerminalRenderer(name);
}),
registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider<any>): vscode.Disposable {
return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider);
return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider, extension);
},
createTreeView(viewId: string, options: { treeDataProvider: vscode.TreeDataProvider<any> }): vscode.TreeView<any> {
return extHostTreeViews.createTreeView(viewId, options);
return extHostTreeViews.createTreeView(viewId, options, extension);
},
registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => {
return extHostWebviews.registerWebviewPanelSerializer(viewType, serializer);
......
......@@ -210,7 +210,7 @@ export interface MainThreadTextEditorsShape extends IDisposable {
}
export interface MainThreadTreeViewsShape extends IDisposable {
$registerTreeViewDataProvider(treeViewId: string): void;
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean }): void;
$refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Thenable<void>;
$reveal(treeViewId: string, treeItem: ITreeItem, parentChain: ITreeItem[], options: { select: boolean, focus: boolean }): Thenable<void>;
}
......
......@@ -17,6 +17,7 @@ import { TreeItemCollapsibleState, ThemeIcon } from 'vs/workbench/api/node/extHo
import { isUndefinedOrNull, isString } from 'vs/base/common/types';
import { equals } from 'vs/base/common/arrays';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtensionDescription, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
type TreeItemHandle = string;
......@@ -59,16 +60,20 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
});
}
registerTreeDataProvider<T>(id: string, treeDataProvider: vscode.TreeDataProvider<T>): vscode.Disposable {
const treeView = this.createTreeView(id, { treeDataProvider });
registerTreeDataProvider<T>(id: string, treeDataProvider: vscode.TreeDataProvider<T>, extension: IExtensionDescription): vscode.Disposable {
const treeView = this.createTreeView(id, { treeDataProvider }, extension);
return { dispose: () => treeView.dispose() };
}
createTreeView<T>(viewId: string, options: { treeDataProvider: vscode.TreeDataProvider<T> }): vscode.TreeView<T> {
createTreeView<T>(viewId: string, options: vscode.TreeViewOptions<T>, extension: IExtensionDescription): vscode.TreeView<T> {
if (!options || !options.treeDataProvider) {
throw new Error('Options with treeDataProvider is mandatory');
}
const treeView = this.createExtHostTreeViewer(viewId, options.treeDataProvider);
if (options.showCollapseAll) {
checkProposedApiEnabled(extension);
}
const treeView = this.createExtHostTreeViewer(viewId, options);
return {
get onDidCollapseElement() { return treeView.onDidCollapseElement; },
get onDidExpandElement() { return treeView.onDidExpandElement; },
......@@ -118,8 +123,8 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
treeView.setVisible(isVisible);
}
private createExtHostTreeViewer<T>(id: string, dataProvider: vscode.TreeDataProvider<T>): ExtHostTreeView<T> {
const treeView = new ExtHostTreeView<T>(id, dataProvider, this._proxy, this.commands.converter, this.logService);
private createExtHostTreeViewer<T>(id: string, options: vscode.TreeViewOptions<T>): ExtHostTreeView<T> {
const treeView = new ExtHostTreeView<T>(id, options, this._proxy, this.commands.converter, this.logService);
this.treeViews.set(id, treeView);
return treeView;
}
......@@ -141,6 +146,8 @@ class ExtHostTreeView<T> extends Disposable {
private static LABEL_HANDLE_PREFIX = '0';
private static ID_HANDLE_PREFIX = '1';
private readonly dataProvider: vscode.TreeDataProvider<T>;
private roots: TreeNode[] | null = null;
private elements: Map<TreeItemHandle, T> = new Map<TreeItemHandle, T>();
private nodes: Map<T, TreeNode> = new Map<T, TreeNode>();
......@@ -165,9 +172,10 @@ class ExtHostTreeView<T> extends Disposable {
private refreshPromise: Promise<void> = Promise.resolve(null);
constructor(private viewId: string, private dataProvider: vscode.TreeDataProvider<T>, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter, private logService: ILogService) {
constructor(private viewId: string, options: vscode.TreeViewOptions<T>, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter, private logService: ILogService) {
super();
this.proxy.$registerTreeViewDataProvider(viewId);
this.dataProvider = options.treeDataProvider;
this.proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll });
if (this.dataProvider.onDidChangeTreeData) {
let refreshingPromise, promiseCallback;
this._register(debounceEvent<T, T[]>(this.dataProvider.onDidChangeTreeData, (last, current) => {
......
......@@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction, IActionItem, ActionRunner } from 'vs/base/common/actions';
import { IAction, IActionItem, ActionRunner, Action } from 'vs/base/common/actions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
......@@ -36,10 +36,10 @@ import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/v
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { localize } from 'vs/nls';
import { timeout } from 'vs/base/common/async';
import { CollapseAllAction } from 'vs/base/parts/tree/browser/treeDefaults';
export class CustomTreeViewPanel extends ViewletPanel {
private menus: TitleMenus;
private treeViewer: ITreeViewer;
constructor(
......@@ -47,15 +47,14 @@ export class CustomTreeViewPanel extends ViewletPanel {
@INotificationService private notificationService: INotificationService,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IInstantiationService private instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@IViewsService viewsService: IViewsService,
) {
super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService);
this.treeViewer = (<ICustomViewDescriptor>ViewsRegistry.getView(options.id)).treeViewer;
const { treeViewer } = (<ICustomViewDescriptor>ViewsRegistry.getView(options.id));
this.treeViewer = treeViewer;
this.treeViewer.onDidChangeActions(() => this.updateActions(), this, this.disposables);
this.disposables.push(toDisposable(() => this.treeViewer.setVisibility(false)));
this.menus = this.instantiationService.createInstance(TitleMenus, this.id);
this.menus.onDidChangeTitle(() => this.updateActions(), this, this.disposables);
this.updateTreeVisibility();
}
......@@ -83,11 +82,11 @@ export class CustomTreeViewPanel extends ViewletPanel {
}
getActions(): IAction[] {
return [...this.menus.getTitleActions()];
return [...this.treeViewer.getPrimaryActions()];
}
getSecondaryActions(): IAction[] {
return this.menus.getTitleSecondaryActions();
return [...this.treeViewer.getSecondaryActions()];
}
getActionItem(action: IAction): IActionItem {
......@@ -180,6 +179,7 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
private activated: boolean = false;
private _hasIconForParentNode = false;
private _hasIconForLeafNode = false;
private _showCollapseAllAction = false;
private domNode: HTMLElement;
private treeContainer: HTMLElement;
......@@ -187,6 +187,7 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
private tree: FileIconThemableWorkbenchTree;
private root: ITreeItem;
private elementsToRefresh: ITreeItem[] = [];
private menus: TitleMenus;
private _dataProvider: ITreeViewDataProvider;
......@@ -202,6 +203,9 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
private _onDidChangeVisibility: Emitter<boolean> = this._register(new Emitter<boolean>());
readonly onDidChangeVisibility: Event<boolean> = this._onDidChangeVisibility.event;
private _onDidChangeActions: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeActions: Event<void> = this._onDidChangeActions.event;
constructor(
private id: string,
private container: ViewContainer,
......@@ -214,6 +218,8 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
) {
super();
this.root = new Root();
this.menus = this._register(this.instantiationService.createInstance(TitleMenus, this.id));
this._register(this.menus.onDidChangeTitle(() => this._onDidChangeActions.fire()));
this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/));
this._register(this.themeService.onThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/));
this._register(this.configurationService.onDidChangeConfiguration(e => {
......@@ -263,6 +269,30 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
return this.isVisible;
}
get showCollapseAllAction(): boolean {
return this._showCollapseAllAction;
}
set showCollapseAllAction(showCollapseAllAction: boolean) {
if (this._showCollapseAllAction !== !!showCollapseAllAction) {
this._showCollapseAllAction = !!showCollapseAllAction;
this._onDidChangeActions.fire();
}
}
getPrimaryActions(): IAction[] {
if (this.showCollapseAllAction) {
const collapseAllAction = new Action('vs.tree.collapse', localize('collapse', "Collapse"), 'monaco-tree-action collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve());
return [...this.menus.getTitleActions(), collapseAllAction];
} else {
return this.menus.getTitleActions();
}
}
getSecondaryActions(): IAction[] {
return this.menus.getTitleSecondaryActions();
}
setVisibility(isVisible: boolean): void {
isVisible = !!isVisible;
if (this.isVisible === isVisible) {
......
......@@ -17,6 +17,7 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { values } from 'vs/base/common/map';
import { Registry } from 'vs/platform/registry/common/platform';
import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IAction } from 'vs/base/common/actions';
export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test';
......@@ -238,6 +239,10 @@ export interface ITreeViewer extends IDisposable {
dataProvider: ITreeViewDataProvider;
showCollapseAllAction: boolean;
readonly visible: boolean;
readonly onDidExpandItem: Event<ITreeItem>;
readonly onDidCollapseItem: Event<ITreeItem>;
......@@ -246,7 +251,7 @@ export interface ITreeViewer extends IDisposable {
readonly onDidChangeVisibility: Event<boolean>;
readonly visible: boolean;
readonly onDidChangeActions: Event<void>;
refresh(treeItems?: ITreeItem[]): TPromise<void>;
......@@ -261,6 +266,10 @@ export interface ITreeViewer extends IDisposable {
getOptimalWidth(): number;
reveal(item: ITreeItem, parentChain: ITreeItem[], options: { select?: boolean }): TPromise<void>;
getPrimaryActions(): IAction[];
getSecondaryActions(): IAction[];
}
export interface ICustomViewDescriptor extends IViewDescriptor {
......
......@@ -206,3 +206,13 @@ export interface IExtensionService {
export interface ProfileSession {
stop(): TPromise<IExtensionHostProfile>;
}
export function checkProposedApiEnabled(extension: IExtensionDescription): void {
if (!extension.enableProposedApi) {
throwProposedApiError(extension);
}
}
export function throwProposedApiError(extension: IExtensionDescription): never {
throw new Error(`[${extension.id}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.id}`);
}
......@@ -72,9 +72,9 @@ suite('ExtHostTreeView', function () {
testObject = new ExtHostTreeViews(target, new ExtHostCommands(rpcProtocol, new ExtHostHeapService(), new NullLogService()), new NullLogService());
onDidChangeTreeNode = new Emitter<{ key: string }>();
onDidChangeTreeNodeWithId = new Emitter<{ key: string }>();
testObject.createTreeView('testNodeTreeProvider', { treeDataProvider: aNodeTreeDataProvider() });
testObject.createTreeView('testNodeWithIdTreeProvider', { treeDataProvider: aNodeWithIdTreeDataProvider() });
testObject.createTreeView('testNodeWithHighlightsTreeProvider', { treeDataProvider: aNodeWithHighlightedLabelTreeDataProvider() });
testObject.createTreeView('testNodeTreeProvider', { treeDataProvider: aNodeTreeDataProvider() }, null);
testObject.createTreeView('testNodeWithIdTreeProvider', { treeDataProvider: aNodeWithIdTreeDataProvider() }, null);
testObject.createTreeView('testNodeWithHighlightsTreeProvider', { treeDataProvider: aNodeWithHighlightedLabelTreeDataProvider() }, null);
return loadCompleteTree('testNodeTreeProvider');
});
......@@ -445,14 +445,14 @@ suite('ExtHostTreeView', function () {
});
test('reveal will throw an error if getParent is not implemented', () => {
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aNodeTreeDataProvider() });
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aNodeTreeDataProvider() }, null);
return treeView.reveal({ key: 'a' })
.then(() => assert.fail('Reveal should throw an error as getParent is not implemented'), () => null);
});
test('reveal will return empty array for root element', () => {
const revealTarget = sinon.spy(target, '$reveal');
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() });
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null);
return treeView.reveal({ key: 'a' })
.then(() => {
assert.ok(revealTarget.calledOnce);
......@@ -465,7 +465,7 @@ suite('ExtHostTreeView', function () {
test('reveal will return parents array for an element when hierarchy is not loaded', () => {
const revealTarget = sinon.spy(target, '$reveal');
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() });
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null);
return treeView.reveal({ key: 'aa' })
.then(() => {
assert.ok(revealTarget.calledOnce);
......@@ -478,7 +478,7 @@ suite('ExtHostTreeView', function () {
test('reveal will return parents array for an element when hierarchy is loaded', () => {
const revealTarget = sinon.spy(target, '$reveal');
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() });
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null);
return testObject.$getChildren('treeDataProvider')
.then(() => testObject.$getChildren('treeDataProvider', '0/0:a'))
.then(() => treeView.reveal({ key: 'aa' })
......@@ -500,7 +500,7 @@ suite('ExtHostTreeView', function () {
}
};
const revealTarget = sinon.spy(target, '$reveal');
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() });
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null);
return treeView.reveal({ key: 'bac' }, { select: false, focus: false })
.then(() => {
assert.ok(revealTarget.calledOnce);
......@@ -516,7 +516,7 @@ suite('ExtHostTreeView', function () {
test('reveal after first udpate', () => {
const revealTarget = sinon.spy(target, '$reveal');
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() });
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null);
return loadCompleteTree('treeDataProvider')
.then(() => {
tree = {
......@@ -544,7 +544,7 @@ suite('ExtHostTreeView', function () {
test('reveal after second udpate', () => {
const revealTarget = sinon.spy(target, '$reveal');
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() });
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null);
return loadCompleteTree('treeDataProvider')
.then(() => {
tree = {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册