Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/actions/browser/toolbar.ts
3294 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { addDisposableListener, getWindow } from '../../../base/browser/dom.js';
7
import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';
8
import { IToolBarOptions, ToggleMenuAction, ToolBar } from '../../../base/browser/ui/toolbar/toolbar.js';
9
import { IAction, Separator, SubmenuAction, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../base/common/actions.js';
10
import { coalesceInPlace } from '../../../base/common/arrays.js';
11
import { intersection } from '../../../base/common/collections.js';
12
import { BugIndicatingError } from '../../../base/common/errors.js';
13
import { Emitter } from '../../../base/common/event.js';
14
import { Iterable } from '../../../base/common/iterator.js';
15
import { DisposableStore } from '../../../base/common/lifecycle.js';
16
import { localize } from '../../../nls.js';
17
import { createActionViewItem, getActionBarActions } from './menuEntryActionViewItem.js';
18
import { IMenuActionOptions, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from '../common/actions.js';
19
import { createConfigureKeybindingAction } from '../common/menuService.js';
20
import { ICommandService } from '../../commands/common/commands.js';
21
import { IContextKeyService } from '../../contextkey/common/contextkey.js';
22
import { IContextMenuService } from '../../contextview/browser/contextView.js';
23
import { IKeybindingService } from '../../keybinding/common/keybinding.js';
24
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
25
import { IActionViewItemService } from './actionViewItemService.js';
26
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
27
28
export const enum HiddenItemStrategy {
29
/** This toolbar doesn't support hiding*/
30
NoHide = -1,
31
/** Hidden items aren't shown anywhere */
32
Ignore = 0,
33
/** Hidden items move into the secondary group */
34
RenderInSecondaryGroup = 1,
35
}
36
37
export type IWorkbenchToolBarOptions = IToolBarOptions & {
38
39
/**
40
* Items of the primary group can be hidden. When this happens the item can
41
* - move into the secondary popup-menu, or
42
* - not be shown at all
43
*/
44
hiddenItemStrategy?: HiddenItemStrategy;
45
46
/**
47
* Optional menu id which is used for a "Reset Menu" command. This should be the
48
* menu id that defines the contents of this workbench menu
49
*/
50
resetMenu?: MenuId;
51
52
/**
53
* Optional menu id which items are used for the context menu of the toolbar.
54
*/
55
contextMenu?: MenuId;
56
57
/**
58
* Optional options how menu actions are created and invoked
59
*/
60
menuOptions?: IMenuActionOptions;
61
62
/**
63
* When set the `workbenchActionExecuted` is automatically send for each invoked action. The `from` property
64
* of the event will the passed `telemetrySource`-value
65
*/
66
telemetrySource?: string;
67
68
/** This is controlled by the WorkbenchToolBar */
69
allowContextMenu?: never;
70
71
/**
72
* Controls the overflow behavior of the primary group of toolbar. This isthe maximum number of items and id of
73
* items that should never overflow
74
*
75
*/
76
overflowBehavior?: { maxItems: number; exempted?: string[] };
77
};
78
79
/**
80
* The `WorkbenchToolBar` does
81
* - support hiding of menu items
82
* - lookup keybindings for each actions automatically
83
* - send `workbenchActionExecuted`-events for each action
84
*
85
* See {@link MenuWorkbenchToolBar} for a toolbar that is backed by a menu.
86
*/
87
export class WorkbenchToolBar extends ToolBar {
88
89
private readonly _sessionDisposables = this._store.add(new DisposableStore());
90
91
constructor(
92
container: HTMLElement,
93
private _options: IWorkbenchToolBarOptions | undefined,
94
@IMenuService private readonly _menuService: IMenuService,
95
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
96
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
97
@IKeybindingService private readonly _keybindingService: IKeybindingService,
98
@ICommandService private readonly _commandService: ICommandService,
99
@ITelemetryService telemetryService: ITelemetryService,
100
) {
101
super(container, _contextMenuService, {
102
// defaults
103
getKeyBinding: (action) => _keybindingService.lookupKeybinding(action.id) ?? undefined,
104
// options (override defaults)
105
..._options,
106
// mandatory (overide options)
107
allowContextMenu: true,
108
skipTelemetry: typeof _options?.telemetrySource === 'string',
109
});
110
111
// telemetry logic
112
const telemetrySource = _options?.telemetrySource;
113
if (telemetrySource) {
114
this._store.add(this.actionBar.onDidRun(e => telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>(
115
'workbenchActionExecuted',
116
{ id: e.action.id, from: telemetrySource })
117
));
118
}
119
}
120
121
override setActions(_primary: readonly IAction[], _secondary: readonly IAction[] = [], menuIds?: readonly MenuId[]): void {
122
123
this._sessionDisposables.clear();
124
const primary: Array<IAction | undefined> = _primary.slice(); // for hiding and overflow we set some items to undefined
125
const secondary = _secondary.slice();
126
const toggleActions: IAction[] = [];
127
let toggleActionsCheckedCount: number = 0;
128
129
const extraSecondary: Array<IAction | undefined> = [];
130
131
let someAreHidden = false;
132
// unless disabled, move all hidden items to secondary group or ignore them
133
if (this._options?.hiddenItemStrategy !== HiddenItemStrategy.NoHide) {
134
for (let i = 0; i < primary.length; i++) {
135
const action = primary[i];
136
if (!(action instanceof MenuItemAction) && !(action instanceof SubmenuItemAction)) {
137
// console.warn(`Action ${action.id}/${action.label} is not a MenuItemAction`);
138
continue;
139
}
140
if (!action.hideActions) {
141
continue;
142
}
143
144
// collect all toggle actions
145
toggleActions.push(action.hideActions.toggle);
146
if (action.hideActions.toggle.checked) {
147
toggleActionsCheckedCount++;
148
}
149
150
// hidden items move into overflow or ignore
151
if (action.hideActions.isHidden) {
152
someAreHidden = true;
153
primary[i] = undefined;
154
if (this._options?.hiddenItemStrategy !== HiddenItemStrategy.Ignore) {
155
extraSecondary[i] = action;
156
}
157
}
158
}
159
}
160
161
// count for max
162
if (this._options?.overflowBehavior !== undefined) {
163
164
const exemptedIds = intersection(new Set(this._options.overflowBehavior.exempted), Iterable.map(primary, a => a?.id));
165
const maxItems = this._options.overflowBehavior.maxItems - exemptedIds.size;
166
167
let count = 0;
168
for (let i = 0; i < primary.length; i++) {
169
const action = primary[i];
170
if (!action) {
171
continue;
172
}
173
count++;
174
if (exemptedIds.has(action.id)) {
175
continue;
176
}
177
if (count >= maxItems) {
178
primary[i] = undefined;
179
extraSecondary[i] = action;
180
}
181
}
182
}
183
184
// coalesce turns Array<IAction|undefined> into IAction[]
185
coalesceInPlace(primary);
186
coalesceInPlace(extraSecondary);
187
super.setActions(primary, Separator.join(extraSecondary, secondary));
188
189
// add context menu for toggle and configure keybinding actions
190
if (toggleActions.length > 0 || primary.length > 0) {
191
this._sessionDisposables.add(addDisposableListener(this.getElement(), 'contextmenu', e => {
192
const event = new StandardMouseEvent(getWindow(this.getElement()), e);
193
194
const action = this.getItemAction(event.target);
195
if (!(action)) {
196
return;
197
}
198
event.preventDefault();
199
event.stopPropagation();
200
201
const primaryActions = [];
202
203
// -- Configure Keybinding Action --
204
if (action instanceof MenuItemAction && action.menuKeybinding) {
205
primaryActions.push(action.menuKeybinding);
206
} else if (!(action instanceof SubmenuItemAction || action instanceof ToggleMenuAction)) {
207
// only enable the configure keybinding action for actions that support keybindings
208
const supportsKeybindings = !!this._keybindingService.lookupKeybinding(action.id);
209
primaryActions.push(createConfigureKeybindingAction(this._commandService, this._keybindingService, action.id, undefined, supportsKeybindings));
210
}
211
212
// -- Hide Actions --
213
if (toggleActions.length > 0) {
214
let noHide = false;
215
216
// last item cannot be hidden when using ignore strategy
217
if (toggleActionsCheckedCount === 1 && this._options?.hiddenItemStrategy === HiddenItemStrategy.Ignore) {
218
noHide = true;
219
for (let i = 0; i < toggleActions.length; i++) {
220
if (toggleActions[i].checked) {
221
toggleActions[i] = toAction({
222
id: action.id,
223
label: action.label,
224
checked: true,
225
enabled: false,
226
run() { }
227
});
228
break; // there is only one
229
}
230
}
231
}
232
233
// add "hide foo" actions
234
if (!noHide && (action instanceof MenuItemAction || action instanceof SubmenuItemAction)) {
235
if (!action.hideActions) {
236
// no context menu for MenuItemAction instances that support no hiding
237
// those are fake actions and need to be cleaned up
238
return;
239
}
240
primaryActions.push(action.hideActions.hide);
241
242
} else {
243
primaryActions.push(toAction({
244
id: 'label',
245
label: localize('hide', "Hide"),
246
enabled: false,
247
run() { }
248
}));
249
}
250
}
251
252
const actions = Separator.join(primaryActions, toggleActions);
253
254
// add "Reset Menu" action
255
if (this._options?.resetMenu && !menuIds) {
256
menuIds = [this._options.resetMenu];
257
}
258
if (someAreHidden && menuIds) {
259
actions.push(new Separator());
260
actions.push(toAction({
261
id: 'resetThisMenu',
262
label: localize('resetThisMenu', "Reset Menu"),
263
run: () => this._menuService.resetHiddenStates(menuIds)
264
}));
265
}
266
267
if (actions.length === 0) {
268
return;
269
}
270
271
this._contextMenuService.showContextMenu({
272
getAnchor: () => event,
273
getActions: () => actions,
274
// add context menu actions (iff appicable)
275
menuId: this._options?.contextMenu,
276
menuActionOptions: { renderShortTitle: true, ...this._options?.menuOptions },
277
skipTelemetry: typeof this._options?.telemetrySource === 'string',
278
contextKeyService: this._contextKeyService,
279
});
280
}));
281
}
282
}
283
}
284
285
// ---- MenuWorkbenchToolBar -------------------------------------------------
286
287
288
export interface IToolBarRenderOptions {
289
/**
290
* Determines what groups are considered primary. Defaults to `navigation`. Items of the primary
291
* group are rendered with buttons and the rest is rendered in the secondary popup-menu.
292
*/
293
primaryGroup?: string | ((actionGroup: string) => boolean);
294
295
/**
296
* Inlinse submenus with just a single item
297
*/
298
shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean;
299
300
/**
301
* Should the primary group allow for separators.
302
*/
303
useSeparatorsInPrimaryActions?: boolean;
304
}
305
306
export interface IMenuWorkbenchToolBarOptions extends IWorkbenchToolBarOptions {
307
308
/**
309
* Optional options to configure how the toolbar renderes items.
310
*/
311
toolbarOptions?: IToolBarRenderOptions;
312
313
/**
314
* Only `undefined` to disable the reset command is allowed, otherwise the menus
315
* id is used.
316
*/
317
resetMenu?: undefined;
318
319
/**
320
* Customize the debounce delay for menu updates
321
*/
322
eventDebounceDelay?: number;
323
}
324
325
/**
326
* A {@link WorkbenchToolBar workbench toolbar} that is purely driven from a {@link MenuId menu}-identifier.
327
*
328
* *Note* that Manual updates via `setActions` are NOT supported.
329
*/
330
export class MenuWorkbenchToolBar extends WorkbenchToolBar {
331
332
private readonly _onDidChangeMenuItems = this._store.add(new Emitter<this>());
333
get onDidChangeMenuItems() { return this._onDidChangeMenuItems.event; }
334
335
constructor(
336
container: HTMLElement,
337
menuId: MenuId,
338
options: IMenuWorkbenchToolBarOptions | undefined,
339
@IMenuService menuService: IMenuService,
340
@IContextKeyService contextKeyService: IContextKeyService,
341
@IContextMenuService contextMenuService: IContextMenuService,
342
@IKeybindingService keybindingService: IKeybindingService,
343
@ICommandService commandService: ICommandService,
344
@ITelemetryService telemetryService: ITelemetryService,
345
@IActionViewItemService actionViewService: IActionViewItemService,
346
@IInstantiationService instantiationService: IInstantiationService,
347
) {
348
super(container, {
349
resetMenu: menuId,
350
...options,
351
actionViewItemProvider: (action, opts) => {
352
let provider = actionViewService.lookUp(menuId, action instanceof SubmenuItemAction ? action.item.submenu.id : action.id);
353
if (!provider) {
354
provider = options?.actionViewItemProvider;
355
}
356
const viewItem = provider?.(action, opts, instantiationService, getWindow(container).vscodeWindowId);
357
if (viewItem) {
358
return viewItem;
359
}
360
return createActionViewItem(instantiationService, action, opts);
361
}
362
}, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService);
363
364
// update logic
365
const menu = this._store.add(menuService.createMenu(menuId, contextKeyService, { emitEventsForSubmenuChanges: true, eventDebounceDelay: options?.eventDebounceDelay }));
366
const updateToolbar = () => {
367
const { primary, secondary } = getActionBarActions(
368
menu.getActions(options?.menuOptions),
369
options?.toolbarOptions?.primaryGroup,
370
options?.toolbarOptions?.shouldInlineSubmenu,
371
options?.toolbarOptions?.useSeparatorsInPrimaryActions
372
);
373
container.classList.toggle('has-no-actions', primary.length === 0 && secondary.length === 0);
374
super.setActions(primary, secondary);
375
};
376
377
this._store.add(menu.onDidChange(() => {
378
updateToolbar();
379
this._onDidChangeMenuItems.fire(this);
380
}));
381
382
this._store.add(actionViewService.onDidChange(e => {
383
if (e === menuId) {
384
updateToolbar();
385
}
386
}));
387
updateToolbar();
388
}
389
390
/**
391
* @deprecated The WorkbenchToolBar does not support this method because it works with menus.
392
*/
393
override setActions(): void {
394
throw new BugIndicatingError('This toolbar is populated from a menu.');
395
}
396
}
397
398