diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index b4c67882ff6..5fdb8d19292 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -17,6 +17,9 @@ import { $, Builder } from 'vs/base/browser/builder'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IDisposable } from 'vs/base/common/lifecycle'; +export const MENU_MNEMONIC_REGEX: RegExp = /\(&{1,2}(\w)\)|&{1,2}(\w)/; +export const MENU_ESCAPED_MNEMONIC_REGEX: RegExp = /(?:&){1,2}(\w)/; + export interface IMenuOptions { context?: any; actionItemProvider?: IActionItemProvider; @@ -208,9 +211,6 @@ interface IMenuItemOptions extends IActionItemOptions { } class MenuActionItem extends BaseActionItem { - static MNEMONIC_REGEX: RegExp = /&&(.)/; - static ESCAPED_MNEMONIC_REGEX: RegExp = /&&(.)/; - public container: HTMLElement; protected $e: Builder; protected $label: Builder; @@ -232,9 +232,9 @@ class MenuActionItem extends BaseActionItem { if (this.options.label && options.enableMnemonics) { let label = this.getAction().label; if (label) { - let matches = MenuActionItem.MNEMONIC_REGEX.exec(label); - if (matches && matches.length === 2) { - this.mnemonic = KeyCodeUtils.fromString(matches[1].toLocaleLowerCase()); + let matches = MENU_MNEMONIC_REGEX.exec(label); + if (matches) { + this.mnemonic = KeyCodeUtils.fromString((!!matches[1] ? matches[1] : matches[2]).toLocaleLowerCase()); } } } @@ -257,10 +257,10 @@ class MenuActionItem extends BaseActionItem { } this.$check = $('span.menu-item-check').attr({ 'role': 'none' }).appendTo(this.$e); - this.$label = $('span.action-label').attr({ 'role': 'none' }).appendTo(this.$e); + this.$label = $('span.action-label').appendTo(this.$e); if (this.options.label && this.options.keybinding) { - $('span.keybinding').attr({ 'role': 'none' }).text(this.options.keybinding).appendTo(this.$e); + $('span.keybinding').text(this.options.keybinding).appendTo(this.$e); } this._updateClass(); @@ -278,30 +278,23 @@ class MenuActionItem extends BaseActionItem { _updateLabel(): void { if (this.options.label) { let label = this.getAction().label; - let mnemonic: string; if (label) { - let matches = MenuActionItem.MNEMONIC_REGEX.exec(label); - if (matches && matches.length === 2) { - mnemonic = matches[1]; - - let ariaLabel = label.replace(MenuActionItem.MNEMONIC_REGEX, mnemonic); - - this.mnemonic = KeyCodeUtils.fromString(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; } - if (this.options.enableMnemonics && mnemonic) { - label = strings.escape(label).replace(MenuActionItem.ESCAPED_MNEMONIC_REGEX, '$1'); - this.$e.attr({ 'aria-keyshortcuts': mnemonic.toLocaleLowerCase() }); - } else { - label = strings.escape(label).replace(MenuActionItem.ESCAPED_MNEMONIC_REGEX, '$1'); + this.$label.attr('aria-label', cleanLabel); + + const matches = MENU_MNEMONIC_REGEX.exec(label); + + if (matches) { + label = strings.escape(label).replace(MENU_ESCAPED_MNEMONIC_REGEX, ''); + this.$e.attr({ 'aria-keyshortcuts': (!!matches[1] ? matches[1] : matches[2]).toLocaleLowerCase() }); } } - this.$label.innerHtml(label); + this.$label.innerHtml(label.trim()); } } @@ -524,3 +517,16 @@ class SubmenuActionItem extends MenuActionItem { } } } + +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(); +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 3650edb20d2..ea32ff76863 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -7,6 +7,7 @@ import * as nls from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; +import * as strings from 'vs/base/common/strings'; import { IMenubarMenu, IMenubarMenuItemAction, IMenubarMenuItemSubmenu, IMenubarKeybinding } from 'vs/platform/menubar/common/menubar'; import { IMenuService, MenuId, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; @@ -17,7 +18,7 @@ import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import * as DOM from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { isMacintosh } from 'vs/base/common/platform'; -import { Menu, IMenuOptions, SubmenuAction } from 'vs/base/browser/ui/menu/menu'; +import { Menu, IMenuOptions, SubmenuAction, MENU_MNEMONIC_REGEX, cleanMnemonic, MENU_ESCAPED_MNEMONIC_REGEX } from 'vs/base/browser/ui/menu/menu'; import { KeyCode, KeyCodeUtils } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; @@ -523,7 +524,7 @@ export class MenubarControl extends Disposable { break; } - return this.currentEnableMenuBarMnemonics ? label : label.replace(/&&(.)/g, '$1'); + return label; } private createOpenRecentMenuAction(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI, commandId: string, isFile: boolean): IAction { @@ -657,10 +658,12 @@ export class MenubarControl extends Disposable { for (let menuTitle of Object.keys(this.topLevelMenus)) { const menu: IMenu = this.topLevelMenus[menuTitle]; let menuIndex = idx++; + const cleanMenuLabel = cleanMnemonic(this.topLevelTitles[menuTitle]); // Create the top level menu button element if (firstTimeSetup) { - const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': 0, 'aria-label': this.topLevelTitles[menuTitle].replace(/&&(.)/g, '$1'), 'aria-haspopup': true }); + + const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': 0, 'aria-label': cleanMenuLabel, 'aria-haspopup': true }); const titleElement = $('div.menubar-menu-title', { 'role': 'none', 'aria-hidden': true }); buttonElement.appendChild(titleElement); @@ -674,18 +677,22 @@ export class MenubarControl extends Disposable { } // Update the button label to reflect mnemonics - let displayTitle = this.topLevelTitles[menuTitle].replace(/&&(.)/g, this.currentEnableMenuBarMnemonics ? '' : '$1'); - this.customMenus[menuIndex].titleElement.innerHTML = displayTitle; + this.customMenus[menuIndex].titleElement.innerHTML = this.currentEnableMenuBarMnemonics ? + strings.escape(this.topLevelTitles[menuTitle]).replace(MENU_ESCAPED_MNEMONIC_REGEX, '') : + cleanMenuLabel; + + let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(this.topLevelTitles[menuTitle]); // Register mnemonics - let mnemonic = (/&&(.)/g).exec(this.topLevelTitles[menuTitle]); - if (mnemonic && mnemonic[1]) { + if (mnemonicMatches) { + let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[2]; + if (firstTimeSetup) { - this.registerMnemonic(menuIndex, mnemonic[1]); + this.registerMnemonic(menuIndex, mnemonic); } if (this.currentEnableMenuBarMnemonics) { - this.customMenus[menuIndex].buttonElement.setAttribute('aria-keyshortcuts', 'Alt+' + mnemonic[1].toLocaleLowerCase()); + this.customMenus[menuIndex].buttonElement.setAttribute('aria-keyshortcuts', 'Alt+' + mnemonic.toLocaleLowerCase()); } else { this.customMenus[menuIndex].buttonElement.removeAttribute('aria-keyshortcuts'); } @@ -1028,7 +1035,7 @@ export class MenubarControl extends Disposable { let menuOptions: IMenuOptions = { getKeyBinding: (action) => this.keybindingService.lookupKeybinding(action.id), actionRunner: this.actionRunner, - enableMnemonics: this.mnemonicsInUse, + enableMnemonics: this.mnemonicsInUse && this.currentEnableMenuBarMnemonics, ariaLabel: customMenu.buttonElement.attributes['aria-label'].value };