Merge from master

This commit is contained in:
Raj Musuku
2019-02-21 17:56:04 -08:00
parent 5a146e34fa
commit 666ae11639
11482 changed files with 119352 additions and 255574 deletions

View 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

View 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

View File

@@ -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;
}

View File

@@ -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 = /(?:&amp;){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();
}

View 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);
}
}

View 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