mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Merge from master
This commit is contained in:
1
src/vs/base/browser/ui/menu/check.svg
Normal file
1
src/vs/base/browser/ui/menu/check.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-2 -2 16 16" enable-background="new -2 -2 16 16"><polygon fill="#424242" points="9,0 4.5,9 3,6 0,6 3,12 6,12 12,0"/></svg>
|
||||
|
After Width: | Height: | Size: 194 B |
1
src/vs/base/browser/ui/menu/ellipsis.svg
Normal file
1
src/vs/base/browser/ui/menu/ellipsis.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>Ellipsis_bold_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M6,7.5A2.5,2.5,0,1,1,3.5,5,2.5,2.5,0,0,1,6,7.5ZM8.5,5A2.5,2.5,0,1,0,11,7.5,2.5,2.5,0,0,0,8.5,5Zm5,0A2.5,2.5,0,1,0,16,7.5,2.5,2.5,0,0,0,13.5,5Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-bg" d="M5,7.5A1.5,1.5,0,1,1,3.5,6,1.5,1.5,0,0,1,5,7.5ZM8.5,6A1.5,1.5,0,1,0,10,7.5,1.5,1.5,0,0,0,8.5,6Zm5,0A1.5,1.5,0,1,0,15,7.5,1.5,1.5,0,0,0,13.5,6Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 748 B |
@@ -14,34 +14,23 @@
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-item {
|
||||
padding: 0;
|
||||
-ms-transform: none;
|
||||
-webkit-transform: none;
|
||||
-moz-transform: none;
|
||||
-o-transform: none;
|
||||
transform: none;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-item.active {
|
||||
-ms-transform: none;
|
||||
-webkit-transform: none;
|
||||
-moz-transform: none;
|
||||
-o-transform: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-item.focused {
|
||||
background-color: #E4E4E4;
|
||||
}
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-menu-item {
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
height: 2.6em;
|
||||
height: 2em;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-label {
|
||||
@@ -50,7 +39,7 @@
|
||||
text-decoration: none;
|
||||
padding: 0 1em;
|
||||
background: none;
|
||||
font-size: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -61,10 +50,15 @@
|
||||
flex: 2 1 auto;
|
||||
padding: 0 1em;
|
||||
text-align: right;
|
||||
font-size: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .submenu-indicator {
|
||||
height: 100%;
|
||||
-webkit-mask: url('submenu.svg') no-repeat 90% 50%/13px 13px;
|
||||
mask: url('submenu.svg') no-repeat 90% 50%/13px 13px;
|
||||
}
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-item.disabled .keybinding,
|
||||
.monaco-menu .monaco-action-bar.vertical .action-item.disabled .submenu-indicator {
|
||||
@@ -81,6 +75,16 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-item {
|
||||
position: static;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-item .monaco-submenu {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-label.separator {
|
||||
padding: 0.5em 0 0 0;
|
||||
margin-bottom: 0.5em;
|
||||
@@ -97,19 +101,25 @@
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-label.checked:after {
|
||||
content: ' \2713';
|
||||
.monaco-menu .monaco-action-bar.vertical .menu-item-check {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
-webkit-mask: url('check.svg') no-repeat 50% 56%/15px 15px;
|
||||
mask: url('check.svg') no-repeat 50% 56%/15px 15px;
|
||||
width: 1em;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-menu-item.checked .menu-item-check {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Context Menu */
|
||||
|
||||
.context-view.monaco-menu-container {
|
||||
font-family: "Segoe WPC", "Segoe UI", ".SFNSDisplay-Light", "SFUIText-Light", "HelveticaNeue-Light", sans-serif, "Droid Sans Fallback";
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif;
|
||||
outline: 0;
|
||||
box-shadow: 0 2px 8px #A8A8A8;
|
||||
border: none;
|
||||
color: #646465;
|
||||
background-color: white;
|
||||
-webkit-animation: fadeIn 0.083s linear;
|
||||
-o-animation: fadeIn 0.083s linear;
|
||||
-moz-animation: fadeIn 0.083s linear;
|
||||
@@ -127,26 +137,72 @@
|
||||
border: 1px solid transparent; /* prevents jumping behaviour on hover or focus */
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
.vs-dark .monaco-menu .monaco-action-bar.vertical .action-item.focused {
|
||||
background-color: #4B4C4D;
|
||||
}
|
||||
|
||||
.vs-dark .context-view.monaco-menu-container {
|
||||
box-shadow: 0 2px 8px #000;
|
||||
color: #BBB;
|
||||
background-color: #2D2F31;
|
||||
}
|
||||
|
||||
/* High Contrast Theming */
|
||||
.hc-black .context-view.monaco-menu-container {
|
||||
border: 2px solid #6FC3DF;
|
||||
color: white;
|
||||
background-color: #0C141F;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hc-black .monaco-menu .monaco-action-bar.vertical .action-item.focused {
|
||||
background: none;
|
||||
border: 1px dotted #f38518;
|
||||
}
|
||||
|
||||
/* Menubar styles */
|
||||
|
||||
.menubar {
|
||||
display: flex;
|
||||
flex-shrink: 1;
|
||||
box-sizing: border-box;
|
||||
height: 30px;
|
||||
-webkit-app-region: no-drag;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fullscreen .menubar {
|
||||
margin: 0px;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
.menubar > .menubar-menu-button {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0px 8px;
|
||||
cursor: default;
|
||||
-webkit-app-region: no-drag;
|
||||
zoom: 1;
|
||||
white-space: nowrap;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.menubar .menubar-menu-items-holder {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
opacity: 1;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.menubar .menubar-menu-items-holder.monaco-menu-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif;
|
||||
outline: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.menubar .menubar-menu-items-holder.monaco-menu-container :focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.menubar .toolbar-toggle-more {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 14px;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.menubar .toolbar-toggle-more {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
-webkit-mask: url('ellipsis.svg') no-repeat 50% 55%/14px 14px;
|
||||
mask: url('ellipsis.svg') no-repeat 50% 55%/14px 14px;
|
||||
}
|
||||
@@ -3,19 +3,24 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./menu';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { IActionRunner, IAction, Action } from 'vs/base/common/actions';
|
||||
import { ActionBar, IActionItemProvider, ActionsOrientation, Separator, ActionItem, IActionItemOptions, BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor } from 'vs/base/browser/dom';
|
||||
import { ResolvedKeybinding, KeyCode, KeyCodeUtils } from 'vs/base/common/keyCodes';
|
||||
import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses } from 'vs/base/browser/dom';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { $, Builder } from 'vs/base/browser/builder';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
|
||||
export const MENU_MNEMONIC_REGEX: RegExp = /\(&{1,2}(.)\)|&{1,2}(.)/;
|
||||
export const MENU_ESCAPED_MNEMONIC_REGEX: RegExp = /(?:&){1,2}(.)/;
|
||||
|
||||
export interface IMenuOptions {
|
||||
context?: any;
|
||||
@@ -23,8 +28,20 @@ export interface IMenuOptions {
|
||||
actionRunner?: IActionRunner;
|
||||
getKeyBinding?: (action: IAction) => ResolvedKeybinding;
|
||||
ariaLabel?: string;
|
||||
enableMnemonics?: boolean;
|
||||
anchorAlignment?: AnchorAlignment;
|
||||
}
|
||||
|
||||
export interface IMenuStyles {
|
||||
shadowColor?: Color;
|
||||
borderColor?: Color;
|
||||
foregroundColor?: Color;
|
||||
backgroundColor?: Color;
|
||||
selectionForegroundColor?: Color;
|
||||
selectionBackgroundColor?: Color;
|
||||
selectionBorderColor?: Color;
|
||||
separatorColor?: Color;
|
||||
}
|
||||
|
||||
export class SubmenuAction extends Action {
|
||||
constructor(label: string, public entries: (SubmenuAction | IAction)[], cssClass?: string) {
|
||||
@@ -37,43 +54,225 @@ interface ISubMenuData {
|
||||
submenu?: Menu;
|
||||
}
|
||||
|
||||
export class Menu {
|
||||
export class Menu extends ActionBar {
|
||||
private mnemonics: Map<KeyCode, Array<MenuActionItem>>;
|
||||
private menuDisposables: IDisposable[];
|
||||
private scrollableElement: DomScrollableElement;
|
||||
private menuElement: HTMLElement;
|
||||
|
||||
private actionBar: ActionBar;
|
||||
private listener: IDisposable;
|
||||
private readonly _onScroll: Emitter<void>;
|
||||
|
||||
constructor(container: HTMLElement, actions: IAction[], options: IMenuOptions = {}) {
|
||||
|
||||
addClass(container, 'monaco-menu-container');
|
||||
container.setAttribute('role', 'presentation');
|
||||
const menuElement = document.createElement('div');
|
||||
addClass(menuElement, 'monaco-menu');
|
||||
menuElement.setAttribute('role', 'presentation');
|
||||
|
||||
let menuContainer = document.createElement('div');
|
||||
addClass(menuContainer, 'monaco-menu');
|
||||
menuContainer.setAttribute('role', 'presentation');
|
||||
container.appendChild(menuContainer);
|
||||
super(menuElement, {
|
||||
orientation: ActionsOrientation.VERTICAL,
|
||||
actionItemProvider: action => this.doGetActionItem(action, options, parentData),
|
||||
context: options.context,
|
||||
actionRunner: options.actionRunner,
|
||||
ariaLabel: options.ariaLabel,
|
||||
triggerKeys: { keys: [KeyCode.Enter], keyDown: true }
|
||||
});
|
||||
|
||||
this.menuElement = menuElement;
|
||||
|
||||
this._onScroll = this._register(new Emitter<void>());
|
||||
|
||||
this.actionsList.setAttribute('role', 'menu');
|
||||
|
||||
this.actionsList.tabIndex = 0;
|
||||
|
||||
this.menuDisposables = [];
|
||||
|
||||
if (options.enableMnemonics) {
|
||||
this.menuDisposables.push(addDisposableListener(menuElement, EventType.KEY_DOWN, (e) => {
|
||||
const key = KeyCodeUtils.fromString(e.key);
|
||||
if (this.mnemonics.has(key)) {
|
||||
EventHelper.stop(e, true);
|
||||
const actions = this.mnemonics.get(key);
|
||||
|
||||
if (actions.length === 1) {
|
||||
if (actions[0] instanceof SubmenuActionItem) {
|
||||
this.focusItemByElement(actions[0].container);
|
||||
}
|
||||
|
||||
actions[0].onClick(event);
|
||||
}
|
||||
|
||||
if (actions.length > 1) {
|
||||
const action = actions.shift();
|
||||
this.focusItemByElement(action.container);
|
||||
|
||||
actions.push(action);
|
||||
this.mnemonics.set(key, actions);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this._register(addDisposableListener(this.domNode, EventType.MOUSE_OUT, e => {
|
||||
let relatedTarget = e.relatedTarget as HTMLElement;
|
||||
if (!isAncestor(relatedTarget, this.domNode)) {
|
||||
this.focusedItem = undefined;
|
||||
this.updateFocus();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(addDisposableListener(this.actionsList, EventType.MOUSE_OVER, e => {
|
||||
let target = e.target as HTMLElement;
|
||||
if (!target || !isAncestor(target, this.actionsList) || target === this.actionsList) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (target.parentElement !== this.actionsList) {
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
if (hasClass(target, 'action-item')) {
|
||||
const lastFocusedItem = this.focusedItem;
|
||||
this.setFocusedItem(target);
|
||||
|
||||
if (lastFocusedItem !== this.focusedItem) {
|
||||
this.updateFocus();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let parentData: ISubMenuData = {
|
||||
parent: this
|
||||
};
|
||||
|
||||
this.actionBar = new ActionBar(menuContainer, {
|
||||
orientation: ActionsOrientation.VERTICAL,
|
||||
actionItemProvider: action => this.doGetActionItem(action, options, parentData),
|
||||
context: options.context,
|
||||
actionRunner: options.actionRunner,
|
||||
isMenu: true,
|
||||
ariaLabel: options.ariaLabel
|
||||
});
|
||||
this.mnemonics = new Map<KeyCode, Array<MenuActionItem>>();
|
||||
|
||||
this.actionBar.push(actions, { icon: true, label: true, isMenu: true });
|
||||
this.push(actions, { icon: true, label: true, isMenu: true });
|
||||
|
||||
// Scroll Logic
|
||||
this.scrollableElement = this._register(new DomScrollableElement(menuElement, {
|
||||
alwaysConsumeMouseWheel: true,
|
||||
horizontal: ScrollbarVisibility.Hidden,
|
||||
vertical: ScrollbarVisibility.Visible,
|
||||
verticalScrollbarSize: 7,
|
||||
handleMouseWheel: true,
|
||||
useShadows: true
|
||||
}));
|
||||
|
||||
const scrollElement = this.scrollableElement.getDomNode();
|
||||
scrollElement.style.position = null;
|
||||
|
||||
menuElement.style.maxHeight = `${Math.max(10, window.innerHeight - container.getBoundingClientRect().top - 30)}px`;
|
||||
|
||||
this.scrollableElement.onScroll(() => {
|
||||
this._onScroll.fire();
|
||||
}, this, this.menuDisposables);
|
||||
|
||||
this._register(addDisposableListener(this.menuElement, EventType.SCROLL, (e) => {
|
||||
this.scrollableElement.scanDomNode();
|
||||
}));
|
||||
|
||||
container.appendChild(this.scrollableElement.getDomNode());
|
||||
this.scrollableElement.scanDomNode();
|
||||
|
||||
this.items.filter(item => !(item instanceof MenuSeparatorActionItem)).forEach((item: MenuActionItem, index: number, array: any[]) => {
|
||||
item.updatePositionInSet(index + 1, array.length);
|
||||
});
|
||||
}
|
||||
|
||||
style(style: IMenuStyles): void {
|
||||
const container = this.getContainer();
|
||||
|
||||
const fgColor = style.foregroundColor ? `${style.foregroundColor}` : null;
|
||||
const bgColor = style.backgroundColor ? `${style.backgroundColor}` : null;
|
||||
const border = style.borderColor ? `2px solid ${style.borderColor}` : null;
|
||||
const shadow = style.shadowColor ? `0 2px 4px ${style.shadowColor}` : null;
|
||||
|
||||
container.style.border = border;
|
||||
this.domNode.style.color = fgColor;
|
||||
this.domNode.style.backgroundColor = bgColor;
|
||||
container.style.boxShadow = shadow;
|
||||
|
||||
if (this.items) {
|
||||
this.items.forEach(item => {
|
||||
if (item instanceof MenuActionItem || item instanceof MenuSeparatorActionItem) {
|
||||
item.style(style);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getContainer(): HTMLElement {
|
||||
return this.scrollableElement.getDomNode();
|
||||
}
|
||||
|
||||
get onScroll(): Event<void> {
|
||||
return this._onScroll.event;
|
||||
}
|
||||
|
||||
get scrollOffset(): number {
|
||||
return this.menuElement.scrollTop;
|
||||
}
|
||||
|
||||
trigger(index: number): void {
|
||||
if (index <= this.items.length && index >= 0) {
|
||||
const item = this.items[index];
|
||||
if (item instanceof SubmenuActionItem) {
|
||||
super.focus(index);
|
||||
item.open(true);
|
||||
} else if (item instanceof MenuActionItem) {
|
||||
super.run(item._action, item._context);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private focusItemByElement(element: HTMLElement) {
|
||||
const lastFocusedItem = this.focusedItem;
|
||||
this.setFocusedItem(element);
|
||||
|
||||
if (lastFocusedItem !== this.focusedItem) {
|
||||
this.updateFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private setFocusedItem(element: HTMLElement): void {
|
||||
for (let i = 0; i < this.actionsList.children.length; i++) {
|
||||
let elem = this.actionsList.children[i];
|
||||
if (element === elem) {
|
||||
this.focusedItem = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private doGetActionItem(action: IAction, options: IMenuOptions, parentData: ISubMenuData): BaseActionItem {
|
||||
if (action instanceof Separator) {
|
||||
return new ActionItem(options.context, action, { icon: true });
|
||||
return new MenuSeparatorActionItem(options.context, action, { icon: true });
|
||||
} else if (action instanceof SubmenuAction) {
|
||||
return new SubmenuActionItem(action, action.entries, parentData, options);
|
||||
const menuActionItem = new SubmenuActionItem(action, action.entries, parentData, options);
|
||||
|
||||
if (options.enableMnemonics) {
|
||||
const mnemonic = menuActionItem.getMnemonic();
|
||||
if (mnemonic && menuActionItem.isEnabled()) {
|
||||
let actionItems: MenuActionItem[] = [];
|
||||
if (this.mnemonics.has(mnemonic)) {
|
||||
actionItems = this.mnemonics.get(mnemonic);
|
||||
}
|
||||
|
||||
actionItems.push(menuActionItem);
|
||||
|
||||
this.mnemonics.set(mnemonic, actionItems);
|
||||
}
|
||||
}
|
||||
|
||||
return menuActionItem;
|
||||
} else {
|
||||
const menuItemOptions: IActionItemOptions = {};
|
||||
const menuItemOptions: IMenuItemOptions = { enableMnemonics: options.enableMnemonics };
|
||||
if (options.getKeyBinding) {
|
||||
const keybinding = options.getKeyBinding(action);
|
||||
if (keybinding) {
|
||||
@@ -81,46 +280,45 @@ export class Menu {
|
||||
}
|
||||
}
|
||||
|
||||
return new MenuActionItem(options.context, action, menuItemOptions);
|
||||
}
|
||||
}
|
||||
const menuActionItem = new MenuActionItem(options.context, action, menuItemOptions);
|
||||
|
||||
public get onDidCancel(): Event<void> {
|
||||
return this.actionBar.onDidCancel;
|
||||
}
|
||||
if (options.enableMnemonics) {
|
||||
const mnemonic = menuActionItem.getMnemonic();
|
||||
if (mnemonic && menuActionItem.isEnabled()) {
|
||||
let actionItems: MenuActionItem[] = [];
|
||||
if (this.mnemonics.has(mnemonic)) {
|
||||
actionItems = this.mnemonics.get(mnemonic);
|
||||
}
|
||||
|
||||
public get onDidBlur(): Event<void> {
|
||||
return this.actionBar.onDidBlur;
|
||||
}
|
||||
actionItems.push(menuActionItem);
|
||||
|
||||
public focus(selectFirst = true) {
|
||||
if (this.actionBar) {
|
||||
this.actionBar.focus(selectFirst);
|
||||
}
|
||||
}
|
||||
this.mnemonics.set(mnemonic, actionItems);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
if (this.actionBar) {
|
||||
this.actionBar.dispose();
|
||||
this.actionBar = null;
|
||||
}
|
||||
|
||||
if (this.listener) {
|
||||
this.listener.dispose();
|
||||
this.listener = null;
|
||||
return menuActionItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IMenuItemOptions extends IActionItemOptions {
|
||||
enableMnemonics?: boolean;
|
||||
}
|
||||
|
||||
class MenuActionItem extends BaseActionItem {
|
||||
static MNEMONIC_REGEX: RegExp = /&&(.)/g;
|
||||
|
||||
protected $e: Builder;
|
||||
protected $label: Builder;
|
||||
protected options: IActionItemOptions;
|
||||
public container: HTMLElement;
|
||||
|
||||
protected options: IMenuItemOptions;
|
||||
protected item: HTMLElement;
|
||||
|
||||
private label: HTMLElement;
|
||||
private check: HTMLElement;
|
||||
private mnemonic: KeyCode;
|
||||
private cssClass: string;
|
||||
protected menuStyle: IMenuStyles;
|
||||
|
||||
constructor(ctx: any, action: IAction, options: IActionItemOptions = {}) {
|
||||
constructor(ctx: any, action: IAction, options: IMenuItemOptions = {}) {
|
||||
options.isMenu = true;
|
||||
super(action, action, options);
|
||||
|
||||
@@ -128,62 +326,97 @@ class MenuActionItem extends BaseActionItem {
|
||||
this.options.icon = options.icon !== undefined ? options.icon : false;
|
||||
this.options.label = options.label !== undefined ? options.label : true;
|
||||
this.cssClass = '';
|
||||
|
||||
// Set mnemonic
|
||||
if (this.options.label && options.enableMnemonics) {
|
||||
let label = this.getAction().label;
|
||||
if (label) {
|
||||
let matches = MENU_MNEMONIC_REGEX.exec(label);
|
||||
if (matches) {
|
||||
this.mnemonic = KeyCodeUtils.fromString((!!matches[1] ? matches[1] : matches[2]).toLocaleLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render(container: HTMLElement): void {
|
||||
render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
|
||||
this.$e = $('a.action-menu-item').appendTo(this.builder);
|
||||
this.container = container;
|
||||
|
||||
this.item = append(this.element, $('a.action-menu-item'));
|
||||
if (this._action.id === Separator.ID) {
|
||||
// A separator is a presentation item
|
||||
this.$e.attr({ role: 'presentation' });
|
||||
this.item.setAttribute('role', 'presentation');
|
||||
} else {
|
||||
this.$e.attr({ role: 'menuitem' });
|
||||
this.item.setAttribute('role', 'menuitem');
|
||||
if (this.mnemonic) {
|
||||
this.item.setAttribute('aria-keyshortcuts', `${this.mnemonic}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.$label = $('span.action-label').appendTo(this.$e);
|
||||
this.check = append(this.item, $('span.menu-item-check'));
|
||||
this.check.setAttribute('role', 'none');
|
||||
|
||||
this.label = append(this.item, $('span.action-label'));
|
||||
|
||||
if (this.options.label && this.options.keybinding) {
|
||||
$('span.keybinding').text(this.options.keybinding).appendTo(this.$e);
|
||||
append(this.item, $('span.keybinding')).textContent = this.options.keybinding;
|
||||
}
|
||||
|
||||
this._updateClass();
|
||||
this._updateLabel();
|
||||
this._updateTooltip();
|
||||
this._updateEnabled();
|
||||
this._updateChecked();
|
||||
this._register(addDisposableListener(this.element, EventType.MOUSE_UP, e => {
|
||||
EventHelper.stop(e, true);
|
||||
this.onClick(e);
|
||||
}));
|
||||
|
||||
this.updateClass();
|
||||
this.updateLabel();
|
||||
this.updateTooltip();
|
||||
this.updateEnabled();
|
||||
this.updateChecked();
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
blur(): void {
|
||||
super.blur();
|
||||
this.applyStyle();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
super.focus();
|
||||
this.$e.domFocus();
|
||||
this.item.focus();
|
||||
this.applyStyle();
|
||||
}
|
||||
|
||||
public _updateLabel(): void {
|
||||
updatePositionInSet(pos: number, setSize: number): void {
|
||||
this.item.setAttribute('aria-posinset', `${pos}`);
|
||||
this.item.setAttribute('aria-setsize', `${setSize}`);
|
||||
}
|
||||
|
||||
updateLabel(): void {
|
||||
if (this.options.label) {
|
||||
let label = this.getAction().label;
|
||||
if (label) {
|
||||
let matches = MenuActionItem.MNEMONIC_REGEX.exec(label);
|
||||
if (matches && matches.length === 2) {
|
||||
let mnemonic = matches[1];
|
||||
|
||||
let ariaLabel = label.replace(MenuActionItem.MNEMONIC_REGEX, mnemonic);
|
||||
|
||||
this.$e.getHTMLElement().accessKey = mnemonic.toLocaleLowerCase();
|
||||
this.$label.attr('aria-label', ariaLabel);
|
||||
} else {
|
||||
this.$label.attr('aria-label', label);
|
||||
const cleanLabel = cleanMnemonic(label);
|
||||
if (!this.options.enableMnemonics) {
|
||||
label = cleanLabel;
|
||||
}
|
||||
|
||||
label = label.replace(MenuActionItem.MNEMONIC_REGEX, '$1\u0332');
|
||||
this.label.setAttribute('aria-label', cleanLabel);
|
||||
|
||||
const matches = MENU_MNEMONIC_REGEX.exec(label);
|
||||
|
||||
if (matches) {
|
||||
label = strings.escape(label).replace(MENU_ESCAPED_MNEMONIC_REGEX, '<u aria-hidden="true">$1</u>');
|
||||
this.item.setAttribute('aria-keyshortcuts', (!!matches[1] ? matches[1] : matches[2]).toLocaleLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
this.$label.text(label);
|
||||
this.label.innerHTML = label.trim();
|
||||
}
|
||||
}
|
||||
|
||||
public _updateTooltip(): void {
|
||||
let title: string = null;
|
||||
updateTooltip(): void {
|
||||
let title: string | null = null;
|
||||
|
||||
if (this.getAction().tooltip) {
|
||||
title = this.getAction().tooltip;
|
||||
@@ -197,50 +430,81 @@ class MenuActionItem extends BaseActionItem {
|
||||
}
|
||||
|
||||
if (title) {
|
||||
this.$e.attr({ title: title });
|
||||
this.item.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
public _updateClass(): void {
|
||||
updateClass(): void {
|
||||
if (this.cssClass) {
|
||||
this.$e.removeClass(this.cssClass);
|
||||
removeClasses(this.item, this.cssClass);
|
||||
}
|
||||
if (this.options.icon) {
|
||||
this.cssClass = this.getAction().class;
|
||||
this.$label.addClass('icon');
|
||||
addClass(this.label, 'icon');
|
||||
if (this.cssClass) {
|
||||
this.$label.addClass(this.cssClass);
|
||||
addClasses(this.label, this.cssClass);
|
||||
}
|
||||
this._updateEnabled();
|
||||
this.updateEnabled();
|
||||
} else {
|
||||
this.$label.removeClass('icon');
|
||||
removeClass(this.label, 'icon');
|
||||
}
|
||||
}
|
||||
|
||||
public _updateEnabled(): void {
|
||||
updateEnabled(): void {
|
||||
if (this.getAction().enabled) {
|
||||
this.builder.removeClass('disabled');
|
||||
this.$e.removeClass('disabled');
|
||||
this.$e.attr({ tabindex: 0 });
|
||||
removeClass(this.element, 'disabled');
|
||||
removeClass(this.item, 'disabled');
|
||||
this.item.tabIndex = 0;
|
||||
} else {
|
||||
this.builder.addClass('disabled');
|
||||
this.$e.addClass('disabled');
|
||||
removeTabIndexAndUpdateFocus(this.$e.getHTMLElement());
|
||||
addClass(this.element, 'disabled');
|
||||
addClass(this.item, 'disabled');
|
||||
removeTabIndexAndUpdateFocus(this.item);
|
||||
}
|
||||
}
|
||||
|
||||
public _updateChecked(): void {
|
||||
updateChecked(): void {
|
||||
if (this.getAction().checked) {
|
||||
this.$label.addClass('checked');
|
||||
addClass(this.item, 'checked');
|
||||
this.item.setAttribute('role', 'menuitemcheckbox');
|
||||
this.item.setAttribute('aria-checked', 'true');
|
||||
} else {
|
||||
this.$label.removeClass('checked');
|
||||
removeClass(this.item, 'checked');
|
||||
this.item.setAttribute('role', 'menuitem');
|
||||
this.item.setAttribute('aria-checked', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
getMnemonic(): KeyCode {
|
||||
return this.mnemonic;
|
||||
}
|
||||
|
||||
protected applyStyle(): void {
|
||||
if (!this.menuStyle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSelected = hasClass(this.element, 'focused');
|
||||
const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor;
|
||||
const bgColor = isSelected && this.menuStyle.selectionBackgroundColor ? this.menuStyle.selectionBackgroundColor : this.menuStyle.backgroundColor;
|
||||
const border = isSelected && this.menuStyle.selectionBorderColor ? `1px solid ${this.menuStyle.selectionBorderColor}` : null;
|
||||
|
||||
this.item.style.color = fgColor ? `${fgColor}` : null;
|
||||
this.check.style.backgroundColor = fgColor ? `${fgColor}` : null;
|
||||
this.item.style.backgroundColor = bgColor ? `${bgColor}` : null;
|
||||
this.container.style.border = border;
|
||||
}
|
||||
|
||||
style(style: IMenuStyles): void {
|
||||
this.menuStyle = style;
|
||||
this.applyStyle();
|
||||
}
|
||||
}
|
||||
|
||||
class SubmenuActionItem extends MenuActionItem {
|
||||
private mysubmenu: Menu;
|
||||
private submenuContainer: Builder;
|
||||
private submenuContainer: HTMLElement;
|
||||
private submenuIndicator: HTMLElement;
|
||||
private submenuDisposables: IDisposable[] = [];
|
||||
private mouseOver: boolean;
|
||||
private showScheduler: RunOnceScheduler;
|
||||
private hideScheduler: RunOnceScheduler;
|
||||
@@ -251,7 +515,7 @@ class SubmenuActionItem extends MenuActionItem {
|
||||
private parentData: ISubMenuData,
|
||||
private submenuOptions?: IMenuOptions
|
||||
) {
|
||||
super(action, action, { label: true, isMenu: true });
|
||||
super(action, action, submenuOptions);
|
||||
|
||||
this.showScheduler = new RunOnceScheduler(() => {
|
||||
if (this.mouseOver) {
|
||||
@@ -261,84 +525,110 @@ class SubmenuActionItem extends MenuActionItem {
|
||||
}, 250);
|
||||
|
||||
this.hideScheduler = new RunOnceScheduler(() => {
|
||||
if ((!isAncestor(document.activeElement, this.builder.getHTMLElement()) && this.parentData.submenu === this.mysubmenu)) {
|
||||
if ((!isAncestor(document.activeElement, this.element) && this.parentData.submenu === this.mysubmenu)) {
|
||||
this.parentData.parent.focus(false);
|
||||
this.cleanupExistingSubmenu(true);
|
||||
}
|
||||
}, 750);
|
||||
}
|
||||
|
||||
public render(container: HTMLElement): void {
|
||||
render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
|
||||
this.$e.addClass('monaco-submenu-item');
|
||||
this.$e.attr('aria-haspopup', 'true');
|
||||
$('span.submenu-indicator').text('\u25B6').appendTo(this.$e);
|
||||
addClass(this.item, 'monaco-submenu-item');
|
||||
this.item.setAttribute('aria-haspopup', 'true');
|
||||
|
||||
$(this.builder).on(EventType.KEY_UP, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
if (event.equals(KeyCode.RightArrow)) {
|
||||
this.submenuIndicator = append(this.item, $('span.submenu-indicator'));
|
||||
this.submenuIndicator.setAttribute('aria-hidden', 'true');
|
||||
|
||||
this._register(addDisposableListener(this.element, EventType.KEY_UP, e => {
|
||||
let event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Enter)) {
|
||||
EventHelper.stop(e, true);
|
||||
|
||||
this.createSubmenu(true);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
$(this.builder).on(EventType.KEY_DOWN, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
if (event.equals(KeyCode.RightArrow)) {
|
||||
this._register(addDisposableListener(this.element, EventType.KEY_DOWN, e => {
|
||||
let event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Enter)) {
|
||||
EventHelper.stop(e, true);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
$(this.builder).on(EventType.MOUSE_OVER, (e) => {
|
||||
this._register(addDisposableListener(this.element, EventType.MOUSE_OVER, e => {
|
||||
if (!this.mouseOver) {
|
||||
this.mouseOver = true;
|
||||
|
||||
this.showScheduler.schedule();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
$(this.builder).on(EventType.MOUSE_LEAVE, (e) => {
|
||||
this._register(addDisposableListener(this.element, EventType.MOUSE_LEAVE, e => {
|
||||
this.mouseOver = false;
|
||||
});
|
||||
}));
|
||||
|
||||
$(this.builder).on(EventType.FOCUS_OUT, (e) => {
|
||||
if (!isAncestor(document.activeElement, this.builder.getHTMLElement())) {
|
||||
this._register(addDisposableListener(this.element, EventType.FOCUS_OUT, e => {
|
||||
if (!isAncestor(document.activeElement, this.element)) {
|
||||
this.hideScheduler.schedule();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this._register(this.parentData.parent.onScroll(() => {
|
||||
this.parentData.parent.focus(false);
|
||||
this.cleanupExistingSubmenu(false);
|
||||
}));
|
||||
}
|
||||
|
||||
public onClick(e: EventLike) {
|
||||
open(selectFirst?: boolean): void {
|
||||
this.cleanupExistingSubmenu(false);
|
||||
this.createSubmenu(selectFirst);
|
||||
}
|
||||
|
||||
onClick(e: EventLike): void {
|
||||
// stop clicking from trying to run an action
|
||||
EventHelper.stop(e, true);
|
||||
|
||||
this.cleanupExistingSubmenu(false);
|
||||
this.createSubmenu(false);
|
||||
}
|
||||
|
||||
private cleanupExistingSubmenu(force: boolean) {
|
||||
private cleanupExistingSubmenu(force: boolean): void {
|
||||
if (this.parentData.submenu && (force || (this.parentData.submenu !== this.mysubmenu))) {
|
||||
this.parentData.submenu.dispose();
|
||||
this.parentData.submenu = null;
|
||||
|
||||
if (this.submenuContainer) {
|
||||
this.submenuContainer.dispose();
|
||||
this.submenuDisposables = dispose(this.submenuDisposables);
|
||||
this.submenuContainer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createSubmenu(selectFirstItem = true) {
|
||||
private createSubmenu(selectFirstItem = true): void {
|
||||
if (!this.parentData.submenu) {
|
||||
this.submenuContainer = $(this.builder).div({ class: 'monaco-submenu menubar-menu-items-holder context-view' });
|
||||
this.submenuContainer = append(this.element, $('div.monaco-submenu'));
|
||||
addClasses(this.submenuContainer, 'menubar-menu-items-holder', 'context-view');
|
||||
|
||||
$(this.submenuContainer).style({
|
||||
'left': `${$(this.builder).getClientArea().width}px`
|
||||
});
|
||||
this.parentData.submenu = new Menu(this.submenuContainer, this.submenuActions, this.submenuOptions);
|
||||
if (this.menuStyle) {
|
||||
this.parentData.submenu.style(this.menuStyle);
|
||||
}
|
||||
|
||||
$(this.submenuContainer).on(EventType.KEY_UP, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
const boundingRect = this.element.getBoundingClientRect();
|
||||
const childBoundingRect = this.submenuContainer.getBoundingClientRect();
|
||||
|
||||
if (window.innerWidth <= boundingRect.right + childBoundingRect.width) {
|
||||
this.submenuContainer.style.left = '10px';
|
||||
this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset + boundingRect.height}px`;
|
||||
} else {
|
||||
this.submenuContainer.style.left = `${this.element.offsetWidth}px`;
|
||||
this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset}px`;
|
||||
}
|
||||
|
||||
this.submenuDisposables.push(addDisposableListener(this.submenuContainer, EventType.KEY_UP, e => {
|
||||
let event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.LeftArrow)) {
|
||||
EventHelper.stop(e, true);
|
||||
|
||||
@@ -346,20 +636,28 @@ class SubmenuActionItem extends MenuActionItem {
|
||||
this.parentData.submenu.dispose();
|
||||
this.parentData.submenu = null;
|
||||
|
||||
this.submenuContainer.dispose();
|
||||
this.submenuDisposables = dispose(this.submenuDisposables);
|
||||
this.submenuContainer = null;
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
$(this.submenuContainer).on(EventType.KEY_DOWN, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
this.submenuDisposables.push(addDisposableListener(this.submenuContainer, EventType.KEY_DOWN, e => {
|
||||
let event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.LeftArrow)) {
|
||||
EventHelper.stop(e, true);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
this.parentData.submenu = new Menu(this.submenuContainer.getHTMLElement(), this.submenuActions, this.submenuOptions);
|
||||
this.submenuDisposables.push(this.parentData.submenu.onDidCancel(() => {
|
||||
this.parentData.parent.focus();
|
||||
this.parentData.submenu.dispose();
|
||||
this.parentData.submenu = null;
|
||||
|
||||
this.submenuDisposables = dispose(this.submenuDisposables);
|
||||
this.submenuContainer = null;
|
||||
}));
|
||||
|
||||
this.parentData.submenu.focus(selectFirstItem);
|
||||
|
||||
this.mysubmenu = this.parentData.submenu;
|
||||
@@ -368,7 +666,24 @@ class SubmenuActionItem extends MenuActionItem {
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
protected applyStyle(): void {
|
||||
super.applyStyle();
|
||||
|
||||
if (!this.menuStyle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSelected = hasClass(this.element, 'focused');
|
||||
const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor;
|
||||
|
||||
this.submenuIndicator.style.backgroundColor = fgColor ? `${fgColor}` : null;
|
||||
|
||||
if (this.parentData.submenu) {
|
||||
this.parentData.submenu.style(this.menuStyle);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.hideScheduler.dispose();
|
||||
@@ -379,8 +694,27 @@ class SubmenuActionItem extends MenuActionItem {
|
||||
}
|
||||
|
||||
if (this.submenuContainer) {
|
||||
this.submenuContainer.dispose();
|
||||
this.submenuDisposables = dispose(this.submenuDisposables);
|
||||
this.submenuContainer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MenuSeparatorActionItem extends ActionItem {
|
||||
style(style: IMenuStyles): void {
|
||||
this.label.style.borderBottomColor = style.separatorColor ? `${style.separatorColor}` : null;
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanMnemonic(label: string): string {
|
||||
const regex = MENU_MNEMONIC_REGEX;
|
||||
|
||||
const matches = regex.exec(label);
|
||||
if (!matches) {
|
||||
return label;
|
||||
}
|
||||
|
||||
const mnemonicInText = matches[0].charAt(0) === '&';
|
||||
|
||||
return label.replace(regex, mnemonicInText ? '$2' : '').trim();
|
||||
}
|
||||
974
src/vs/base/browser/ui/menu/menubar.ts
Normal file
974
src/vs/base/browser/ui/menu/menubar.ts
Normal file
@@ -0,0 +1,974 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
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 } from 'vs/base/browser/ui/menu/menu';
|
||||
import { ActionRunner, IAction, IActionRunner } from 'vs/base/common/actions';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { KeyCode, KeyCodeUtils, ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
const $ = DOM.$;
|
||||
|
||||
export interface IMenuBarOptions {
|
||||
enableMnemonics?: boolean;
|
||||
visibility?: string;
|
||||
getKeybinding?: (action: IAction) => ResolvedKeybinding;
|
||||
}
|
||||
|
||||
export interface MenuBarMenu {
|
||||
actions: IAction[];
|
||||
label: string;
|
||||
}
|
||||
|
||||
enum MenubarState {
|
||||
HIDDEN,
|
||||
VISIBLE,
|
||||
FOCUSED,
|
||||
OPEN
|
||||
}
|
||||
|
||||
export class MenuBar extends Disposable {
|
||||
|
||||
static readonly OVERFLOW_INDEX: number = -1;
|
||||
|
||||
private menuCache: {
|
||||
buttonElement: HTMLElement;
|
||||
titleElement: HTMLElement;
|
||||
label: string;
|
||||
actions?: IAction[];
|
||||
}[];
|
||||
|
||||
private overflowMenu: {
|
||||
buttonElement: HTMLElement;
|
||||
titleElement: HTMLElement;
|
||||
label: string;
|
||||
actions?: IAction[];
|
||||
};
|
||||
|
||||
private focusedMenu: {
|
||||
index: number;
|
||||
holder?: HTMLElement;
|
||||
widget?: Menu;
|
||||
};
|
||||
|
||||
private focusToReturn: HTMLElement;
|
||||
private menuUpdater: RunOnceScheduler;
|
||||
|
||||
// Input-related
|
||||
private _mnemonicsInUse: boolean;
|
||||
private openedViaKeyboard: boolean;
|
||||
private awaitingAltRelease: boolean;
|
||||
private ignoreNextMouseUp: boolean;
|
||||
private mnemonics: Map<KeyCode, number>;
|
||||
|
||||
private updatePending: boolean;
|
||||
private _focusState: MenubarState;
|
||||
private actionRunner: IActionRunner;
|
||||
|
||||
private _onVisibilityChange: Emitter<boolean>;
|
||||
private _onFocusStateChange: Emitter<boolean>;
|
||||
|
||||
private numMenusShown: number;
|
||||
private menuStyle: IMenuStyles;
|
||||
private overflowLayoutScheduled: IDisposable;
|
||||
|
||||
constructor(private container: HTMLElement, private options: IMenuBarOptions = {}) {
|
||||
super();
|
||||
|
||||
this.container.attributes['role'] = 'menubar';
|
||||
|
||||
this.menuCache = [];
|
||||
this.mnemonics = new Map<KeyCode, number>();
|
||||
|
||||
this._focusState = MenubarState.VISIBLE;
|
||||
|
||||
this._onVisibilityChange = this._register(new Emitter<boolean>());
|
||||
this._onFocusStateChange = this._register(new Emitter<boolean>());
|
||||
|
||||
this.createOverflowMenu();
|
||||
|
||||
this.menuUpdater = this._register(new RunOnceScheduler(() => this.update(), 200));
|
||||
|
||||
this.actionRunner = this._register(new ActionRunner());
|
||||
this._register(this.actionRunner.onDidBeforeRun(() => {
|
||||
this.setUnfocusedState();
|
||||
}));
|
||||
|
||||
this._register(ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
let eventHandled = true;
|
||||
const key = !!e.key ? KeyCodeUtils.fromString(e.key) : KeyCode.Unknown;
|
||||
|
||||
if (event.equals(KeyCode.LeftArrow)) {
|
||||
this.focusPrevious();
|
||||
} else if (event.equals(KeyCode.RightArrow)) {
|
||||
this.focusNext();
|
||||
} else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) {
|
||||
this.setUnfocusedState();
|
||||
} else if (!this.isOpen && !event.ctrlKey && this.options.enableMnemonics && this.mnemonicsInUse && this.mnemonics.has(key)) {
|
||||
const menuIndex = this.mnemonics.get(key);
|
||||
this.onMenuTriggered(menuIndex, false);
|
||||
} else {
|
||||
eventHandled = false;
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(window, DOM.EventType.MOUSE_DOWN, () => {
|
||||
// This mouse event is outside the menubar so it counts as a focus out
|
||||
if (this.isFocused) {
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_IN, (e) => {
|
||||
let event = e as FocusEvent;
|
||||
|
||||
if (event.relatedTarget) {
|
||||
if (!this.container.contains(event.relatedTarget as HTMLElement)) {
|
||||
this.focusToReturn = event.relatedTarget as HTMLElement;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_OUT, (e) => {
|
||||
let event = e as FocusEvent;
|
||||
|
||||
if (event.relatedTarget) {
|
||||
if (!this.container.contains(event.relatedTarget as HTMLElement)) {
|
||||
this.focusToReturn = null;
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
if (!this.options.enableMnemonics || !e.altKey || e.ctrlKey || e.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = KeyCodeUtils.fromString(e.key);
|
||||
if (!this.mnemonics.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mnemonicsInUse = true;
|
||||
this.updateMnemonicVisibility(true);
|
||||
|
||||
const menuIndex = this.mnemonics.get(key);
|
||||
this.onMenuTriggered(menuIndex, false);
|
||||
}));
|
||||
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
|
||||
push(arg: MenuBarMenu | MenuBarMenu[]): void {
|
||||
const menus: MenuBarMenu[] = !Array.isArray(arg) ? [arg] : arg;
|
||||
|
||||
menus.forEach((menuBarMenu) => {
|
||||
const menuIndex = this.menuCache.length;
|
||||
const cleanMenuLabel = cleanMnemonic(menuBarMenu.label);
|
||||
|
||||
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': cleanMenuLabel, 'aria-haspopup': true });
|
||||
const titleElement = $('div.menubar-menu-title', { 'role': 'none', 'aria-hidden': true });
|
||||
|
||||
buttonElement.appendChild(titleElement);
|
||||
this.container.insertBefore(buttonElement, this.overflowMenu.buttonElement);
|
||||
|
||||
let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(menuBarMenu.label);
|
||||
|
||||
// Register mnemonics
|
||||
if (mnemonicMatches) {
|
||||
let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[2];
|
||||
|
||||
this.registerMnemonic(this.menuCache.length, mnemonic);
|
||||
}
|
||||
|
||||
this.updateLabels(titleElement, buttonElement, menuBarMenu.label);
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
let eventHandled = true;
|
||||
|
||||
if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) {
|
||||
this.focusedMenu = { index: menuIndex };
|
||||
this.openedViaKeyboard = true;
|
||||
this.focusState = MenubarState.OPEN;
|
||||
} else {
|
||||
eventHandled = false;
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
Gesture.addTarget(buttonElement);
|
||||
this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {
|
||||
// Ignore this touch if the menu is touched
|
||||
if (this.isOpen && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ignoreNextMouseUp = false;
|
||||
this.onMenuTriggered(menuIndex, true);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e) => {
|
||||
if (!this.isOpen) {
|
||||
// Open the menu with mouse down and ignore the following mouse up event
|
||||
this.ignoreNextMouseUp = true;
|
||||
this.onMenuTriggered(menuIndex, true);
|
||||
} else {
|
||||
this.ignoreNextMouseUp = false;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {
|
||||
if (!this.ignoreNextMouseUp) {
|
||||
if (this.isFocused) {
|
||||
this.onMenuTriggered(menuIndex, true);
|
||||
}
|
||||
} else {
|
||||
this.ignoreNextMouseUp = false;
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {
|
||||
if (this.isOpen && !this.isCurrentMenu(menuIndex)) {
|
||||
this.menuCache[menuIndex].buttonElement.focus();
|
||||
this.cleanupCustomMenu();
|
||||
this.showCustomMenu(menuIndex, false);
|
||||
} else if (this.isFocused && !this.isOpen) {
|
||||
this.focusedMenu = { index: menuIndex };
|
||||
buttonElement.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
this.menuCache.push({
|
||||
label: menuBarMenu.label,
|
||||
actions: menuBarMenu.actions,
|
||||
buttonElement: buttonElement,
|
||||
titleElement: titleElement
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createOverflowMenu(): void {
|
||||
const label = nls.localize('mMore', "...");
|
||||
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': label, 'aria-haspopup': true });
|
||||
const titleElement = $('div.menubar-menu-title.toolbar-toggle-more', { 'role': 'none', 'aria-hidden': true });
|
||||
|
||||
buttonElement.appendChild(titleElement);
|
||||
this.container.appendChild(buttonElement);
|
||||
buttonElement.style.visibility = 'hidden';
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
let eventHandled = true;
|
||||
|
||||
if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) {
|
||||
this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };
|
||||
this.openedViaKeyboard = true;
|
||||
this.focusState = MenubarState.OPEN;
|
||||
} else {
|
||||
eventHandled = false;
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
Gesture.addTarget(buttonElement);
|
||||
this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {
|
||||
// Ignore this touch if the menu is touched
|
||||
if (this.isOpen && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ignoreNextMouseUp = false;
|
||||
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e) => {
|
||||
if (!this.isOpen) {
|
||||
// Open the menu with mouse down and ignore the following mouse up event
|
||||
this.ignoreNextMouseUp = true;
|
||||
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
|
||||
} else {
|
||||
this.ignoreNextMouseUp = false;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {
|
||||
if (!this.ignoreNextMouseUp) {
|
||||
if (this.isFocused) {
|
||||
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
|
||||
}
|
||||
} else {
|
||||
this.ignoreNextMouseUp = false;
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {
|
||||
if (this.isOpen && !this.isCurrentMenu(MenuBar.OVERFLOW_INDEX)) {
|
||||
this.overflowMenu.buttonElement.focus();
|
||||
this.cleanupCustomMenu();
|
||||
this.showCustomMenu(MenuBar.OVERFLOW_INDEX, false);
|
||||
} else if (this.isFocused && !this.isOpen) {
|
||||
this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };
|
||||
buttonElement.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
this.overflowMenu = {
|
||||
buttonElement: buttonElement,
|
||||
titleElement: titleElement,
|
||||
label: 'More'
|
||||
};
|
||||
}
|
||||
|
||||
updateMenu(menu: MenuBarMenu): void {
|
||||
const menuToUpdate = this.menuCache.filter(menuBarMenu => menuBarMenu.label === menu.label);
|
||||
if (menuToUpdate && menuToUpdate.length) {
|
||||
menuToUpdate[0].actions = menu.actions;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.menuCache.forEach(menuBarMenu => {
|
||||
DOM.removeNode(menuBarMenu.titleElement);
|
||||
DOM.removeNode(menuBarMenu.buttonElement);
|
||||
});
|
||||
|
||||
DOM.removeNode(this.overflowMenu.titleElement);
|
||||
DOM.removeNode(this.overflowMenu.buttonElement);
|
||||
|
||||
this.overflowLayoutScheduled = dispose(this.overflowLayoutScheduled);
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
|
||||
getWidth(): number {
|
||||
if (this.menuCache) {
|
||||
const left = this.menuCache[0].buttonElement.getBoundingClientRect().left;
|
||||
const right = this.hasOverflow ? this.overflowMenu.buttonElement.getBoundingClientRect().right : this.menuCache[this.menuCache.length - 1].buttonElement.getBoundingClientRect().right;
|
||||
return right - left;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
getHeight(): number {
|
||||
return this.container.clientHeight;
|
||||
}
|
||||
|
||||
private updateOverflowAction(): void {
|
||||
if (!this.menuCache || !this.menuCache.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sizeAvailable = this.container.offsetWidth;
|
||||
let currentSize = 0;
|
||||
let full = false;
|
||||
const prevNumMenusShown = this.numMenusShown;
|
||||
this.numMenusShown = 0;
|
||||
for (let menuBarMenu of this.menuCache) {
|
||||
if (!full) {
|
||||
const size = menuBarMenu.buttonElement.offsetWidth;
|
||||
if (currentSize + size > sizeAvailable) {
|
||||
full = true;
|
||||
} else {
|
||||
currentSize += size;
|
||||
this.numMenusShown++;
|
||||
if (this.numMenusShown > prevNumMenusShown) {
|
||||
menuBarMenu.buttonElement.style.visibility = 'visible';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (full) {
|
||||
menuBarMenu.buttonElement.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
// Overflow
|
||||
if (full) {
|
||||
// Can't fit the more button, need to remove more menus
|
||||
while (currentSize + this.overflowMenu.buttonElement.offsetWidth > sizeAvailable && this.numMenusShown > 0) {
|
||||
this.numMenusShown--;
|
||||
const size = this.menuCache[this.numMenusShown].buttonElement.offsetWidth;
|
||||
this.menuCache[this.numMenusShown].buttonElement.style.visibility = 'hidden';
|
||||
currentSize -= size;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
DOM.removeNode(this.overflowMenu.buttonElement);
|
||||
this.container.insertBefore(this.overflowMenu.buttonElement, this.menuCache[this.numMenusShown].buttonElement);
|
||||
this.overflowMenu.buttonElement.style.visibility = 'visible';
|
||||
} else {
|
||||
DOM.removeNode(this.overflowMenu.buttonElement);
|
||||
this.container.appendChild(this.overflowMenu.buttonElement);
|
||||
this.overflowMenu.buttonElement.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
private updateLabels(titleElement: HTMLElement, buttonElement: HTMLElement, label: string): void {
|
||||
const cleanMenuLabel = cleanMnemonic(label);
|
||||
|
||||
// Update the button label to reflect mnemonics
|
||||
titleElement.innerHTML = this.options.enableMnemonics ?
|
||||
strings.escape(label).replace(MENU_ESCAPED_MNEMONIC_REGEX, '<mnemonic aria-hidden="true">$1</mnemonic>') :
|
||||
cleanMenuLabel;
|
||||
|
||||
let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(label);
|
||||
|
||||
// Register mnemonics
|
||||
if (mnemonicMatches) {
|
||||
let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[2];
|
||||
|
||||
if (this.options.enableMnemonics) {
|
||||
buttonElement.setAttribute('aria-keyshortcuts', 'Alt+' + mnemonic.toLocaleLowerCase());
|
||||
} else {
|
||||
buttonElement.removeAttribute('aria-keyshortcuts');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style(style: IMenuStyles): void {
|
||||
this.menuStyle = style;
|
||||
}
|
||||
|
||||
update(options?: IMenuBarOptions): void {
|
||||
if (options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
// Don't update while using the menu
|
||||
if (this.isFocused) {
|
||||
this.updatePending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.menuCache.forEach(menuBarMenu => {
|
||||
this.updateLabels(menuBarMenu.titleElement, menuBarMenu.buttonElement, menuBarMenu.label);
|
||||
});
|
||||
|
||||
if (!this.overflowLayoutScheduled) {
|
||||
this.overflowLayoutScheduled = DOM.scheduleAtNextAnimationFrame(() => {
|
||||
this.updateOverflowAction();
|
||||
this.overflowLayoutScheduled = void 0;
|
||||
});
|
||||
}
|
||||
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
|
||||
private registerMnemonic(menuIndex: number, mnemonic: string): void {
|
||||
this.mnemonics.set(KeyCodeUtils.fromString(mnemonic), menuIndex);
|
||||
}
|
||||
|
||||
private hideMenubar(): void {
|
||||
if (this.container.style.display !== 'none') {
|
||||
this.container.style.display = 'none';
|
||||
this._onVisibilityChange.fire(false);
|
||||
}
|
||||
}
|
||||
|
||||
private showMenubar(): void {
|
||||
if (this.container.style.display !== 'flex') {
|
||||
this.container.style.display = 'flex';
|
||||
this._onVisibilityChange.fire(true);
|
||||
}
|
||||
}
|
||||
|
||||
private get focusState(): MenubarState {
|
||||
return this._focusState;
|
||||
}
|
||||
|
||||
private set focusState(value: MenubarState) {
|
||||
if (this._focusState >= MenubarState.FOCUSED && value < MenubarState.FOCUSED) {
|
||||
// Losing focus, update the menu if needed
|
||||
|
||||
if (this.updatePending) {
|
||||
this.menuUpdater.schedule();
|
||||
this.updatePending = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (value === this._focusState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isVisible = this.isVisible;
|
||||
const isOpen = this.isOpen;
|
||||
const isFocused = this.isFocused;
|
||||
|
||||
this._focusState = value;
|
||||
|
||||
switch (value) {
|
||||
case MenubarState.HIDDEN:
|
||||
if (isVisible) {
|
||||
this.hideMenubar();
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
this.cleanupCustomMenu();
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
this.focusedMenu = null;
|
||||
|
||||
if (this.focusToReturn) {
|
||||
this.focusToReturn.focus();
|
||||
this.focusToReturn = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
case MenubarState.VISIBLE:
|
||||
if (!isVisible) {
|
||||
this.showMenubar();
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
this.cleanupCustomMenu();
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
if (this.focusedMenu) {
|
||||
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
|
||||
this.overflowMenu.buttonElement.blur();
|
||||
} else {
|
||||
this.menuCache[this.focusedMenu.index].buttonElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
this.focusedMenu = null;
|
||||
|
||||
if (this.focusToReturn) {
|
||||
this.focusToReturn.focus();
|
||||
this.focusToReturn = null;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case MenubarState.FOCUSED:
|
||||
if (!isVisible) {
|
||||
this.showMenubar();
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
this.cleanupCustomMenu();
|
||||
}
|
||||
|
||||
if (this.focusedMenu) {
|
||||
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
|
||||
this.overflowMenu.buttonElement.focus();
|
||||
} else {
|
||||
this.menuCache[this.focusedMenu.index].buttonElement.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MenubarState.OPEN:
|
||||
if (!isVisible) {
|
||||
this.showMenubar();
|
||||
}
|
||||
|
||||
if (this.focusedMenu) {
|
||||
this.showCustomMenu(this.focusedMenu.index, this.openedViaKeyboard);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this._focusState = value;
|
||||
this._onFocusStateChange.fire(this.focusState >= MenubarState.FOCUSED);
|
||||
}
|
||||
|
||||
private get isVisible(): boolean {
|
||||
return this.focusState >= MenubarState.VISIBLE;
|
||||
}
|
||||
|
||||
private get isFocused(): boolean {
|
||||
return this.focusState >= MenubarState.FOCUSED;
|
||||
}
|
||||
|
||||
private get isOpen(): boolean {
|
||||
return this.focusState >= MenubarState.OPEN;
|
||||
}
|
||||
|
||||
private get hasOverflow(): boolean {
|
||||
return this.numMenusShown < this.menuCache.length;
|
||||
}
|
||||
|
||||
private setUnfocusedState(): void {
|
||||
if (this.options.visibility === 'toggle' || this.options.visibility === 'hidden') {
|
||||
this.focusState = MenubarState.HIDDEN;
|
||||
} else if (this.options.visibility === 'default' && browser.isFullscreen()) {
|
||||
this.focusState = MenubarState.HIDDEN;
|
||||
} else {
|
||||
this.focusState = MenubarState.VISIBLE;
|
||||
}
|
||||
|
||||
this.ignoreNextMouseUp = false;
|
||||
this.mnemonicsInUse = false;
|
||||
this.updateMnemonicVisibility(false);
|
||||
}
|
||||
|
||||
private focusPrevious(): void {
|
||||
|
||||
if (!this.focusedMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let newFocusedIndex = (this.focusedMenu.index - 1 + this.numMenusShown) % this.numMenusShown;
|
||||
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
|
||||
newFocusedIndex = this.numMenusShown - 1;
|
||||
} else if (this.focusedMenu.index === 0 && this.hasOverflow) {
|
||||
newFocusedIndex = MenuBar.OVERFLOW_INDEX;
|
||||
}
|
||||
|
||||
if (newFocusedIndex === this.focusedMenu.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isOpen) {
|
||||
this.cleanupCustomMenu();
|
||||
this.showCustomMenu(newFocusedIndex);
|
||||
} else if (this.isFocused) {
|
||||
this.focusedMenu.index = newFocusedIndex;
|
||||
if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {
|
||||
this.overflowMenu.buttonElement.focus();
|
||||
} else {
|
||||
this.menuCache[newFocusedIndex].buttonElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private focusNext(): void {
|
||||
if (!this.focusedMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newFocusedIndex = (this.focusedMenu.index + 1) % this.numMenusShown;
|
||||
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
|
||||
newFocusedIndex = 0;
|
||||
} else if (this.focusedMenu.index === this.numMenusShown - 1) {
|
||||
newFocusedIndex = MenuBar.OVERFLOW_INDEX;
|
||||
}
|
||||
|
||||
if (newFocusedIndex === this.focusedMenu.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isOpen) {
|
||||
this.cleanupCustomMenu();
|
||||
this.showCustomMenu(newFocusedIndex);
|
||||
} else if (this.isFocused) {
|
||||
this.focusedMenu.index = newFocusedIndex;
|
||||
if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {
|
||||
this.overflowMenu.buttonElement.focus();
|
||||
} else {
|
||||
this.menuCache[newFocusedIndex].buttonElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateMnemonicVisibility(visible: boolean): void {
|
||||
if (this.menuCache) {
|
||||
this.menuCache.forEach(menuBarMenu => {
|
||||
if (menuBarMenu.titleElement.children.length) {
|
||||
let child = menuBarMenu.titleElement.children.item(0) as HTMLElement;
|
||||
if (child) {
|
||||
child.style.textDecoration = visible ? 'underline' : null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private get mnemonicsInUse(): boolean {
|
||||
return this._mnemonicsInUse;
|
||||
}
|
||||
|
||||
private set mnemonicsInUse(value: boolean) {
|
||||
this._mnemonicsInUse = value;
|
||||
}
|
||||
|
||||
public get onVisibilityChange(): Event<boolean> {
|
||||
return this._onVisibilityChange.event;
|
||||
}
|
||||
|
||||
public get onFocusStateChange(): Event<boolean> {
|
||||
return this._onFocusStateChange.event;
|
||||
}
|
||||
|
||||
private onMenuTriggered(menuIndex: number, clicked: boolean) {
|
||||
if (this.isOpen) {
|
||||
if (this.isCurrentMenu(menuIndex)) {
|
||||
this.setUnfocusedState();
|
||||
} else {
|
||||
this.cleanupCustomMenu();
|
||||
this.showCustomMenu(menuIndex, this.openedViaKeyboard);
|
||||
}
|
||||
} else {
|
||||
this.focusedMenu = { index: menuIndex };
|
||||
this.openedViaKeyboard = !clicked;
|
||||
this.focusState = MenubarState.OPEN;
|
||||
}
|
||||
}
|
||||
|
||||
private onModifierKeyToggled(modifierKeyStatus: IModifierKeyStatus): void {
|
||||
const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey;
|
||||
|
||||
if (this.options.visibility === 'hidden') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Alt key pressed while menu is focused. This should return focus away from the menubar
|
||||
if (this.isFocused && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.altKey) {
|
||||
this.setUnfocusedState();
|
||||
this.mnemonicsInUse = false;
|
||||
this.awaitingAltRelease = true;
|
||||
}
|
||||
|
||||
// Clean alt key press and release
|
||||
if (allModifiersReleased && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.lastKeyReleased === 'alt') {
|
||||
if (!this.awaitingAltRelease) {
|
||||
if (!this.isFocused) {
|
||||
this.mnemonicsInUse = true;
|
||||
this.focusedMenu = { index: this.numMenusShown > 0 ? 0 : MenuBar.OVERFLOW_INDEX };
|
||||
this.focusState = MenubarState.FOCUSED;
|
||||
} else if (!this.isOpen) {
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alt key released
|
||||
if (!modifierKeyStatus.altKey && modifierKeyStatus.lastKeyReleased === 'alt') {
|
||||
this.awaitingAltRelease = false;
|
||||
}
|
||||
|
||||
if (this.options.enableMnemonics && this.menuCache && !this.isOpen) {
|
||||
this.updateMnemonicVisibility((!this.awaitingAltRelease && modifierKeyStatus.altKey) || this.mnemonicsInUse);
|
||||
}
|
||||
}
|
||||
|
||||
private isCurrentMenu(menuIndex: number): boolean {
|
||||
if (!this.focusedMenu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.focusedMenu.index === menuIndex;
|
||||
}
|
||||
|
||||
private cleanupCustomMenu(): void {
|
||||
if (this.focusedMenu) {
|
||||
// Remove focus from the menus first
|
||||
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
|
||||
this.overflowMenu.buttonElement.focus();
|
||||
} else {
|
||||
this.menuCache[this.focusedMenu.index].buttonElement.focus();
|
||||
}
|
||||
|
||||
if (this.focusedMenu.holder) {
|
||||
DOM.removeClass(this.focusedMenu.holder.parentElement, 'open');
|
||||
this.focusedMenu.holder.remove();
|
||||
}
|
||||
|
||||
if (this.focusedMenu.widget) {
|
||||
this.focusedMenu.widget.dispose();
|
||||
}
|
||||
|
||||
this.focusedMenu = { index: this.focusedMenu.index };
|
||||
}
|
||||
}
|
||||
|
||||
private showCustomMenu(menuIndex: number, selectFirst = true): void {
|
||||
const actualMenuIndex = menuIndex >= this.numMenusShown ? MenuBar.OVERFLOW_INDEX : menuIndex;
|
||||
const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menuCache[actualMenuIndex];
|
||||
const menuHolder = $('div.menubar-menu-items-holder');
|
||||
|
||||
DOM.addClass(customMenu.buttonElement, 'open');
|
||||
menuHolder.style.top = `${this.container.clientHeight}px`;
|
||||
menuHolder.style.left = `${customMenu.buttonElement.getBoundingClientRect().left}px`;
|
||||
|
||||
customMenu.buttonElement.appendChild(menuHolder);
|
||||
|
||||
let menuOptions: IMenuOptions = {
|
||||
getKeyBinding: this.options.getKeybinding,
|
||||
actionRunner: this.actionRunner,
|
||||
enableMnemonics: this.mnemonicsInUse && this.options.enableMnemonics,
|
||||
ariaLabel: customMenu.buttonElement.attributes['aria-label'].value
|
||||
};
|
||||
|
||||
let menuWidget = this._register(new Menu(menuHolder, customMenu.actions, menuOptions));
|
||||
menuWidget.style(this.menuStyle);
|
||||
|
||||
this._register(menuWidget.onDidCancel(() => {
|
||||
this.focusState = MenubarState.FOCUSED;
|
||||
}));
|
||||
|
||||
this._register(menuWidget.onDidBlur(() => {
|
||||
setTimeout(() => {
|
||||
this.cleanupCustomMenu();
|
||||
}, 100);
|
||||
}));
|
||||
|
||||
if (actualMenuIndex !== menuIndex) {
|
||||
menuWidget.trigger(menuIndex - this.numMenusShown);
|
||||
} else {
|
||||
menuWidget.focus(selectFirst);
|
||||
}
|
||||
|
||||
this.focusedMenu = {
|
||||
index: actualMenuIndex,
|
||||
holder: menuHolder,
|
||||
widget: menuWidget
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type ModifierKey = 'alt' | 'ctrl' | 'shift';
|
||||
|
||||
interface IModifierKeyStatus {
|
||||
altKey: boolean;
|
||||
shiftKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
lastKeyPressed?: ModifierKey;
|
||||
lastKeyReleased?: ModifierKey;
|
||||
}
|
||||
|
||||
|
||||
class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
|
||||
|
||||
private _subscriptions: IDisposable[] = [];
|
||||
private _keyStatus: IModifierKeyStatus;
|
||||
private static instance: ModifierKeyEmitter;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
|
||||
this._keyStatus = {
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
ctrlKey: false
|
||||
};
|
||||
|
||||
this._subscriptions.push(domEvent(document.body, 'keydown')(e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
|
||||
if (e.altKey && !this._keyStatus.altKey) {
|
||||
this._keyStatus.lastKeyPressed = 'alt';
|
||||
} else if (e.ctrlKey && !this._keyStatus.ctrlKey) {
|
||||
this._keyStatus.lastKeyPressed = 'ctrl';
|
||||
} else if (e.shiftKey && !this._keyStatus.shiftKey) {
|
||||
this._keyStatus.lastKeyPressed = 'shift';
|
||||
} else if (event.keyCode !== KeyCode.Alt) {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this._keyStatus.altKey = e.altKey;
|
||||
this._keyStatus.ctrlKey = e.ctrlKey;
|
||||
this._keyStatus.shiftKey = e.shiftKey;
|
||||
|
||||
if (this._keyStatus.lastKeyPressed) {
|
||||
this.fire(this._keyStatus);
|
||||
}
|
||||
}));
|
||||
this._subscriptions.push(domEvent(document.body, 'keyup')(e => {
|
||||
if (!e.altKey && this._keyStatus.altKey) {
|
||||
this._keyStatus.lastKeyReleased = 'alt';
|
||||
} else if (!e.ctrlKey && this._keyStatus.ctrlKey) {
|
||||
this._keyStatus.lastKeyReleased = 'ctrl';
|
||||
} else if (!e.shiftKey && this._keyStatus.shiftKey) {
|
||||
this._keyStatus.lastKeyReleased = 'shift';
|
||||
} else {
|
||||
this._keyStatus.lastKeyReleased = undefined;
|
||||
}
|
||||
|
||||
if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}
|
||||
|
||||
this._keyStatus.altKey = e.altKey;
|
||||
this._keyStatus.ctrlKey = e.ctrlKey;
|
||||
this._keyStatus.shiftKey = e.shiftKey;
|
||||
|
||||
if (this._keyStatus.lastKeyReleased) {
|
||||
this.fire(this._keyStatus);
|
||||
}
|
||||
}));
|
||||
this._subscriptions.push(domEvent(document.body, 'mousedown')(e => {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}));
|
||||
|
||||
|
||||
this._subscriptions.push(domEvent(window, 'blur')(e => {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
this._keyStatus.lastKeyReleased = undefined;
|
||||
this._keyStatus.altKey = false;
|
||||
this._keyStatus.shiftKey = false;
|
||||
this._keyStatus.shiftKey = false;
|
||||
|
||||
this.fire(this._keyStatus);
|
||||
}));
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!ModifierKeyEmitter.instance) {
|
||||
ModifierKeyEmitter.instance = new ModifierKeyEmitter();
|
||||
}
|
||||
|
||||
return ModifierKeyEmitter.instance;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this._subscriptions = dispose(this._subscriptions);
|
||||
}
|
||||
}
|
||||
3
src/vs/base/browser/ui/menu/submenu.svg
Normal file
3
src/vs/base/browser/ui/menu/submenu.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.52051 12.3643L9.87793 7L4.52051 1.635742L5.13574 1.0205078L11.1221 7L5.13574 12.9795L4.52051 12.3643Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 233 B |
Reference in New Issue
Block a user