Skip to content

Commit d00ae9b

Browse files
committed
microsoft#55879 Collapse all action for custom views
1 parent 51a83e4 commit d00ae9b

9 files changed

Lines changed: 114 additions & 43 deletions

File tree

src/vs/vscode.proposed.d.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1196,7 +1196,30 @@ declare module 'vscode' {
11961196
}
11971197
//#endregion
11981198

1199-
//#region Tree Item Label Highlights
1199+
//#region Tree View
1200+
1201+
/**
1202+
* Options for creating a [TreeView](#TreeView]
1203+
*/
1204+
export interface TreeViewOptions<T> {
1205+
1206+
/**
1207+
* A data provider that provides tree data.
1208+
*/
1209+
treeDataProvider: TreeDataProvider<T>;
1210+
1211+
/**
1212+
* Whether to show collapse all action or not.
1213+
*/
1214+
showCollapseAll?: boolean;
1215+
}
1216+
1217+
namespace window {
1218+
1219+
export function createTreeView<T>(viewId: string, options: TreeViewOptions<T>): TreeView<T>;
1220+
1221+
}
1222+
12001223
/**
12011224
* Label describing the [Tree item](#TreeItem)
12021225
*/

src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie
2626
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews);
2727
}
2828

29-
$registerTreeViewDataProvider(treeViewId: string): void {
29+
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean }): void {
3030
const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService);
3131
this._dataProviders.set(treeViewId, dataProvider);
3232
const viewer = this.getTreeViewer(treeViewId);
3333
if (viewer) {
3434
viewer.dataProvider = dataProvider;
35+
viewer.showCollapseAllAction = !!options.showCollapseAll;
3536
this.registerListeners(treeViewId, viewer);
3637
this._proxy.$setVisible(treeViewId, viewer.visible);
3738
} else {

src/vs/workbench/api/node/extHost.api.impl.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,24 +59,14 @@ import { ExtHostUrls } from 'vs/workbench/api/node/extHostUrls';
5959
import { ExtHostWebviews } from 'vs/workbench/api/node/extHostWebview';
6060
import { ExtHostWindow } from 'vs/workbench/api/node/extHostWindow';
6161
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
62-
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
62+
import { IExtensionDescription, throwProposedApiError, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
6363
import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier';
6464
import * as vscode from 'vscode';
6565

6666
export interface IExtensionApiFactory {
6767
(extension: IExtensionDescription): typeof vscode;
6868
}
6969

70-
export function checkProposedApiEnabled(extension: IExtensionDescription): void {
71-
if (!extension.enableProposedApi) {
72-
throwProposedApiError(extension);
73-
}
74-
}
75-
76-
function throwProposedApiError(extension: IExtensionDescription): never {
77-
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}`);
78-
}
79-
8070
function proposedApiFunction<T>(extension: IExtensionDescription, fn: T): T {
8171
if (extension.enableProposedApi) {
8272
return fn;
@@ -461,10 +451,10 @@ export function createApiFactory(
461451
return extHostTerminalService.createTerminalRenderer(name);
462452
}),
463453
registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider<any>): vscode.Disposable {
464-
return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider);
454+
return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider, extension);
465455
},
466456
createTreeView(viewId: string, options: { treeDataProvider: vscode.TreeDataProvider<any> }): vscode.TreeView<any> {
467-
return extHostTreeViews.createTreeView(viewId, options);
457+
return extHostTreeViews.createTreeView(viewId, options, extension);
468458
},
469459
registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => {
470460
return extHostWebviews.registerWebviewPanelSerializer(viewType, serializer);

src/vs/workbench/api/node/extHost.protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export interface MainThreadTextEditorsShape extends IDisposable {
210210
}
211211

212212
export interface MainThreadTreeViewsShape extends IDisposable {
213-
$registerTreeViewDataProvider(treeViewId: string): void;
213+
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean }): void;
214214
$refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Thenable<void>;
215215
$reveal(treeViewId: string, treeItem: ITreeItem, parentChain: ITreeItem[], options: { select: boolean, focus: boolean }): Thenable<void>;
216216
}

src/vs/workbench/api/node/extHostTreeViews.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { TreeItemCollapsibleState, ThemeIcon } from 'vs/workbench/api/node/extHo
1717
import { isUndefinedOrNull, isString } from 'vs/base/common/types';
1818
import { equals } from 'vs/base/common/arrays';
1919
import { ILogService } from 'vs/platform/log/common/log';
20+
import { IExtensionDescription, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
2021

2122
type TreeItemHandle = string;
2223

@@ -59,16 +60,20 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
5960
});
6061
}
6162

62-
registerTreeDataProvider<T>(id: string, treeDataProvider: vscode.TreeDataProvider<T>): vscode.Disposable {
63-
const treeView = this.createTreeView(id, { treeDataProvider });
63+
registerTreeDataProvider<T>(id: string, treeDataProvider: vscode.TreeDataProvider<T>, extension: IExtensionDescription): vscode.Disposable {
64+
const treeView = this.createTreeView(id, { treeDataProvider }, extension);
6465
return { dispose: () => treeView.dispose() };
6566
}
6667

67-
createTreeView<T>(viewId: string, options: { treeDataProvider: vscode.TreeDataProvider<T> }): vscode.TreeView<T> {
68+
createTreeView<T>(viewId: string, options: vscode.TreeViewOptions<T>, extension: IExtensionDescription): vscode.TreeView<T> {
6869
if (!options || !options.treeDataProvider) {
6970
throw new Error('Options with treeDataProvider is mandatory');
7071
}
71-
const treeView = this.createExtHostTreeViewer(viewId, options.treeDataProvider);
72+
if (options.showCollapseAll) {
73+
checkProposedApiEnabled(extension);
74+
}
75+
76+
const treeView = this.createExtHostTreeViewer(viewId, options);
7277
return {
7378
get onDidCollapseElement() { return treeView.onDidCollapseElement; },
7479
get onDidExpandElement() { return treeView.onDidExpandElement; },
@@ -118,8 +123,8 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
118123
treeView.setVisible(isVisible);
119124
}
120125

121-
private createExtHostTreeViewer<T>(id: string, dataProvider: vscode.TreeDataProvider<T>): ExtHostTreeView<T> {
122-
const treeView = new ExtHostTreeView<T>(id, dataProvider, this._proxy, this.commands.converter, this.logService);
126+
private createExtHostTreeViewer<T>(id: string, options: vscode.TreeViewOptions<T>): ExtHostTreeView<T> {
127+
const treeView = new ExtHostTreeView<T>(id, options, this._proxy, this.commands.converter, this.logService);
123128
this.treeViews.set(id, treeView);
124129
return treeView;
125130
}
@@ -141,6 +146,8 @@ class ExtHostTreeView<T> extends Disposable {
141146
private static LABEL_HANDLE_PREFIX = '0';
142147
private static ID_HANDLE_PREFIX = '1';
143148

149+
private readonly dataProvider: vscode.TreeDataProvider<T>;
150+
144151
private roots: TreeNode[] | null = null;
145152
private elements: Map<TreeItemHandle, T> = new Map<TreeItemHandle, T>();
146153
private nodes: Map<T, TreeNode> = new Map<T, TreeNode>();
@@ -165,9 +172,10 @@ class ExtHostTreeView<T> extends Disposable {
165172

166173
private refreshPromise: Promise<void> = Promise.resolve(null);
167174

168-
constructor(private viewId: string, private dataProvider: vscode.TreeDataProvider<T>, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter, private logService: ILogService) {
175+
constructor(private viewId: string, options: vscode.TreeViewOptions<T>, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter, private logService: ILogService) {
169176
super();
170-
this.proxy.$registerTreeViewDataProvider(viewId);
177+
this.dataProvider = options.treeDataProvider;
178+
this.proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll });
171179
if (this.dataProvider.onDidChangeTreeData) {
172180
let refreshingPromise, promiseCallback;
173181
this._register(debounceEvent<T, T[]>(this.dataProvider.onDidChangeTreeData, (last, current) => {

src/vs/workbench/browser/parts/views/customView.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event';
88
import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
99
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1010
import { TPromise } from 'vs/base/common/winjs.base';
11-
import { IAction, IActionItem, ActionRunner } from 'vs/base/common/actions';
11+
import { IAction, IActionItem, ActionRunner, Action } from 'vs/base/common/actions';
1212
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
1313
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
1414
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
@@ -36,26 +36,25 @@ import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/v
3636
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
3737
import { localize } from 'vs/nls';
3838
import { timeout } from 'vs/base/common/async';
39+
import { CollapseAllAction } from 'vs/base/parts/tree/browser/treeDefaults';
3940

4041
export class CustomTreeViewPanel extends ViewletPanel {
4142

42-
private menus: TitleMenus;
4343
private treeViewer: ITreeViewer;
4444

4545
constructor(
4646
options: IViewletViewOptions,
4747
@INotificationService private notificationService: INotificationService,
4848
@IKeybindingService keybindingService: IKeybindingService,
4949
@IContextMenuService contextMenuService: IContextMenuService,
50-
@IInstantiationService private instantiationService: IInstantiationService,
5150
@IConfigurationService configurationService: IConfigurationService,
5251
@IViewsService viewsService: IViewsService,
5352
) {
5453
super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService);
55-
this.treeViewer = (<ICustomViewDescriptor>ViewsRegistry.getView(options.id)).treeViewer;
54+
const { treeViewer } = (<ICustomViewDescriptor>ViewsRegistry.getView(options.id));
55+
this.treeViewer = treeViewer;
56+
this.treeViewer.onDidChangeActions(() => this.updateActions(), this, this.disposables);
5657
this.disposables.push(toDisposable(() => this.treeViewer.setVisibility(false)));
57-
this.menus = this.instantiationService.createInstance(TitleMenus, this.id);
58-
this.menus.onDidChangeTitle(() => this.updateActions(), this, this.disposables);
5958
this.updateTreeVisibility();
6059
}
6160

@@ -83,11 +82,11 @@ export class CustomTreeViewPanel extends ViewletPanel {
8382
}
8483

8584
getActions(): IAction[] {
86-
return [...this.menus.getTitleActions()];
85+
return [...this.treeViewer.getPrimaryActions()];
8786
}
8887

8988
getSecondaryActions(): IAction[] {
90-
return this.menus.getTitleSecondaryActions();
89+
return [...this.treeViewer.getSecondaryActions()];
9190
}
9291

9392
getActionItem(action: IAction): IActionItem {
@@ -180,13 +179,15 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
180179
private activated: boolean = false;
181180
private _hasIconForParentNode = false;
182181
private _hasIconForLeafNode = false;
182+
private _showCollapseAllAction = false;
183183

184184
private domNode: HTMLElement;
185185
private treeContainer: HTMLElement;
186186
private message: HTMLDivElement;
187187
private tree: FileIconThemableWorkbenchTree;
188188
private root: ITreeItem;
189189
private elementsToRefresh: ITreeItem[] = [];
190+
private menus: TitleMenus;
190191

191192
private _dataProvider: ITreeViewDataProvider;
192193

@@ -202,6 +203,9 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
202203
private _onDidChangeVisibility: Emitter<boolean> = this._register(new Emitter<boolean>());
203204
readonly onDidChangeVisibility: Event<boolean> = this._onDidChangeVisibility.event;
204205

206+
private _onDidChangeActions: Emitter<void> = this._register(new Emitter<void>());
207+
readonly onDidChangeActions: Event<void> = this._onDidChangeActions.event;
208+
205209
constructor(
206210
private id: string,
207211
private container: ViewContainer,
@@ -214,6 +218,8 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
214218
) {
215219
super();
216220
this.root = new Root();
221+
this.menus = this._register(this.instantiationService.createInstance(TitleMenus, this.id));
222+
this._register(this.menus.onDidChangeTitle(() => this._onDidChangeActions.fire()));
217223
this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/));
218224
this._register(this.themeService.onThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/));
219225
this._register(this.configurationService.onDidChangeConfiguration(e => {
@@ -263,6 +269,30 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer {
263269
return this.isVisible;
264270
}
265271

272+
get showCollapseAllAction(): boolean {
273+
return this._showCollapseAllAction;
274+
}
275+
276+
set showCollapseAllAction(showCollapseAllAction: boolean) {
277+
if (this._showCollapseAllAction !== !!showCollapseAllAction) {
278+
this._showCollapseAllAction = !!showCollapseAllAction;
279+
this._onDidChangeActions.fire();
280+
}
281+
}
282+
283+
getPrimaryActions(): IAction[] {
284+
if (this.showCollapseAllAction) {
285+
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());
286+
return [...this.menus.getTitleActions(), collapseAllAction];
287+
} else {
288+
return this.menus.getTitleActions();
289+
}
290+
}
291+
292+
getSecondaryActions(): IAction[] {
293+
return this.menus.getTitleSecondaryActions();
294+
}
295+
266296
setVisibility(isVisible: boolean): void {
267297
isVisible = !!isVisible;
268298
if (this.isVisible === isVisible) {

src/vs/workbench/common/views.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService';
1717
import { values } from 'vs/base/common/map';
1818
import { Registry } from 'vs/platform/registry/common/platform';
1919
import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry';
20+
import { IAction } from 'vs/base/common/actions';
2021

2122
export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test';
2223

@@ -238,6 +239,10 @@ export interface ITreeViewer extends IDisposable {
238239

239240
dataProvider: ITreeViewDataProvider;
240241

242+
showCollapseAllAction: boolean;
243+
244+
readonly visible: boolean;
245+
241246
readonly onDidExpandItem: Event<ITreeItem>;
242247

243248
readonly onDidCollapseItem: Event<ITreeItem>;
@@ -246,7 +251,7 @@ export interface ITreeViewer extends IDisposable {
246251

247252
readonly onDidChangeVisibility: Event<boolean>;
248253

249-
readonly visible: boolean;
254+
readonly onDidChangeActions: Event<void>;
250255

251256
refresh(treeItems?: ITreeItem[]): TPromise<void>;
252257

@@ -261,6 +266,10 @@ export interface ITreeViewer extends IDisposable {
261266
getOptimalWidth(): number;
262267

263268
reveal(item: ITreeItem, parentChain: ITreeItem[], options: { select?: boolean }): TPromise<void>;
269+
270+
getPrimaryActions(): IAction[];
271+
272+
getSecondaryActions(): IAction[];
264273
}
265274

266275
export interface ICustomViewDescriptor extends IViewDescriptor {

src/vs/workbench/services/extensions/common/extensions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,13 @@ export interface IExtensionService {
206206
export interface ProfileSession {
207207
stop(): TPromise<IExtensionHostProfile>;
208208
}
209+
210+
export function checkProposedApiEnabled(extension: IExtensionDescription): void {
211+
if (!extension.enableProposedApi) {
212+
throwProposedApiError(extension);
213+
}
214+
}
215+
216+
export function throwProposedApiError(extension: IExtensionDescription): never {
217+
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}`);
218+
}

0 commit comments

Comments
 (0)