feat: add fileMenu / viewMenu / appMenu roles (#16328)

This commit is contained in:
Milan Burda 2019-01-10 14:32:03 +01:00 committed by Alexey Kuzmin
parent 72af4941f8
commit eb02a422de
6 changed files with 271 additions and 263 deletions

View file

@ -1,77 +1,11 @@
const { shell, Menu } = require('electron') const { shell, Menu } = require('electron')
const isMac = process.platform === 'darwin'
const setDefaultApplicationMenu = () => { const setDefaultApplicationMenu = () => {
if (Menu.getApplicationMenu()) return if (Menu.getApplicationMenu()) return
const template = [ const helpMenu = {
{
label: 'Edit',
submenu: [
{
role: 'undo'
},
{
role: 'redo'
},
{
type: 'separator'
},
{
role: 'cut'
},
{
role: 'copy'
},
{
role: 'paste'
},
{
role: 'pasteandmatchstyle'
},
{
role: 'delete'
},
{
role: 'selectall'
}
]
},
{
label: 'View',
submenu: [
{
role: 'reload'
},
{
role: 'forcereload'
},
{
role: 'toggledevtools'
},
{
type: 'separator'
},
{
role: 'resetzoom'
},
{
role: 'zoomin'
},
{
role: 'zoomout'
},
{
type: 'separator'
},
{
role: 'togglefullscreen'
}
]
},
{
role: 'windowMenu'
},
{
role: 'help', role: 'help',
submenu: [ submenu: [
{ {
@ -102,62 +36,15 @@ const setDefaultApplicationMenu = () => {
} }
] ]
} }
]
if (process.platform === 'darwin') { const template = [
template.unshift({ ...(isMac ? [{ role: 'appMenu' }] : []),
label: 'Electron', { role: 'fileMenu' },
submenu: [ { role: 'editMenu' },
{ { role: 'viewMenu' },
role: 'about' { role: 'windowMenu' },
}, helpMenu
{
type: 'separator'
},
{
role: 'services'
},
{
type: 'separator'
},
{
role: 'hide'
},
{
role: 'hideothers'
},
{
role: 'unhide'
},
{
type: 'separator'
},
{
role: 'quit'
}
] ]
})
template[1].submenu.push({
type: 'separator'
}, {
label: 'Speech',
submenu: [
{
role: 'startspeaking'
},
{
role: 'stopspeaking'
}
]
})
} else {
template.unshift({
label: 'File',
submenu: [{
role: 'quit'
}]
})
}
const menu = Menu.buildFromTemplate(template) const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu) Menu.setApplicationMenu(menu)

View file

@ -82,11 +82,14 @@ The `role` property can have following values:
* `resetZoom` - Reset the focused page's zoom level to the original size. * `resetZoom` - Reset the focused page's zoom level to the original size.
* `zoomIn` - Zoom in the focused page by 10%. * `zoomIn` - Zoom in the focused page by 10%.
* `zoomOut` - Zoom out the focused page by 10%. * `zoomOut` - Zoom out the focused page by 10%.
* `fileMenu` - Whole default "File" menu (Close / Quit)
* `editMenu` - Whole default "Edit" menu (Undo, Copy, etc.). * `editMenu` - Whole default "Edit" menu (Undo, Copy, etc.).
* `viewMenu` - Whole default "View" menu (Reload, Toggle Developer Tools, etc.)
* `windowMenu` - Whole default "Window" menu (Minimize, Close, etc.). * `windowMenu` - Whole default "Window" menu (Minimize, Close, etc.).
The following additional roles are available on _macOS_: The following additional roles are available on _macOS_:
* `appMenu` - Whole default "App" menu (About, Services, etc.)
* `about` - Map to the `orderFrontStandardAboutPanel` action. * `about` - Map to the `orderFrontStandardAboutPanel` action.
* `hide` - Map to the `hide` action. * `hide` - Map to the `hide` action.
* `hideOthers` - Map to the `hideOtherApplications` action. * `hideOthers` - Map to the `hideOtherApplications` action.

View file

@ -158,6 +158,29 @@ simple template API:
const { app, Menu } = require('electron') const { app, Menu } = require('electron')
const template = [ const template = [
// { role: 'appMenu' }
...(process.platform === 'darwin' ? [{
label: app.getName(),
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
}] : []),
// { role: 'fileMenu' }
{
label: 'File',
submenu: [
isMac ? { role: 'close' } : { role: 'quit' }
]
},
// { role: 'editMenu' }
{ {
label: 'Edit', label: 'Edit',
submenu: [ submenu: [
@ -167,11 +190,26 @@ const template = [
{ role: 'cut' }, { role: 'cut' },
{ role: 'copy' }, { role: 'copy' },
{ role: 'paste' }, { role: 'paste' },
{ role: 'pasteandmatchstyle' }, ...(isMac ? [
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' }, { role: 'delete' },
{ role: 'selectall' } { role: 'selectAll' },
{ type: 'separator' },
{
label: 'Speech',
submenu: [
{ role: 'startspeaking' },
{ role: 'stopspeaking' }
]
}
] : [
{ role: 'delete' },
{ type: 'separator' },
{ role: 'selectAll' }
])
] ]
}, },
// { role: 'viewMenu' }
{ {
label: 'View', label: 'View',
submenu: [ submenu: [
@ -186,11 +224,20 @@ const template = [
{ role: 'togglefullscreen' } { role: 'togglefullscreen' }
] ]
}, },
// { role: 'windowMenu' }
{ {
role: 'window', label: 'Window',
submenu: [ submenu: [
{ role: 'minimize' }, { role: 'minimize' },
{ role: 'zoom' },
...(isMac ? [
{ type: 'separator' },
{ role: 'front' },
{ type: 'separator' },
{ role: 'window' }
] : [
{ role: 'close' } { role: 'close' }
])
] ]
}, },
{ {
@ -204,44 +251,6 @@ const template = [
} }
] ]
if (process.platform === 'darwin') {
template.unshift({
label: app.getName(),
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
})
// Edit menu
template[1].submenu.push(
{ type: 'separator' },
{
label: 'Speech',
submenu: [
{ role: 'startspeaking' },
{ role: 'stopspeaking' }
]
}
)
// Window menu
template[3].submenu = [
{ role: 'close' },
{ role: 'minimize' },
{ role: 'zoom' },
{ type: 'separator' },
{ role: 'front' }
]
}
const menu = Menu.buildFromTemplate(template) const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu) Menu.setApplicationMenu(menu)
``` ```

View file

@ -2,14 +2,18 @@
const { app } = require('electron') const { app } = require('electron')
const isMac = process.platform === 'darwin'
const isWindows = process.platform === 'win32'
const isLinux = process.platform === 'linux'
const roles = { const roles = {
about: { about: {
get label () { get label () {
return process.platform === 'linux' ? 'About' : `About ${app.getName()}` return isLinux ? 'About' : `About ${app.getName()}`
} }
}, },
close: { close: {
label: process.platform === 'darwin' ? 'Close Window' : 'Close', label: isMac ? 'Close Window' : 'Close',
accelerator: 'CommandOrControl+W', accelerator: 'CommandOrControl+W',
windowMethod: 'close' windowMethod: 'close'
}, },
@ -78,12 +82,12 @@ const roles = {
default: return 'Quit' default: return 'Quit'
} }
}, },
accelerator: process.platform === 'win32' ? null : 'CommandOrControl+Q', accelerator: isWindows ? null : 'CommandOrControl+Q',
appMethod: 'quit' appMethod: 'quit'
}, },
redo: { redo: {
label: 'Redo', label: 'Redo',
accelerator: process.platform === 'win32' ? 'Control+Y' : 'Shift+CommandOrControl+Z', accelerator: isWindows ? 'Control+Y' : 'Shift+CommandOrControl+Z',
webContentsMethod: 'redo' webContentsMethod: 'redo'
}, },
reload: { reload: {
@ -122,13 +126,13 @@ const roles = {
}, },
toggledevtools: { toggledevtools: {
label: 'Toggle Developer Tools', label: 'Toggle Developer Tools',
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', accelerator: isMac ? 'Alt+Command+I' : 'Ctrl+Shift+I',
nonNativeMacOSRole: true, nonNativeMacOSRole: true,
windowMethod: 'toggleDevTools' windowMethod: 'toggleDevTools'
}, },
togglefullscreen: { togglefullscreen: {
label: 'Toggle Full Screen', label: 'Toggle Full Screen',
accelerator: process.platform === 'darwin' ? 'Control+Command+F' : 'F11', accelerator: isMac ? 'Control+Command+F' : 'F11',
windowMethod: (window) => { windowMethod: (window) => {
window.setFullScreen(!window.isFullScreen()) window.setFullScreen(!window.isFullScreen())
} }
@ -167,79 +171,95 @@ const roles = {
}) })
} }
}, },
// Edit submenu (should fit both Mac & Windows) // App submenu should be used for Mac only
appmenu: {
get label () {
return app.getName()
},
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
},
// File submenu
filemenu: {
label: 'File',
submenu: [
isMac ? { role: 'close' } : { role: 'quit' }
]
},
// Edit submenu
editmenu: { editmenu: {
label: 'Edit', label: 'Edit',
submenu: [ submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
...(isMac ? [
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' },
{ type: 'separator' },
{ {
role: 'undo' label: 'Speech',
}, submenu: [
{ { role: 'startspeaking' },
role: 'redo' { role: 'stopspeaking' }
}, ]
{
type: 'separator'
},
{
role: 'cut'
},
{
role: 'copy'
},
{
role: 'paste'
},
process.platform === 'darwin' ? {
role: 'pasteAndMatchStyle'
} : null,
{
role: 'delete'
},
process.platform === 'win32' ? {
type: 'separator'
} : null,
{
role: 'selectAll'
} }
] : [
{ role: 'delete' },
{ type: 'separator' },
{ role: 'selectAll' }
])
] ]
}, },
// View submenu
// Window submenu should be used for Mac only viewmenu: {
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forcereload' },
{ role: 'toggledevtools' },
{ type: 'separator' },
{ role: 'resetzoom' },
{ role: 'zoomin' },
{ role: 'zoomout' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
},
// Window submenu
windowmenu: { windowmenu: {
label: 'Window', label: 'Window',
submenu: [ submenu: [
{ { role: 'minimize' },
role: 'minimize' { role: 'zoom' },
}, ...(isMac ? [
{ { type: 'separator' },
role: 'zoom' { role: 'front' },
}, { type: 'separator' },
process.platform !== 'darwin' ? { { role: 'window' }
label: 'close' ] : [
} : null, { role: 'close' }
process.platform === 'darwin' ? { ])
type: 'separator'
} : null,
process.platform === 'darwin' ? {
role: 'front'
} : null,
process.platform === 'darwin' ? {
type: 'separator'
} : null,
process.platform === 'darwin' ? {
role: 'window'
} : null
] ]
} }
} }
const canExecuteRole = (role) => { const canExecuteRole = (role) => {
if (!roles.hasOwnProperty(role)) return false if (!roles.hasOwnProperty(role)) return false
if (process.platform !== 'darwin') return true if (!isMac) return true
// macOS handles all roles natively except for a few // macOS handles all roles natively except for a few
return roles[role].nonNativeMacOSRole return roles[role].nonNativeMacOSRole

View file

@ -159,13 +159,13 @@ Menu.buildFromTemplate = function (template) {
if (!Array.isArray(template)) { if (!Array.isArray(template)) {
throw new TypeError('Invalid template for Menu: Menu template must be an array') throw new TypeError('Invalid template for Menu: Menu template must be an array')
} }
const menu = new Menu()
if (!areValidTemplateItems(template)) { if (!areValidTemplateItems(template)) {
throw new TypeError('Invalid template for MenuItem: must have at least one of label, role or type') throw new TypeError('Invalid template for MenuItem: must have at least one of label, role or type')
} }
const filtered = removeExtraSeparators(template) const filtered = removeExtraSeparators(template)
const sorted = sortTemplate(filtered) const sorted = sortTemplate(filtered)
const menu = new Menu()
sorted.forEach((item) => menu.append(new MenuItem(item))) sorted.forEach((item) => menu.append(new MenuItem(item)))
return menu return menu

View file

@ -306,6 +306,64 @@ describe('MenuItems', () => {
}) })
}) })
describe('MenuItem appMenu', () => {
before(function () {
if (process.platform !== 'darwin') {
this.skip()
}
})
it('includes a default submenu layout when submenu is empty', () => {
const item = new MenuItem({ role: 'appMenu' })
expect(item.label).to.equal(app.getName())
expect(item.submenu.items[0].role).to.equal('about')
expect(item.submenu.items[1].type).to.equal('separator')
expect(item.submenu.items[2].role).to.equal('services')
expect(item.submenu.items[3].type).to.equal('separator')
expect(item.submenu.items[4].role).to.equal('hide')
expect(item.submenu.items[5].role).to.equal('hideothers')
expect(item.submenu.items[6].role).to.equal('unhide')
expect(item.submenu.items[7].type).to.equal('separator')
expect(item.submenu.items[8].role).to.equal('quit')
})
it('overrides default layout when submenu is specified', () => {
const item = new MenuItem({
role: 'appMenu',
submenu: [{
role: 'close'
}]
})
expect(item.label).to.equal(app.getName())
expect(item.submenu.items[0].role).to.equal('close')
})
})
describe('MenuItem fileMenu', () => {
it('includes a default submenu layout when submenu is empty', () => {
const item = new MenuItem({ role: 'fileMenu' })
expect(item.label).to.equal('File')
if (process.platform === 'darwin') {
expect(item.submenu.items[0].role).to.equal('close')
} else {
expect(item.submenu.items[0].role).to.equal('quit')
}
})
it('overrides default layout when submenu is specified', () => {
const item = new MenuItem({
role: 'fileMenu',
submenu: [{
role: 'about'
}]
})
expect(item.label).to.equal('File')
expect(item.submenu.items[0].role).to.equal('about')
})
})
describe('MenuItem editMenu', () => { describe('MenuItem editMenu', () => {
it('includes a default submenu layout when submenu is empty', () => { it('includes a default submenu layout when submenu is empty', () => {
const item = new MenuItem({ role: 'editMenu' }) const item = new MenuItem({ role: 'editMenu' })
@ -322,9 +380,11 @@ describe('MenuItems', () => {
expect(item.submenu.items[6].role).to.equal('pasteandmatchstyle') expect(item.submenu.items[6].role).to.equal('pasteandmatchstyle')
expect(item.submenu.items[7].role).to.equal('delete') expect(item.submenu.items[7].role).to.equal('delete')
expect(item.submenu.items[8].role).to.equal('selectall') expect(item.submenu.items[8].role).to.equal('selectall')
} expect(item.submenu.items[9].type).to.equal('separator')
expect(item.submenu.items[10].label).to.equal('Speech')
if (process.platform === 'win32') { expect(item.submenu.items[10].submenu.items[0].role).to.equal('startspeaking')
expect(item.submenu.items[10].submenu.items[1].role).to.equal('stopspeaking')
} else {
expect(item.submenu.items[6].role).to.equal('delete') expect(item.submenu.items[6].role).to.equal('delete')
expect(item.submenu.items[7].type).to.equal('separator') expect(item.submenu.items[7].type).to.equal('separator')
expect(item.submenu.items[8].role).to.equal('selectall') expect(item.submenu.items[8].role).to.equal('selectall')
@ -343,6 +403,34 @@ describe('MenuItems', () => {
}) })
}) })
describe('MenuItem viewMenu', () => {
it('includes a default submenu layout when submenu is empty', () => {
const item = new MenuItem({ role: 'viewMenu' })
expect(item.label).to.equal('View')
expect(item.submenu.items[0].role).to.equal('reload')
expect(item.submenu.items[1].role).to.equal('forcereload')
expect(item.submenu.items[2].role).to.equal('toggledevtools')
expect(item.submenu.items[3].type).to.equal('separator')
expect(item.submenu.items[4].role).to.equal('resetzoom')
expect(item.submenu.items[5].role).to.equal('zoomin')
expect(item.submenu.items[6].role).to.equal('zoomout')
expect(item.submenu.items[7].type).to.equal('separator')
expect(item.submenu.items[8].role).to.equal('togglefullscreen')
})
it('overrides default layout when submenu is specified', () => {
const item = new MenuItem({
role: 'viewMenu',
submenu: [{
role: 'close'
}]
})
expect(item.label).to.equal('View')
expect(item.submenu.items[0].role).to.equal('close')
})
})
describe('MenuItem windowMenu', () => { describe('MenuItem windowMenu', () => {
it('includes a default submenu layout when submenu is empty', () => { it('includes a default submenu layout when submenu is empty', () => {
const item = new MenuItem({ role: 'windowMenu' }) const item = new MenuItem({ role: 'windowMenu' })
@ -354,9 +442,10 @@ describe('MenuItems', () => {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
expect(item.submenu.items[2].type).to.equal('separator') expect(item.submenu.items[2].type).to.equal('separator')
expect(item.submenu.items[3].role).to.equal('front') expect(item.submenu.items[3].role).to.equal('front')
expect(item.submenu.items[4].type).to.equal('separator') expect(item.submenu.items[4].type).to.equal('separator')
expect(item.submenu.items[5].role).to.equal('window') expect(item.submenu.items[5].role).to.equal('window')
} else {
expect(item.submenu.items[2].role).to.equal('close')
} }
}) })