mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-02 09:35:40 -05:00
Merge from vscode 731f9c25632dbbf01ee3a7892ad9d2791fe0260c
This commit is contained in:
@@ -3,10 +3,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IAction, IActionRunner } from 'vs/base/common/actions';
|
||||
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IAction, IActionRunner, IActionViewItem } from 'vs/base/common/actions';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { SubmenuAction } from 'vs/base/browser/ui/menu/menu';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
|
||||
export interface IContextMenuEvent {
|
||||
@@ -16,15 +14,9 @@ export interface IContextMenuEvent {
|
||||
readonly metaKey?: boolean;
|
||||
}
|
||||
|
||||
export class ContextSubMenu extends SubmenuAction {
|
||||
constructor(label: string, public entries: Array<ContextSubMenu | IAction>) {
|
||||
super(label, entries, 'contextsubmenu');
|
||||
}
|
||||
}
|
||||
|
||||
export interface IContextMenuDelegate {
|
||||
getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number; };
|
||||
getActions(): ReadonlyArray<IAction | ContextSubMenu>;
|
||||
getActions(): IAction[];
|
||||
getCheckedActionsRepresentation?(action: IAction): 'radio' | 'checkbox';
|
||||
getActionViewItem?(action: IAction): IActionViewItem | undefined;
|
||||
getActionsContext?(event?: IContextMenuEvent): any;
|
||||
@@ -36,3 +28,7 @@ export interface IContextMenuDelegate {
|
||||
anchorAlignment?: AnchorAlignment;
|
||||
domForShadowRoot?: HTMLElement;
|
||||
}
|
||||
|
||||
export interface IContextMenuProvider {
|
||||
showContextMenu(delegate: IContextMenuDelegate): void;
|
||||
}
|
||||
|
||||
406
src/vs/base/browser/ui/actionbar/actionViewItems.ts
Normal file
406
src/vs/base/browser/ui/actionbar/actionViewItems.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./actionbar';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { SelectBox, ISelectOptionItem, ISelectBoxOptions } from 'vs/base/browser/ui/selectBox/selectBox';
|
||||
import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner, Separator, IActionViewItem } from 'vs/base/common/actions';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { EventType, Gesture } from 'vs/base/browser/touch';
|
||||
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { DataTransfers } from 'vs/base/browser/dnd';
|
||||
import { isFirefox } from 'vs/base/browser/browser';
|
||||
|
||||
export interface IBaseActionViewItemOptions {
|
||||
draggable?: boolean;
|
||||
isMenu?: boolean;
|
||||
useEventAsContext?: boolean;
|
||||
}
|
||||
|
||||
export class BaseActionViewItem extends Disposable implements IActionViewItem {
|
||||
|
||||
element: HTMLElement | undefined;
|
||||
|
||||
_context: any;
|
||||
_action: IAction;
|
||||
|
||||
private _actionRunner: IActionRunner | undefined;
|
||||
|
||||
constructor(context: any, action: IAction, protected options: IBaseActionViewItemOptions = {}) {
|
||||
super();
|
||||
|
||||
this._context = context || this;
|
||||
this._action = action;
|
||||
|
||||
if (action instanceof Action) {
|
||||
this._register(action.onDidChange(event => {
|
||||
if (!this.element) {
|
||||
// we have not been rendered yet, so there
|
||||
// is no point in updating the UI
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleActionChangeEvent(event);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private handleActionChangeEvent(event: IActionChangeEvent): void {
|
||||
if (event.enabled !== undefined) {
|
||||
this.updateEnabled();
|
||||
}
|
||||
|
||||
if (event.checked !== undefined) {
|
||||
this.updateChecked();
|
||||
}
|
||||
|
||||
if (event.class !== undefined) {
|
||||
this.updateClass();
|
||||
}
|
||||
|
||||
if (event.label !== undefined) {
|
||||
this.updateLabel();
|
||||
this.updateTooltip();
|
||||
}
|
||||
|
||||
if (event.tooltip !== undefined) {
|
||||
this.updateTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
get actionRunner(): IActionRunner {
|
||||
if (!this._actionRunner) {
|
||||
this._actionRunner = this._register(new ActionRunner());
|
||||
}
|
||||
|
||||
return this._actionRunner;
|
||||
}
|
||||
|
||||
set actionRunner(actionRunner: IActionRunner) {
|
||||
this._actionRunner = actionRunner;
|
||||
}
|
||||
|
||||
getAction(): IAction {
|
||||
return this._action;
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this._action.enabled;
|
||||
}
|
||||
|
||||
setActionContext(newContext: unknown): void {
|
||||
this._context = newContext;
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
const element = this.element = container;
|
||||
this._register(Gesture.addTarget(container));
|
||||
|
||||
const enableDragging = this.options && this.options.draggable;
|
||||
if (enableDragging) {
|
||||
container.draggable = true;
|
||||
|
||||
if (isFirefox) {
|
||||
// Firefox: requires to set a text data transfer to get going
|
||||
this._register(DOM.addDisposableListener(container, DOM.EventType.DRAG_START, e => e.dataTransfer?.setData(DataTransfers.TEXT, this._action.label)));
|
||||
}
|
||||
}
|
||||
|
||||
this._register(DOM.addDisposableListener(element, EventType.Tap, e => this.onClick(e)));
|
||||
|
||||
this._register(DOM.addDisposableListener(element, DOM.EventType.MOUSE_DOWN, e => {
|
||||
if (!enableDragging) {
|
||||
DOM.EventHelper.stop(e, true); // do not run when dragging is on because that would disable it
|
||||
}
|
||||
|
||||
if (this._action.enabled && e.button === 0) {
|
||||
DOM.addClass(element, 'active');
|
||||
}
|
||||
}));
|
||||
|
||||
if (platform.isMacintosh) {
|
||||
// macOS: allow to trigger the button when holding Ctrl+key and pressing the
|
||||
// main mouse button. This is for scenarios where e.g. some interaction forces
|
||||
// the Ctrl+key to be pressed and hold but the user still wants to interact
|
||||
// with the actions (for example quick access in quick navigation mode).
|
||||
this._register(DOM.addDisposableListener(element, DOM.EventType.CONTEXT_MENU, e => {
|
||||
if (e.button === 0 && e.ctrlKey === true) {
|
||||
this.onClick(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this._register(DOM.addDisposableListener(element, DOM.EventType.CLICK, e => {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
// See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
|
||||
// > Writing to the clipboard
|
||||
// > You can use the "cut" and "copy" commands without any special
|
||||
// permission if you are using them in a short-lived event handler
|
||||
// for a user action (for example, a click handler).
|
||||
|
||||
// => to get the Copy and Paste context menu actions working on Firefox,
|
||||
// there should be no timeout here
|
||||
if (this.options && this.options.isMenu) {
|
||||
this.onClick(e);
|
||||
} else {
|
||||
platform.setImmediate(() => this.onClick(e));
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(element, DOM.EventType.DBLCLICK, e => {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
}));
|
||||
|
||||
[DOM.EventType.MOUSE_UP, DOM.EventType.MOUSE_OUT].forEach(event => {
|
||||
this._register(DOM.addDisposableListener(element, event, e => {
|
||||
DOM.EventHelper.stop(e);
|
||||
DOM.removeClass(element, 'active');
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
onClick(event: DOM.EventLike): void {
|
||||
DOM.EventHelper.stop(event, true);
|
||||
|
||||
const context = types.isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : undefined : this._context;
|
||||
this.actionRunner.run(this._action, context);
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
if (this.element) {
|
||||
this.element.focus();
|
||||
DOM.addClass(this.element, 'focused');
|
||||
}
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
if (this.element) {
|
||||
this.element.blur();
|
||||
DOM.removeClass(this.element, 'focused');
|
||||
}
|
||||
}
|
||||
|
||||
protected updateEnabled(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateLabel(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateTooltip(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateClass(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateChecked(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.element) {
|
||||
DOM.removeNode(this.element);
|
||||
this.element = undefined;
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IActionViewItemOptions extends IBaseActionViewItemOptions {
|
||||
icon?: boolean;
|
||||
label?: boolean;
|
||||
keybinding?: string | null;
|
||||
}
|
||||
|
||||
export class ActionViewItem extends BaseActionViewItem {
|
||||
|
||||
protected label: HTMLElement | undefined;
|
||||
protected options: IActionViewItemOptions;
|
||||
|
||||
private cssClass?: string;
|
||||
|
||||
constructor(context: unknown, action: IAction, options: IActionViewItemOptions = {}) {
|
||||
super(context, action, options);
|
||||
|
||||
this.options = options;
|
||||
this.options.icon = options.icon !== undefined ? options.icon : false;
|
||||
this.options.label = options.label !== undefined ? options.label : true;
|
||||
this.cssClass = '';
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
|
||||
if (this.element) {
|
||||
this.label = DOM.append(this.element, DOM.$('a.action-label'));
|
||||
}
|
||||
|
||||
if (this.label) {
|
||||
if (this._action.id === Separator.ID) {
|
||||
this.label.setAttribute('role', 'presentation'); // A separator is a presentation item
|
||||
} else {
|
||||
if (this.options.isMenu) {
|
||||
this.label.setAttribute('role', 'menuitem');
|
||||
} else {
|
||||
this.label.setAttribute('role', 'button');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.label && this.options.keybinding && this.element) {
|
||||
DOM.append(this.element, DOM.$('span.keybinding')).textContent = this.options.keybinding;
|
||||
}
|
||||
|
||||
this.updateClass();
|
||||
this.updateLabel();
|
||||
this.updateTooltip();
|
||||
this.updateEnabled();
|
||||
this.updateChecked();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
super.focus();
|
||||
|
||||
if (this.label) {
|
||||
this.label.focus();
|
||||
}
|
||||
}
|
||||
|
||||
updateLabel(): void {
|
||||
if (this.options.label && this.label) {
|
||||
this.label.textContent = this.getAction().label;
|
||||
}
|
||||
}
|
||||
|
||||
updateTooltip(): void {
|
||||
let title: string | null = null;
|
||||
|
||||
if (this.getAction().tooltip) {
|
||||
title = this.getAction().tooltip;
|
||||
|
||||
} else if (!this.options.label && this.getAction().label && this.options.icon) {
|
||||
title = this.getAction().label;
|
||||
|
||||
if (this.options.keybinding) {
|
||||
title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding);
|
||||
}
|
||||
}
|
||||
|
||||
if (title && this.label) {
|
||||
this.label.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
updateClass(): void {
|
||||
if (this.cssClass && this.label) {
|
||||
DOM.removeClasses(this.label, this.cssClass);
|
||||
}
|
||||
|
||||
if (this.options.icon) {
|
||||
this.cssClass = this.getAction().class;
|
||||
|
||||
if (this.label) {
|
||||
DOM.addClass(this.label, 'codicon');
|
||||
if (this.cssClass) {
|
||||
DOM.addClasses(this.label, this.cssClass);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateEnabled();
|
||||
} else {
|
||||
if (this.label) {
|
||||
DOM.removeClass(this.label, 'codicon');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateEnabled(): void {
|
||||
if (this.getAction().enabled) {
|
||||
if (this.label) {
|
||||
this.label.removeAttribute('aria-disabled');
|
||||
DOM.removeClass(this.label, 'disabled');
|
||||
this.label.tabIndex = 0;
|
||||
}
|
||||
|
||||
if (this.element) {
|
||||
DOM.removeClass(this.element, 'disabled');
|
||||
}
|
||||
} else {
|
||||
if (this.label) {
|
||||
this.label.setAttribute('aria-disabled', 'true');
|
||||
DOM.addClass(this.label, 'disabled');
|
||||
DOM.removeTabIndexAndUpdateFocus(this.label);
|
||||
}
|
||||
|
||||
if (this.element) {
|
||||
DOM.addClass(this.element, 'disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateChecked(): void {
|
||||
if (this.label) {
|
||||
if (this.getAction().checked) {
|
||||
DOM.addClass(this.label, 'checked');
|
||||
} else {
|
||||
DOM.removeClass(this.label, 'checked');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectActionViewItem extends BaseActionViewItem {
|
||||
protected selectBox: SelectBox;
|
||||
|
||||
constructor(ctx: unknown, action: IAction, options: ISelectOptionItem[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions) {
|
||||
super(ctx, action);
|
||||
|
||||
this.selectBox = new SelectBox(options, selected, contextViewProvider, undefined, selectBoxOptions);
|
||||
|
||||
this._register(this.selectBox);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
setOptions(options: ISelectOptionItem[], selected?: number): void {
|
||||
this.selectBox.setOptions(options, selected);
|
||||
}
|
||||
|
||||
select(index: number): void {
|
||||
this.selectBox.select(index);
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.selectBox.onDidSelect(e => {
|
||||
this.actionRunner.run(this._action, this.getActionContext(e.selected, e.index));
|
||||
}));
|
||||
}
|
||||
|
||||
protected getActionContext(option: string, index: number) {
|
||||
return option;
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
if (this.selectBox) {
|
||||
this.selectBox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
if (this.selectBox) {
|
||||
this.selectBox.blur();
|
||||
}
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
this.selectBox.render(container);
|
||||
}
|
||||
}
|
||||
@@ -4,382 +4,14 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./actionbar';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { SelectBox, ISelectOptionItem, ISelectBoxOptions } from 'vs/base/browser/ui/selectBox/selectBox';
|
||||
import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner, IRunEvent } from 'vs/base/common/actions';
|
||||
import { Disposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IAction, IActionRunner, ActionRunner, IRunEvent, Separator, IActionViewItem, IActionViewItemProvider } from 'vs/base/common/actions';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { EventType, Gesture } from 'vs/base/browser/touch';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { DataTransfers } from 'vs/base/browser/dnd';
|
||||
import { isFirefox } from 'vs/base/browser/browser';
|
||||
|
||||
export interface IActionViewItem extends IDisposable {
|
||||
actionRunner: IActionRunner;
|
||||
setActionContext(context: any): void;
|
||||
render(element: HTMLElement): void;
|
||||
isEnabled(): boolean;
|
||||
focus(fromRight?: boolean): void;
|
||||
blur(): void;
|
||||
}
|
||||
|
||||
export interface IBaseActionViewItemOptions {
|
||||
draggable?: boolean;
|
||||
isMenu?: boolean;
|
||||
useEventAsContext?: boolean;
|
||||
}
|
||||
|
||||
export class BaseActionViewItem extends Disposable implements IActionViewItem {
|
||||
|
||||
element: HTMLElement | undefined;
|
||||
|
||||
_context: any;
|
||||
_action: IAction;
|
||||
|
||||
private _actionRunner: IActionRunner | undefined;
|
||||
|
||||
constructor(context: any, action: IAction, protected options?: IBaseActionViewItemOptions) {
|
||||
super();
|
||||
|
||||
this._context = context || this;
|
||||
this._action = action;
|
||||
|
||||
if (action instanceof Action) {
|
||||
this._register(action.onDidChange(event => {
|
||||
if (!this.element) {
|
||||
// we have not been rendered yet, so there
|
||||
// is no point in updating the UI
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleActionChangeEvent(event);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private handleActionChangeEvent(event: IActionChangeEvent): void {
|
||||
if (event.enabled !== undefined) {
|
||||
this.updateEnabled();
|
||||
}
|
||||
|
||||
if (event.checked !== undefined) {
|
||||
this.updateChecked();
|
||||
}
|
||||
|
||||
if (event.class !== undefined) {
|
||||
this.updateClass();
|
||||
}
|
||||
|
||||
if (event.label !== undefined) {
|
||||
this.updateLabel();
|
||||
this.updateTooltip();
|
||||
}
|
||||
|
||||
if (event.tooltip !== undefined) {
|
||||
this.updateTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
get actionRunner(): IActionRunner {
|
||||
if (!this._actionRunner) {
|
||||
this._actionRunner = this._register(new ActionRunner());
|
||||
}
|
||||
|
||||
return this._actionRunner;
|
||||
}
|
||||
|
||||
set actionRunner(actionRunner: IActionRunner) {
|
||||
this._actionRunner = actionRunner;
|
||||
}
|
||||
|
||||
getAction(): IAction {
|
||||
return this._action;
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this._action.enabled;
|
||||
}
|
||||
|
||||
setActionContext(newContext: unknown): void {
|
||||
this._context = newContext;
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
const element = this.element = container;
|
||||
this._register(Gesture.addTarget(container));
|
||||
|
||||
const enableDragging = this.options && this.options.draggable;
|
||||
if (enableDragging) {
|
||||
container.draggable = true;
|
||||
|
||||
if (isFirefox) {
|
||||
// Firefox: requires to set a text data transfer to get going
|
||||
this._register(DOM.addDisposableListener(container, DOM.EventType.DRAG_START, e => e.dataTransfer?.setData(DataTransfers.TEXT, this._action.label)));
|
||||
}
|
||||
}
|
||||
|
||||
this._register(DOM.addDisposableListener(element, EventType.Tap, e => this.onClick(e)));
|
||||
|
||||
this._register(DOM.addDisposableListener(element, DOM.EventType.MOUSE_DOWN, e => {
|
||||
if (!enableDragging) {
|
||||
DOM.EventHelper.stop(e, true); // do not run when dragging is on because that would disable it
|
||||
}
|
||||
|
||||
if (this._action.enabled && e.button === 0) {
|
||||
DOM.addClass(element, 'active');
|
||||
}
|
||||
}));
|
||||
|
||||
if (platform.isMacintosh) {
|
||||
// macOS: allow to trigger the button when holding Ctrl+key and pressing the
|
||||
// main mouse button. This is for scenarios where e.g. some interaction forces
|
||||
// the Ctrl+key to be pressed and hold but the user still wants to interact
|
||||
// with the actions (for example quick access in quick navigation mode).
|
||||
this._register(DOM.addDisposableListener(element, DOM.EventType.CONTEXT_MENU, e => {
|
||||
if (e.button === 0 && e.ctrlKey === true) {
|
||||
this.onClick(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this._register(DOM.addDisposableListener(element, DOM.EventType.CLICK, e => {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
// See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
|
||||
// > Writing to the clipboard
|
||||
// > You can use the "cut" and "copy" commands without any special
|
||||
// permission if you are using them in a short-lived event handler
|
||||
// for a user action (for example, a click handler).
|
||||
|
||||
// => to get the Copy and Paste context menu actions working on Firefox,
|
||||
// there should be no timeout here
|
||||
if (this.options && this.options.isMenu) {
|
||||
this.onClick(e);
|
||||
} else {
|
||||
platform.setImmediate(() => this.onClick(e));
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(element, DOM.EventType.DBLCLICK, e => {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
}));
|
||||
|
||||
[DOM.EventType.MOUSE_UP, DOM.EventType.MOUSE_OUT].forEach(event => {
|
||||
this._register(DOM.addDisposableListener(element, event, e => {
|
||||
DOM.EventHelper.stop(e);
|
||||
DOM.removeClass(element, 'active');
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
onClick(event: DOM.EventLike): void {
|
||||
DOM.EventHelper.stop(event, true);
|
||||
|
||||
const context = types.isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : undefined : this._context;
|
||||
this.actionRunner.run(this._action, context);
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
if (this.element) {
|
||||
this.element.focus();
|
||||
DOM.addClass(this.element, 'focused');
|
||||
}
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
if (this.element) {
|
||||
this.element.blur();
|
||||
DOM.removeClass(this.element, 'focused');
|
||||
}
|
||||
}
|
||||
|
||||
protected updateEnabled(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateLabel(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateTooltip(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateClass(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateChecked(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.element) {
|
||||
DOM.removeNode(this.element);
|
||||
this.element = undefined;
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class Separator extends Action {
|
||||
|
||||
static readonly ID = 'vs.actions.separator';
|
||||
|
||||
constructor(label?: string) {
|
||||
super(Separator.ID, label, label ? 'separator text' : 'separator');
|
||||
this.checked = false;
|
||||
this.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IActionViewItemOptions extends IBaseActionViewItemOptions {
|
||||
icon?: boolean;
|
||||
label?: boolean;
|
||||
keybinding?: string | null;
|
||||
}
|
||||
|
||||
export class ActionViewItem extends BaseActionViewItem {
|
||||
|
||||
protected label: HTMLElement | undefined;
|
||||
protected options: IActionViewItemOptions;
|
||||
|
||||
private cssClass?: string;
|
||||
|
||||
constructor(context: unknown, action: IAction, options: IActionViewItemOptions = {}) {
|
||||
super(context, action, options);
|
||||
|
||||
this.options = options;
|
||||
this.options.icon = options.icon !== undefined ? options.icon : false;
|
||||
this.options.label = options.label !== undefined ? options.label : true;
|
||||
this.cssClass = '';
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
|
||||
if (this.element) {
|
||||
this.label = DOM.append(this.element, DOM.$('a.action-label'));
|
||||
}
|
||||
|
||||
if (this.label) {
|
||||
if (this._action.id === Separator.ID) {
|
||||
this.label.setAttribute('role', 'presentation'); // A separator is a presentation item
|
||||
} else {
|
||||
if (this.options.isMenu) {
|
||||
this.label.setAttribute('role', 'menuitem');
|
||||
} else {
|
||||
this.label.setAttribute('role', 'button');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.label && this.options.keybinding && this.element) {
|
||||
DOM.append(this.element, DOM.$('span.keybinding')).textContent = this.options.keybinding;
|
||||
}
|
||||
|
||||
this.updateClass();
|
||||
this.updateLabel();
|
||||
this.updateTooltip();
|
||||
this.updateEnabled();
|
||||
this.updateChecked();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
super.focus();
|
||||
|
||||
if (this.label) {
|
||||
this.label.focus();
|
||||
}
|
||||
}
|
||||
|
||||
updateLabel(): void {
|
||||
if (this.options.label && this.label) {
|
||||
this.label.textContent = this.getAction().label;
|
||||
}
|
||||
}
|
||||
|
||||
updateTooltip(): void {
|
||||
let title: string | null = null;
|
||||
|
||||
if (this.getAction().tooltip) {
|
||||
title = this.getAction().tooltip;
|
||||
|
||||
} else if (!this.options.label && this.getAction().label && this.options.icon) {
|
||||
title = this.getAction().label;
|
||||
|
||||
if (this.options.keybinding) {
|
||||
title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding);
|
||||
}
|
||||
}
|
||||
|
||||
if (title && this.label) {
|
||||
this.label.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
updateClass(): void {
|
||||
if (this.cssClass && this.label) {
|
||||
DOM.removeClasses(this.label, this.cssClass);
|
||||
}
|
||||
|
||||
if (this.options.icon) {
|
||||
this.cssClass = this.getAction().class;
|
||||
|
||||
if (this.label) {
|
||||
DOM.addClass(this.label, 'codicon');
|
||||
if (this.cssClass) {
|
||||
DOM.addClasses(this.label, this.cssClass);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateEnabled();
|
||||
} else {
|
||||
if (this.label) {
|
||||
DOM.removeClass(this.label, 'codicon');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateEnabled(): void {
|
||||
if (this.getAction().enabled) {
|
||||
if (this.label) {
|
||||
this.label.removeAttribute('aria-disabled');
|
||||
DOM.removeClass(this.label, 'disabled');
|
||||
this.label.tabIndex = 0;
|
||||
}
|
||||
|
||||
if (this.element) {
|
||||
DOM.removeClass(this.element, 'disabled');
|
||||
}
|
||||
} else {
|
||||
if (this.label) {
|
||||
this.label.setAttribute('aria-disabled', 'true');
|
||||
DOM.addClass(this.label, 'disabled');
|
||||
DOM.removeTabIndexAndUpdateFocus(this.label);
|
||||
}
|
||||
|
||||
if (this.element) {
|
||||
DOM.addClass(this.element, 'disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateChecked(): void {
|
||||
if (this.label) {
|
||||
if (this.getAction().checked) {
|
||||
DOM.addClass(this.label, 'checked');
|
||||
} else {
|
||||
DOM.removeClass(this.label, 'checked');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import { IActionViewItemOptions, ActionViewItem, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
|
||||
export const enum ActionsOrientation {
|
||||
HORIZONTAL,
|
||||
@@ -393,41 +25,30 @@ export interface ActionTrigger {
|
||||
keyDown: boolean;
|
||||
}
|
||||
|
||||
export interface IActionViewItemProvider {
|
||||
(action: IAction): IActionViewItem | undefined;
|
||||
}
|
||||
|
||||
export interface IActionBarOptions {
|
||||
orientation?: ActionsOrientation;
|
||||
context?: any;
|
||||
actionViewItemProvider?: IActionViewItemProvider;
|
||||
actionRunner?: IActionRunner;
|
||||
ariaLabel?: string;
|
||||
animated?: boolean;
|
||||
triggerKeys?: ActionTrigger;
|
||||
allowContextMenu?: boolean;
|
||||
preventLoopNavigation?: boolean;
|
||||
readonly orientation?: ActionsOrientation;
|
||||
readonly context?: any;
|
||||
readonly actionViewItemProvider?: IActionViewItemProvider;
|
||||
readonly actionRunner?: IActionRunner;
|
||||
readonly ariaLabel?: string;
|
||||
readonly animated?: boolean;
|
||||
readonly triggerKeys?: ActionTrigger;
|
||||
readonly allowContextMenu?: boolean;
|
||||
readonly preventLoopNavigation?: boolean;
|
||||
}
|
||||
|
||||
const defaultOptions: IActionBarOptions = {
|
||||
orientation: ActionsOrientation.HORIZONTAL,
|
||||
context: null,
|
||||
triggerKeys: {
|
||||
keys: [KeyCode.Enter, KeyCode.Space],
|
||||
keyDown: false
|
||||
}
|
||||
};
|
||||
|
||||
export interface IActionOptions extends IActionViewItemOptions {
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export class ActionBar extends Disposable implements IActionRunner {
|
||||
|
||||
options: IActionBarOptions;
|
||||
private readonly options: IActionBarOptions;
|
||||
|
||||
private _actionRunner: IActionRunner;
|
||||
private _context: unknown;
|
||||
private _orientation: ActionsOrientation;
|
||||
private _triggerKeys: ActionTrigger;
|
||||
|
||||
// View Items
|
||||
viewItems: IActionViewItem[];
|
||||
@@ -450,15 +71,16 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
private _onDidBeforeRun = this._register(new Emitter<IRunEvent>());
|
||||
readonly onDidBeforeRun: Event<IRunEvent> = this._onDidBeforeRun.event;
|
||||
|
||||
constructor(container: HTMLElement, options: IActionBarOptions = defaultOptions) {
|
||||
constructor(container: HTMLElement, options: IActionBarOptions = {}) {
|
||||
super();
|
||||
|
||||
this.options = options;
|
||||
this._context = options.context;
|
||||
|
||||
if (!this.options.triggerKeys) {
|
||||
this.options.triggerKeys = defaultOptions.triggerKeys;
|
||||
}
|
||||
this._context = options.context ?? null;
|
||||
this._orientation = this.options.orientation ?? ActionsOrientation.HORIZONTAL;
|
||||
this._triggerKeys = this.options.triggerKeys ?? {
|
||||
keys: [KeyCode.Enter, KeyCode.Space],
|
||||
keyDown: false
|
||||
};
|
||||
|
||||
if (this.options.actionRunner) {
|
||||
this._actionRunner = this.options.actionRunner;
|
||||
@@ -483,7 +105,7 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
let previousKey: KeyCode;
|
||||
let nextKey: KeyCode;
|
||||
|
||||
switch (this.options.orientation) {
|
||||
switch (this._orientation) {
|
||||
case ActionsOrientation.HORIZONTAL:
|
||||
previousKey = KeyCode.LeftArrow;
|
||||
nextKey = KeyCode.RightArrow;
|
||||
@@ -517,7 +139,7 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
this._onDidCancel.fire();
|
||||
} else if (this.isTriggerKeyEvent(event)) {
|
||||
// Staying out of the else branch even if not triggered
|
||||
if (this.options.triggerKeys && this.options.triggerKeys.keyDown) {
|
||||
if (this._triggerKeys.keyDown) {
|
||||
this.doTrigger(event);
|
||||
}
|
||||
} else {
|
||||
@@ -535,7 +157,7 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
|
||||
// Run action on Enter/Space
|
||||
if (this.isTriggerKeyEvent(event)) {
|
||||
if (this.options.triggerKeys && !this.options.triggerKeys.keyDown) {
|
||||
if (!this._triggerKeys.keyDown) {
|
||||
this.doTrigger(event);
|
||||
}
|
||||
|
||||
@@ -582,11 +204,9 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
|
||||
private isTriggerKeyEvent(event: StandardKeyboardEvent): boolean {
|
||||
let ret = false;
|
||||
if (this.options.triggerKeys) {
|
||||
this.options.triggerKeys.keys.forEach(keyCode => {
|
||||
ret = ret || event.equals(keyCode);
|
||||
});
|
||||
}
|
||||
this._triggerKeys.keys.forEach(keyCode => {
|
||||
ret = ret || event.equals(keyCode);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -845,53 +465,6 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectActionViewItem extends BaseActionViewItem {
|
||||
protected selectBox: SelectBox;
|
||||
|
||||
constructor(ctx: unknown, action: IAction, options: ISelectOptionItem[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions) {
|
||||
super(ctx, action);
|
||||
|
||||
this.selectBox = new SelectBox(options, selected, contextViewProvider, undefined, selectBoxOptions);
|
||||
|
||||
this._register(this.selectBox);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
setOptions(options: ISelectOptionItem[], selected?: number): void {
|
||||
this.selectBox.setOptions(options, selected);
|
||||
}
|
||||
|
||||
select(index: number): void {
|
||||
this.selectBox.select(index);
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.selectBox.onDidSelect(e => {
|
||||
this.actionRunner.run(this._action, this.getActionContext(e.selected, e.index));
|
||||
}));
|
||||
}
|
||||
|
||||
protected getActionContext(option: string, index: number) {
|
||||
return option;
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
if (this.selectBox) {
|
||||
this.selectBox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
if (this.selectBox) {
|
||||
this.selectBox.blur();
|
||||
}
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
this.selectBox.render(container);
|
||||
}
|
||||
}
|
||||
|
||||
export function prepareActions(actions: IAction[]): IAction[] {
|
||||
if (!actions.length) {
|
||||
return actions;
|
||||
|
||||
@@ -10,9 +10,9 @@ import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
|
||||
export interface ICheckboxOpts extends ICheckboxStyles {
|
||||
readonly actionClassName?: string;
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
|
||||
import 'vs/css!./dropdown';
|
||||
import { Gesture, EventType as GestureEventType } from 'vs/base/browser/touch';
|
||||
import { ActionRunner, IAction, IActionRunner } from 'vs/base/common/actions';
|
||||
import { BaseActionViewItem, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ActionRunner, IAction } from 'vs/base/common/actions';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IContextViewProvider, IAnchor, AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { IMenuOptions } from 'vs/base/browser/ui/menu/menu';
|
||||
import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { EventHelper, EventType, removeClass, addClass, append, $, addDisposableListener, addClasses, DOMEvent } from 'vs/base/browser/dom';
|
||||
import { IContextMenuDelegate } from 'vs/base/browser/contextmenu';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { EventHelper, EventType, removeClass, addClass, append, $, addDisposableListener, DOMEvent } from 'vs/base/browser/dom';
|
||||
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
|
||||
@@ -201,17 +200,13 @@ export class Dropdown extends BaseDropdown {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IContextMenuProvider {
|
||||
showContextMenu(delegate: IContextMenuDelegate): void;
|
||||
}
|
||||
|
||||
export interface IActionProvider {
|
||||
getActions(): ReadonlyArray<IAction>;
|
||||
getActions(): IAction[];
|
||||
}
|
||||
|
||||
export interface IDropdownMenuOptions extends IBaseDropdownOptions {
|
||||
contextMenuProvider: IContextMenuProvider;
|
||||
actions?: ReadonlyArray<IAction>;
|
||||
actions?: IAction[];
|
||||
actionProvider?: IActionProvider;
|
||||
menuClassName?: string;
|
||||
menuAsChild?: boolean; // scope down for #99448
|
||||
@@ -220,7 +215,7 @@ export interface IDropdownMenuOptions extends IBaseDropdownOptions {
|
||||
export class DropdownMenu extends BaseDropdown {
|
||||
private _contextMenuProvider: IContextMenuProvider;
|
||||
private _menuOptions: IMenuOptions | undefined;
|
||||
private _actions: ReadonlyArray<IAction> = [];
|
||||
private _actions: IAction[] = [];
|
||||
private actionProvider?: IActionProvider;
|
||||
private menuClassName: string;
|
||||
private menuAsChild?: boolean;
|
||||
@@ -243,7 +238,7 @@ export class DropdownMenu extends BaseDropdown {
|
||||
return this._menuOptions;
|
||||
}
|
||||
|
||||
private get actions(): ReadonlyArray<IAction> {
|
||||
private get actions(): IAction[] {
|
||||
if (this.actionProvider) {
|
||||
return this.actionProvider.getActions();
|
||||
}
|
||||
@@ -251,7 +246,7 @@ export class DropdownMenu extends BaseDropdown {
|
||||
return this._actions;
|
||||
}
|
||||
|
||||
private set actions(actions: ReadonlyArray<IAction>) {
|
||||
private set actions(actions: IAction[]) {
|
||||
this._actions = actions;
|
||||
}
|
||||
|
||||
@@ -283,106 +278,3 @@ export class DropdownMenu extends BaseDropdown {
|
||||
removeClass(this.element, 'active');
|
||||
}
|
||||
}
|
||||
|
||||
export class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
private menuActionsOrProvider: ReadonlyArray<IAction> | IActionProvider;
|
||||
private dropdownMenu: DropdownMenu | undefined;
|
||||
private contextMenuProvider: IContextMenuProvider;
|
||||
private actionViewItemProvider?: IActionViewItemProvider;
|
||||
private keybindings?: (action: IAction) => ResolvedKeybinding | undefined;
|
||||
private clazz: string | undefined;
|
||||
private anchorAlignmentProvider: (() => AnchorAlignment) | undefined;
|
||||
private menuAsChild?: boolean;
|
||||
|
||||
private _onDidChangeVisibility = this._register(new Emitter<boolean>());
|
||||
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
|
||||
|
||||
constructor(action: IAction, menuActions: ReadonlyArray<IAction>, contextMenuProvider: IContextMenuProvider, actionViewItemProvider: IActionViewItemProvider | undefined, actionRunner: IActionRunner | undefined, keybindings: ((action: IAction) => ResolvedKeybinding | undefined) | undefined, clazz: string | undefined, anchorAlignmentProvider?: () => AnchorAlignment, menuAsChild?: boolean);
|
||||
constructor(action: IAction, actionProvider: IActionProvider, contextMenuProvider: IContextMenuProvider, actionViewItemProvider: IActionViewItemProvider | undefined, actionRunner: IActionRunner | undefined, keybindings: ((action: IAction) => ResolvedKeybinding) | undefined, clazz: string | undefined, anchorAlignmentProvider?: () => AnchorAlignment, menuAsChild?: boolean);
|
||||
constructor(action: IAction, menuActionsOrProvider: ReadonlyArray<IAction> | IActionProvider, contextMenuProvider: IContextMenuProvider, actionViewItemProvider: IActionViewItemProvider | undefined, actionRunner: IActionRunner | undefined, keybindings: ((action: IAction) => ResolvedKeybinding | undefined) | undefined, clazz: string | undefined, anchorAlignmentProvider?: () => AnchorAlignment, menuAsChild?: boolean) {
|
||||
super(null, action);
|
||||
|
||||
this.menuActionsOrProvider = menuActionsOrProvider;
|
||||
this.contextMenuProvider = contextMenuProvider;
|
||||
this.actionViewItemProvider = actionViewItemProvider;
|
||||
if (actionRunner) {
|
||||
this.actionRunner = actionRunner;
|
||||
}
|
||||
this.keybindings = keybindings;
|
||||
this.clazz = clazz;
|
||||
this.anchorAlignmentProvider = anchorAlignmentProvider;
|
||||
this.menuAsChild = menuAsChild;
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => {
|
||||
this.element = append(el, $('a.action-label.codicon')); // todo@aeschli: remove codicon, should come through `this.clazz`
|
||||
if (this.clazz) {
|
||||
addClasses(this.element, this.clazz);
|
||||
}
|
||||
|
||||
this.element.tabIndex = 0;
|
||||
this.element.setAttribute('role', 'button');
|
||||
this.element.setAttribute('aria-haspopup', 'true');
|
||||
this.element.setAttribute('aria-expanded', 'false');
|
||||
this.element.title = this._action.label || '';
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const options: IDropdownMenuOptions = {
|
||||
contextMenuProvider: this.contextMenuProvider,
|
||||
labelRenderer: labelRenderer,
|
||||
menuAsChild: this.menuAsChild
|
||||
};
|
||||
|
||||
// Render the DropdownMenu around a simple action to toggle it
|
||||
if (Array.isArray(this.menuActionsOrProvider)) {
|
||||
options.actions = this.menuActionsOrProvider;
|
||||
} else {
|
||||
options.actionProvider = this.menuActionsOrProvider as IActionProvider;
|
||||
}
|
||||
|
||||
this.dropdownMenu = this._register(new DropdownMenu(container, options));
|
||||
this._register(this.dropdownMenu.onDidChangeVisibility(visible => {
|
||||
this.element?.setAttribute('aria-expanded', `${visible}`);
|
||||
this._onDidChangeVisibility.fire(visible);
|
||||
}));
|
||||
|
||||
this.dropdownMenu.menuOptions = {
|
||||
actionViewItemProvider: this.actionViewItemProvider,
|
||||
actionRunner: this.actionRunner,
|
||||
getKeyBinding: this.keybindings,
|
||||
context: this._context
|
||||
};
|
||||
|
||||
if (this.anchorAlignmentProvider) {
|
||||
const that = this;
|
||||
|
||||
this.dropdownMenu.menuOptions = {
|
||||
...this.dropdownMenu.menuOptions,
|
||||
get anchorAlignment(): AnchorAlignment {
|
||||
return that.anchorAlignmentProvider!();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setActionContext(newContext: unknown): void {
|
||||
super.setActionContext(newContext);
|
||||
|
||||
if (this.dropdownMenu) {
|
||||
if (this.dropdownMenu.menuOptions) {
|
||||
this.dropdownMenu.menuOptions.context = newContext;
|
||||
} else {
|
||||
this.dropdownMenu.menuOptions = { context: newContext };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
show(): void {
|
||||
if (this.dropdownMenu) {
|
||||
this.dropdownMenu.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
136
src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts
Normal file
136
src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dropdown';
|
||||
import { IAction, IActionRunner, IActionViewItemProvider } from 'vs/base/common/actions';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { append, $, addClasses } from 'vs/base/browser/dom';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { BaseActionViewItem, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
import { IActionProvider, DropdownMenu, IDropdownMenuOptions, ILabelRenderer } from 'vs/base/browser/ui/dropdown/dropdown';
|
||||
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
|
||||
import { asArray } from 'vs/base/common/arrays';
|
||||
|
||||
export interface IKeybindingProvider {
|
||||
(action: IAction): ResolvedKeybinding | undefined;
|
||||
}
|
||||
|
||||
export interface IAnchorAlignmentProvider {
|
||||
(): AnchorAlignment;
|
||||
}
|
||||
|
||||
export interface IDropdownMenuActionViewItemOptions extends IBaseActionViewItemOptions {
|
||||
readonly actionViewItemProvider?: IActionViewItemProvider;
|
||||
readonly keybindingProvider?: IKeybindingProvider;
|
||||
readonly actionRunner?: IActionRunner;
|
||||
readonly classNames?: string[] | string;
|
||||
readonly anchorAlignmentProvider?: IAnchorAlignmentProvider;
|
||||
readonly menuAsChild?: boolean;
|
||||
}
|
||||
|
||||
export class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
private menuActionsOrProvider: readonly IAction[] | IActionProvider;
|
||||
private dropdownMenu: DropdownMenu | undefined;
|
||||
private contextMenuProvider: IContextMenuProvider;
|
||||
|
||||
private _onDidChangeVisibility = this._register(new Emitter<boolean>());
|
||||
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
|
||||
|
||||
constructor(
|
||||
action: IAction,
|
||||
menuActionsOrProvider: readonly IAction[] | IActionProvider,
|
||||
contextMenuProvider: IContextMenuProvider,
|
||||
protected options: IDropdownMenuActionViewItemOptions = {}
|
||||
) {
|
||||
super(null, action, options);
|
||||
|
||||
this.menuActionsOrProvider = menuActionsOrProvider;
|
||||
this.contextMenuProvider = contextMenuProvider;
|
||||
|
||||
if (this.options.actionRunner) {
|
||||
this.actionRunner = this.options.actionRunner;
|
||||
}
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => {
|
||||
this.element = append(el, $('a.action-label'));
|
||||
|
||||
const classNames = this.options.classNames ? asArray(this.options.classNames) : [];
|
||||
|
||||
// todo@aeschli: remove codicon, should come through `this.options.classNames`
|
||||
if (!classNames.find(c => c === 'icon')) {
|
||||
classNames.push('codicon');
|
||||
}
|
||||
|
||||
addClasses(this.element, ...classNames);
|
||||
|
||||
this.element.tabIndex = 0;
|
||||
this.element.setAttribute('role', 'button');
|
||||
this.element.setAttribute('aria-haspopup', 'true');
|
||||
this.element.setAttribute('aria-expanded', 'false');
|
||||
this.element.title = this._action.label || '';
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const options: IDropdownMenuOptions = {
|
||||
contextMenuProvider: this.contextMenuProvider,
|
||||
labelRenderer: labelRenderer,
|
||||
menuAsChild: this.options.menuAsChild
|
||||
};
|
||||
|
||||
// Render the DropdownMenu around a simple action to toggle it
|
||||
if (Array.isArray(this.menuActionsOrProvider)) {
|
||||
options.actions = this.menuActionsOrProvider;
|
||||
} else {
|
||||
options.actionProvider = this.menuActionsOrProvider as IActionProvider;
|
||||
}
|
||||
|
||||
this.dropdownMenu = this._register(new DropdownMenu(container, options));
|
||||
this._register(this.dropdownMenu.onDidChangeVisibility(visible => {
|
||||
this.element?.setAttribute('aria-expanded', `${visible}`);
|
||||
this._onDidChangeVisibility.fire(visible);
|
||||
}));
|
||||
|
||||
this.dropdownMenu.menuOptions = {
|
||||
actionViewItemProvider: this.options.actionViewItemProvider,
|
||||
actionRunner: this.actionRunner,
|
||||
getKeyBinding: this.options.keybindingProvider,
|
||||
context: this._context
|
||||
};
|
||||
|
||||
if (this.options.anchorAlignmentProvider) {
|
||||
const that = this;
|
||||
|
||||
this.dropdownMenu.menuOptions = {
|
||||
...this.dropdownMenu.menuOptions,
|
||||
get anchorAlignment(): AnchorAlignment {
|
||||
return that.options.anchorAlignmentProvider!();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setActionContext(newContext: unknown): void {
|
||||
super.setActionContext(newContext);
|
||||
|
||||
if (this.dropdownMenu) {
|
||||
if (this.dropdownMenu.menuOptions) {
|
||||
this.dropdownMenu.menuOptions.context = newContext;
|
||||
} else {
|
||||
this.dropdownMenu.menuOptions = { context: newContext };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
show(): void {
|
||||
if (this.dropdownMenu) {
|
||||
this.dropdownMenu.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { IActionRunner, IAction, Action } from 'vs/base/common/actions';
|
||||
import { ActionBar, IActionViewItemProvider, ActionsOrientation, Separator, ActionViewItem, IActionViewItemOptions, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IActionRunner, IAction, SubmenuAction, Separator, IActionViewItemProvider } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses, clearNode, createStyleSheet, isInShadowDOM } from 'vs/base/browser/dom';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
@@ -19,6 +19,7 @@ import { Event } from 'vs/base/common/event';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { isLinux, isMacintosh } from 'vs/base/common/platform';
|
||||
import { Codicon, registerIcon, stripCodicons } from 'vs/base/common/codicons';
|
||||
import { BaseActionViewItem, ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
import { formatRule } from 'vs/base/browser/ui/codicons/codiconStyles';
|
||||
|
||||
export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/;
|
||||
@@ -42,6 +43,7 @@ export interface IMenuOptions {
|
||||
anchorAlignment?: AnchorAlignment;
|
||||
expandDirection?: Direction;
|
||||
useEventAsContext?: boolean;
|
||||
submenuIds?: Set<string>;
|
||||
}
|
||||
|
||||
export interface IMenuStyles {
|
||||
@@ -55,12 +57,6 @@ export interface IMenuStyles {
|
||||
separatorColor?: Color;
|
||||
}
|
||||
|
||||
export class SubmenuAction extends Action {
|
||||
constructor(label: string, public entries: ReadonlyArray<SubmenuAction | IAction>, cssClass?: string) {
|
||||
super(!!cssClass ? cssClass : 'submenu', label, '', true);
|
||||
}
|
||||
}
|
||||
|
||||
interface ISubMenuData {
|
||||
parent: Menu;
|
||||
submenu?: Menu;
|
||||
@@ -209,6 +205,15 @@ export class Menu extends ActionBar {
|
||||
|
||||
menuElement.style.maxHeight = `${Math.max(10, window.innerHeight - container.getBoundingClientRect().top - 30)}px`;
|
||||
|
||||
actions = actions.filter(a => {
|
||||
if (options.submenuIds?.has(a.id)) {
|
||||
console.warn(`Found submenu cycle: ${a.id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.push(actions, { icon: true, label: true, isMenu: true });
|
||||
|
||||
container.appendChild(this.scrollableElement.getDomNode());
|
||||
@@ -317,7 +322,8 @@ export class Menu extends ActionBar {
|
||||
if (action instanceof Separator) {
|
||||
return new MenuSeparatorActionViewItem(options.context, action, { icon: true });
|
||||
} else if (action instanceof SubmenuAction) {
|
||||
const menuActionViewItem = new SubmenuMenuActionViewItem(action, action.entries, parentData, options);
|
||||
const actions = Array.isArray(action.actions) ? action.actions : action.actions();
|
||||
const menuActionViewItem = new SubmenuMenuActionViewItem(action, actions, parentData, { ...options, submenuIds: new Set([...(options.submenuIds || []), action.id]) });
|
||||
|
||||
if (options.enableMnemonics) {
|
||||
const mnemonic = menuActionViewItem.getMnemonic();
|
||||
|
||||
@@ -11,8 +11,8 @@ import * as nls from 'vs/nls';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch';
|
||||
import { cleanMnemonic, IMenuOptions, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, SubmenuAction, IMenuStyles, Direction } from 'vs/base/browser/ui/menu/menu';
|
||||
import { ActionRunner, IAction, IActionRunner } from 'vs/base/common/actions';
|
||||
import { cleanMnemonic, IMenuOptions, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, IMenuStyles, Direction } from 'vs/base/browser/ui/menu/menu';
|
||||
import { ActionRunner, IAction, IActionRunner, SubmenuAction, Separator } from 'vs/base/common/actions';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { KeyCode, ResolvedKeybinding, KeyMod } from 'vs/base/common/keyCodes';
|
||||
@@ -22,7 +22,6 @@ import { asArray } from 'vs/base/common/arrays';
|
||||
import { ScanCodeUtils, ScanCode } from 'vs/base/common/scanCode';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
|
||||
const $ = DOM.$;
|
||||
@@ -40,7 +39,7 @@ export interface IMenuBarOptions {
|
||||
}
|
||||
|
||||
export interface MenuBarMenu {
|
||||
actions: ReadonlyArray<IAction>;
|
||||
actions: IAction[];
|
||||
label: string;
|
||||
}
|
||||
|
||||
@@ -59,7 +58,7 @@ export class MenuBar extends Disposable {
|
||||
buttonElement: HTMLElement;
|
||||
titleElement: HTMLElement;
|
||||
label: string;
|
||||
actions?: ReadonlyArray<IAction>;
|
||||
actions?: IAction[];
|
||||
}[];
|
||||
|
||||
private overflowMenu!: {
|
||||
@@ -506,7 +505,7 @@ export class MenuBar extends Disposable {
|
||||
|
||||
this.overflowMenu.actions = [];
|
||||
for (let idx = this.numMenusShown; idx < this.menuCache.length; idx++) {
|
||||
this.overflowMenu.actions.push(new SubmenuAction(this.menuCache[idx].label, this.menuCache[idx].actions || []));
|
||||
this.overflowMenu.actions.push(new SubmenuAction(`menubar.submenu.${this.menuCache[idx].label}`, this.menuCache[idx].label, this.menuCache[idx].actions || []));
|
||||
}
|
||||
|
||||
if (this.overflowMenu.buttonElement.nextElementSibling !== this.menuCache[this.numMenusShown].buttonElement) {
|
||||
|
||||
@@ -5,17 +5,16 @@
|
||||
|
||||
import 'vs/css!./toolbar';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action, IActionRunner, IAction } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IContextMenuProvider, DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown';
|
||||
import { Action, IActionRunner, IAction, IActionViewItemProvider, SubmenuAction } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
|
||||
export const CONTEXT = 'context.toolbar';
|
||||
import { EventMultiplexer } from 'vs/base/common/event';
|
||||
import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
|
||||
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
|
||||
|
||||
const toolBarMoreIcon = registerIcon('toolbar-more', Codicon.more);
|
||||
|
||||
@@ -37,12 +36,13 @@ export class ToolBar extends Disposable {
|
||||
private actionBar: ActionBar;
|
||||
private toggleMenuAction: ToggleMenuAction;
|
||||
private toggleMenuActionViewItem: DropdownMenuActionViewItem | undefined;
|
||||
private toggleMenuActionViewItemDisposable: IDisposable = Disposable.None;
|
||||
private submenuActionViewItems: DropdownMenuActionViewItem[] = [];
|
||||
private hasSecondaryActions: boolean = false;
|
||||
private lookupKeybindings: boolean;
|
||||
|
||||
private _onDidChangeDropdownVisibility = this._register(new Emitter<boolean>());
|
||||
private _onDidChangeDropdownVisibility = this._register(new EventMultiplexer<boolean>());
|
||||
readonly onDidChangeDropdownVisibility = this._onDidChangeDropdownVisibility.event;
|
||||
private disposables = new DisposableStore();
|
||||
|
||||
constructor(container: HTMLElement, contextMenuProvider: IContextMenuProvider, options: IToolBarOptions = { orientation: ActionsOrientation.HORIZONTAL }) {
|
||||
super();
|
||||
@@ -61,35 +61,57 @@ export class ToolBar extends Disposable {
|
||||
ariaLabel: options.ariaLabel,
|
||||
actionRunner: options.actionRunner,
|
||||
actionViewItemProvider: (action: IAction) => {
|
||||
|
||||
// Return special action item for the toggle menu action
|
||||
if (action.id === ToggleMenuAction.ID) {
|
||||
|
||||
this.toggleMenuActionViewItemDisposable.dispose();
|
||||
|
||||
// Create new
|
||||
this.toggleMenuActionViewItem = new DropdownMenuActionViewItem(
|
||||
action,
|
||||
(<ToggleMenuAction>action).menuActions,
|
||||
contextMenuProvider,
|
||||
this.options.actionViewItemProvider,
|
||||
this.actionRunner,
|
||||
this.options.getKeyBinding,
|
||||
toolBarMoreIcon.classNames,
|
||||
this.options.anchorAlignmentProvider,
|
||||
true
|
||||
{
|
||||
actionViewItemProvider: this.options.actionViewItemProvider,
|
||||
actionRunner: this.actionRunner,
|
||||
keybindingProvider: this.options.getKeyBinding,
|
||||
classNames: toolBarMoreIcon.classNames,
|
||||
anchorAlignmentProvider: this.options.anchorAlignmentProvider,
|
||||
menuAsChild: true
|
||||
}
|
||||
);
|
||||
this.toggleMenuActionViewItem.setActionContext(this.actionBar.context);
|
||||
|
||||
this.toggleMenuActionViewItemDisposable = combinedDisposable(
|
||||
this.toggleMenuActionViewItem,
|
||||
this.toggleMenuActionViewItem.onDidChangeVisibility(e => this._onDidChangeDropdownVisibility.fire(e))
|
||||
);
|
||||
this.disposables.add(this._onDidChangeDropdownVisibility.add(this.toggleMenuActionViewItem.onDidChangeVisibility));
|
||||
|
||||
return this.toggleMenuActionViewItem;
|
||||
}
|
||||
|
||||
return options.actionViewItemProvider ? options.actionViewItemProvider(action) : undefined;
|
||||
if (options.actionViewItemProvider) {
|
||||
const result = options.actionViewItemProvider(action);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (action instanceof SubmenuAction) {
|
||||
const actions = Array.isArray(action.actions) ? action.actions : action.actions();
|
||||
const result = new DropdownMenuActionViewItem(
|
||||
action,
|
||||
actions,
|
||||
contextMenuProvider,
|
||||
{
|
||||
actionViewItemProvider: this.options.actionViewItemProvider,
|
||||
actionRunner: this.actionRunner,
|
||||
keybindingProvider: this.options.getKeyBinding,
|
||||
classNames: action.class,
|
||||
anchorAlignmentProvider: this.options.anchorAlignmentProvider,
|
||||
menuAsChild: true
|
||||
}
|
||||
);
|
||||
result.setActionContext(this.actionBar.context);
|
||||
this.submenuActionViewItems.push(result);
|
||||
this.disposables.add(this._onDidChangeDropdownVisibility.add(result.onDidChangeVisibility));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -107,6 +129,9 @@ export class ToolBar extends Disposable {
|
||||
if (this.toggleMenuActionViewItem) {
|
||||
this.toggleMenuActionViewItem.setActionContext(context);
|
||||
}
|
||||
for (const actionViewItem of this.submenuActionViewItems) {
|
||||
actionViewItem.setActionContext(context);
|
||||
}
|
||||
}
|
||||
|
||||
getContainer(): HTMLElement {
|
||||
@@ -126,6 +151,8 @@ export class ToolBar extends Disposable {
|
||||
}
|
||||
|
||||
setActions(primaryActions: ReadonlyArray<IAction>, secondaryActions?: ReadonlyArray<IAction>): void {
|
||||
this.clear();
|
||||
|
||||
let primaryActionsToSet = primaryActions ? primaryActions.slice(0) : [];
|
||||
|
||||
// Inject additional action to open secondary actions if present
|
||||
@@ -135,8 +162,6 @@ export class ToolBar extends Disposable {
|
||||
primaryActionsToSet.push(this.toggleMenuAction);
|
||||
}
|
||||
|
||||
this.actionBar.clear();
|
||||
|
||||
primaryActionsToSet.forEach(action => {
|
||||
this.actionBar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) });
|
||||
});
|
||||
@@ -148,25 +173,15 @@ export class ToolBar extends Disposable {
|
||||
return withNullAsUndefined(key?.getLabel());
|
||||
}
|
||||
|
||||
addPrimaryAction(primaryAction: IAction): () => void {
|
||||
return () => {
|
||||
|
||||
// Add after the "..." action if we have secondary actions
|
||||
if (this.hasSecondaryActions) {
|
||||
let itemCount = this.actionBar.length();
|
||||
this.actionBar.push(primaryAction, { icon: true, label: false, index: itemCount, keybinding: this.getKeybindingLabel(primaryAction) });
|
||||
}
|
||||
|
||||
// Otherwise just add to the end
|
||||
else {
|
||||
this.actionBar.push(primaryAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(primaryAction) });
|
||||
}
|
||||
};
|
||||
private clear(): void {
|
||||
this.submenuActionViewItems = [];
|
||||
this.disposables.clear();
|
||||
this.actionBar.clear();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.clear();
|
||||
super.dispose();
|
||||
this.toggleMenuActionViewItemDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user