Path: blob/main/src/vs/platform/actions/browser/toolbar.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { addDisposableListener, getWindow } from '../../../base/browser/dom.js';6import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';7import { IToolBarOptions, ToggleMenuAction, ToolBar } from '../../../base/browser/ui/toolbar/toolbar.js';8import { IAction, Separator, SubmenuAction, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../base/common/actions.js';9import { coalesceInPlace } from '../../../base/common/arrays.js';10import { intersection } from '../../../base/common/collections.js';11import { BugIndicatingError } from '../../../base/common/errors.js';12import { Emitter } from '../../../base/common/event.js';13import { Iterable } from '../../../base/common/iterator.js';14import { DisposableStore } from '../../../base/common/lifecycle.js';15import { localize } from '../../../nls.js';16import { createActionViewItem, getActionBarActions } from './menuEntryActionViewItem.js';17import { IMenuActionOptions, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from '../common/actions.js';18import { createConfigureKeybindingAction } from '../common/menuService.js';19import { ICommandService } from '../../commands/common/commands.js';20import { IContextKeyService } from '../../contextkey/common/contextkey.js';21import { IContextMenuService } from '../../contextview/browser/contextView.js';22import { IKeybindingService } from '../../keybinding/common/keybinding.js';23import { ITelemetryService } from '../../telemetry/common/telemetry.js';24import { IActionViewItemService } from './actionViewItemService.js';25import { IInstantiationService } from '../../instantiation/common/instantiation.js';2627export const enum HiddenItemStrategy {28/** This toolbar doesn't support hiding*/29NoHide = -1,30/** Hidden items aren't shown anywhere */31Ignore = 0,32/** Hidden items move into the secondary group */33RenderInSecondaryGroup = 1,34}3536export type IWorkbenchToolBarOptions = IToolBarOptions & {3738/**39* Items of the primary group can be hidden. When this happens the item can40* - move into the secondary popup-menu, or41* - not be shown at all42*/43hiddenItemStrategy?: HiddenItemStrategy;4445/**46* Optional menu id which is used for a "Reset Menu" command. This should be the47* menu id that defines the contents of this workbench menu48*/49resetMenu?: MenuId;5051/**52* Optional menu id which items are used for the context menu of the toolbar.53*/54contextMenu?: MenuId;5556/**57* Optional options how menu actions are created and invoked58*/59menuOptions?: IMenuActionOptions;6061/**62* When set the `workbenchActionExecuted` is automatically send for each invoked action. The `from` property63* of the event will the passed `telemetrySource`-value64*/65telemetrySource?: string;6667/** This is controlled by the WorkbenchToolBar */68allowContextMenu?: never;6970/**71* Controls the overflow behavior of the primary group of toolbar. This isthe maximum number of items and id of72* items that should never overflow73*74*/75overflowBehavior?: { maxItems: number; exempted?: string[] };76};7778/**79* The `WorkbenchToolBar` does80* - support hiding of menu items81* - lookup keybindings for each actions automatically82* - send `workbenchActionExecuted`-events for each action83*84* See {@link MenuWorkbenchToolBar} for a toolbar that is backed by a menu.85*/86export class WorkbenchToolBar extends ToolBar {8788private readonly _sessionDisposables = this._store.add(new DisposableStore());8990constructor(91container: HTMLElement,92private _options: IWorkbenchToolBarOptions | undefined,93@IMenuService private readonly _menuService: IMenuService,94@IContextKeyService private readonly _contextKeyService: IContextKeyService,95@IContextMenuService private readonly _contextMenuService: IContextMenuService,96@IKeybindingService private readonly _keybindingService: IKeybindingService,97@ICommandService private readonly _commandService: ICommandService,98@ITelemetryService telemetryService: ITelemetryService,99) {100super(container, _contextMenuService, {101// defaults102getKeyBinding: (action) => _keybindingService.lookupKeybinding(action.id) ?? undefined,103// options (override defaults)104..._options,105// mandatory (overide options)106allowContextMenu: true,107skipTelemetry: typeof _options?.telemetrySource === 'string',108});109110// telemetry logic111const telemetrySource = _options?.telemetrySource;112if (telemetrySource) {113this._store.add(this.actionBar.onDidRun(e => telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>(114'workbenchActionExecuted',115{ id: e.action.id, from: telemetrySource })116));117}118}119120override setActions(_primary: readonly IAction[], _secondary: readonly IAction[] = [], menuIds?: readonly MenuId[]): void {121122this._sessionDisposables.clear();123const primary: Array<IAction | undefined> = _primary.slice(); // for hiding and overflow we set some items to undefined124const secondary = _secondary.slice();125const toggleActions: IAction[] = [];126let toggleActionsCheckedCount: number = 0;127128const extraSecondary: Array<IAction | undefined> = [];129130let someAreHidden = false;131// unless disabled, move all hidden items to secondary group or ignore them132if (this._options?.hiddenItemStrategy !== HiddenItemStrategy.NoHide) {133for (let i = 0; i < primary.length; i++) {134const action = primary[i];135if (!(action instanceof MenuItemAction) && !(action instanceof SubmenuItemAction)) {136// console.warn(`Action ${action.id}/${action.label} is not a MenuItemAction`);137continue;138}139if (!action.hideActions) {140continue;141}142143// collect all toggle actions144toggleActions.push(action.hideActions.toggle);145if (action.hideActions.toggle.checked) {146toggleActionsCheckedCount++;147}148149// hidden items move into overflow or ignore150if (action.hideActions.isHidden) {151someAreHidden = true;152primary[i] = undefined;153if (this._options?.hiddenItemStrategy !== HiddenItemStrategy.Ignore) {154extraSecondary[i] = action;155}156}157}158}159160// count for max161if (this._options?.overflowBehavior !== undefined) {162163const exemptedIds = intersection(new Set(this._options.overflowBehavior.exempted), Iterable.map(primary, a => a?.id));164const maxItems = this._options.overflowBehavior.maxItems - exemptedIds.size;165166let count = 0;167for (let i = 0; i < primary.length; i++) {168const action = primary[i];169if (!action) {170continue;171}172count++;173if (exemptedIds.has(action.id)) {174continue;175}176if (count >= maxItems) {177primary[i] = undefined;178extraSecondary[i] = action;179}180}181}182183// coalesce turns Array<IAction|undefined> into IAction[]184coalesceInPlace(primary);185coalesceInPlace(extraSecondary);186super.setActions(primary, Separator.join(extraSecondary, secondary));187188// add context menu for toggle and configure keybinding actions189if (toggleActions.length > 0 || primary.length > 0) {190this._sessionDisposables.add(addDisposableListener(this.getElement(), 'contextmenu', e => {191const event = new StandardMouseEvent(getWindow(this.getElement()), e);192193const action = this.getItemAction(event.target);194if (!(action)) {195return;196}197event.preventDefault();198event.stopPropagation();199200const primaryActions = [];201202// -- Configure Keybinding Action --203if (action instanceof MenuItemAction && action.menuKeybinding) {204primaryActions.push(action.menuKeybinding);205} else if (!(action instanceof SubmenuItemAction || action instanceof ToggleMenuAction)) {206// only enable the configure keybinding action for actions that support keybindings207const supportsKeybindings = !!this._keybindingService.lookupKeybinding(action.id);208primaryActions.push(createConfigureKeybindingAction(this._commandService, this._keybindingService, action.id, undefined, supportsKeybindings));209}210211// -- Hide Actions --212if (toggleActions.length > 0) {213let noHide = false;214215// last item cannot be hidden when using ignore strategy216if (toggleActionsCheckedCount === 1 && this._options?.hiddenItemStrategy === HiddenItemStrategy.Ignore) {217noHide = true;218for (let i = 0; i < toggleActions.length; i++) {219if (toggleActions[i].checked) {220toggleActions[i] = toAction({221id: action.id,222label: action.label,223checked: true,224enabled: false,225run() { }226});227break; // there is only one228}229}230}231232// add "hide foo" actions233if (!noHide && (action instanceof MenuItemAction || action instanceof SubmenuItemAction)) {234if (!action.hideActions) {235// no context menu for MenuItemAction instances that support no hiding236// those are fake actions and need to be cleaned up237return;238}239primaryActions.push(action.hideActions.hide);240241} else {242primaryActions.push(toAction({243id: 'label',244label: localize('hide', "Hide"),245enabled: false,246run() { }247}));248}249}250251const actions = Separator.join(primaryActions, toggleActions);252253// add "Reset Menu" action254if (this._options?.resetMenu && !menuIds) {255menuIds = [this._options.resetMenu];256}257if (someAreHidden && menuIds) {258actions.push(new Separator());259actions.push(toAction({260id: 'resetThisMenu',261label: localize('resetThisMenu', "Reset Menu"),262run: () => this._menuService.resetHiddenStates(menuIds)263}));264}265266if (actions.length === 0) {267return;268}269270this._contextMenuService.showContextMenu({271getAnchor: () => event,272getActions: () => actions,273// add context menu actions (iff appicable)274menuId: this._options?.contextMenu,275menuActionOptions: { renderShortTitle: true, ...this._options?.menuOptions },276skipTelemetry: typeof this._options?.telemetrySource === 'string',277contextKeyService: this._contextKeyService,278});279}));280}281}282}283284// ---- MenuWorkbenchToolBar -------------------------------------------------285286287export interface IToolBarRenderOptions {288/**289* Determines what groups are considered primary. Defaults to `navigation`. Items of the primary290* group are rendered with buttons and the rest is rendered in the secondary popup-menu.291*/292primaryGroup?: string | ((actionGroup: string) => boolean);293294/**295* Inlinse submenus with just a single item296*/297shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean;298299/**300* Should the primary group allow for separators.301*/302useSeparatorsInPrimaryActions?: boolean;303}304305export interface IMenuWorkbenchToolBarOptions extends IWorkbenchToolBarOptions {306307/**308* Optional options to configure how the toolbar renderes items.309*/310toolbarOptions?: IToolBarRenderOptions;311312/**313* Only `undefined` to disable the reset command is allowed, otherwise the menus314* id is used.315*/316resetMenu?: undefined;317318/**319* Customize the debounce delay for menu updates320*/321eventDebounceDelay?: number;322}323324/**325* A {@link WorkbenchToolBar workbench toolbar} that is purely driven from a {@link MenuId menu}-identifier.326*327* *Note* that Manual updates via `setActions` are NOT supported.328*/329export class MenuWorkbenchToolBar extends WorkbenchToolBar {330331private readonly _onDidChangeMenuItems = this._store.add(new Emitter<this>());332get onDidChangeMenuItems() { return this._onDidChangeMenuItems.event; }333334constructor(335container: HTMLElement,336menuId: MenuId,337options: IMenuWorkbenchToolBarOptions | undefined,338@IMenuService menuService: IMenuService,339@IContextKeyService contextKeyService: IContextKeyService,340@IContextMenuService contextMenuService: IContextMenuService,341@IKeybindingService keybindingService: IKeybindingService,342@ICommandService commandService: ICommandService,343@ITelemetryService telemetryService: ITelemetryService,344@IActionViewItemService actionViewService: IActionViewItemService,345@IInstantiationService instantiationService: IInstantiationService,346) {347super(container, {348resetMenu: menuId,349...options,350actionViewItemProvider: (action, opts) => {351let provider = actionViewService.lookUp(menuId, action instanceof SubmenuItemAction ? action.item.submenu.id : action.id);352if (!provider) {353provider = options?.actionViewItemProvider;354}355const viewItem = provider?.(action, opts, instantiationService, getWindow(container).vscodeWindowId);356if (viewItem) {357return viewItem;358}359return createActionViewItem(instantiationService, action, opts);360}361}, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService);362363// update logic364const menu = this._store.add(menuService.createMenu(menuId, contextKeyService, { emitEventsForSubmenuChanges: true, eventDebounceDelay: options?.eventDebounceDelay }));365const updateToolbar = () => {366const { primary, secondary } = getActionBarActions(367menu.getActions(options?.menuOptions),368options?.toolbarOptions?.primaryGroup,369options?.toolbarOptions?.shouldInlineSubmenu,370options?.toolbarOptions?.useSeparatorsInPrimaryActions371);372container.classList.toggle('has-no-actions', primary.length === 0 && secondary.length === 0);373super.setActions(primary, secondary);374};375376this._store.add(menu.onDidChange(() => {377updateToolbar();378this._onDidChangeMenuItems.fire(this);379}));380381this._store.add(actionViewService.onDidChange(e => {382if (e === menuId) {383updateToolbar();384}385}));386updateToolbar();387}388389/**390* @deprecated The WorkbenchToolBar does not support this method because it works with menus.391*/392override setActions(): void {393throw new BugIndicatingError('This toolbar is populated from a menu.');394}395}396397398