Atom/src/main-process/application-menu.js

249 lines
7.7 KiB
JavaScript

const { app, Menu } = require('electron');
const _ = require('underscore-plus');
const MenuHelpers = require('../menu-helpers');
// Used to manage the global application menu.
//
// It's created by {AtomApplication} upon instantiation and used to add, remove
// and maintain the state of all menu items.
module.exports = class ApplicationMenu {
constructor(version, autoUpdateManager) {
this.version = version;
this.autoUpdateManager = autoUpdateManager;
this.windowTemplates = new WeakMap();
this.setActiveTemplate(this.getDefaultTemplate());
this.autoUpdateManager.on('state-changed', state =>
this.showUpdateMenuItem(state)
);
}
// Public: Updates the entire menu with the given keybindings.
//
// window - The BrowserWindow this menu template is associated with.
// template - The Object which describes the menu to display.
// keystrokesByCommand - An Object where the keys are commands and the values
// are Arrays containing the keystroke.
update(window, template, keystrokesByCommand) {
this.translateTemplate(template, keystrokesByCommand);
this.substituteVersion(template);
this.windowTemplates.set(window, template);
if (window === this.lastFocusedWindow)
return this.setActiveTemplate(template);
}
setActiveTemplate(template) {
if (!_.isEqual(template, this.activeTemplate)) {
this.activeTemplate = template;
this.menu = Menu.buildFromTemplate(_.deepClone(template));
Menu.setApplicationMenu(this.menu);
}
return this.showUpdateMenuItem(this.autoUpdateManager.getState());
}
// Register a BrowserWindow with this application menu.
addWindow(window) {
if (this.lastFocusedWindow == null) this.lastFocusedWindow = window;
const focusHandler = () => {
this.lastFocusedWindow = window;
const template = this.windowTemplates.get(window);
if (template) this.setActiveTemplate(template);
};
window.on('focus', focusHandler);
window.once('closed', () => {
if (window === this.lastFocusedWindow) this.lastFocusedWindow = null;
this.windowTemplates.delete(window);
window.removeListener('focus', focusHandler);
});
this.enableWindowSpecificItems(true);
}
// Flattens the given menu and submenu items into an single Array.
//
// menu - A complete menu configuration object for atom-shell's menu API.
//
// Returns an Array of native menu items.
flattenMenuItems(menu) {
const object = menu.items || {};
let items = [];
for (let index in object) {
const item = object[index];
items.push(item);
if (item.submenu)
items = items.concat(this.flattenMenuItems(item.submenu));
}
return items;
}
// Flattens the given menu template into an single Array.
//
// template - An object describing the menu item.
//
// Returns an Array of native menu items.
flattenMenuTemplate(template) {
let items = [];
for (let item of template) {
items.push(item);
if (item.submenu)
items = items.concat(this.flattenMenuTemplate(item.submenu));
}
return items;
}
// Public: Used to make all window related menu items are active.
//
// enable - If true enables all window specific items, if false disables all
// window specific items.
enableWindowSpecificItems(enable) {
for (let item of this.flattenMenuItems(this.menu)) {
if (item.metadata && item.metadata.windowSpecific) item.enabled = enable;
}
}
// Replaces VERSION with the current version.
substituteVersion(template) {
let item = this.flattenMenuTemplate(template).find(
({ label }) => label === 'VERSION'
);
if (item) item.label = `Version ${this.version}`;
}
// Sets the proper visible state the update menu items
showUpdateMenuItem(state) {
const items = this.flattenMenuItems(this.menu);
const checkForUpdateItem = items.find(
({ label }) => label === 'Check for Update'
);
const checkingForUpdateItem = items.find(
({ label }) => label === 'Checking for Update'
);
const downloadingUpdateItem = items.find(
({ label }) => label === 'Downloading Update'
);
const installUpdateItem = items.find(
({ label }) => label === 'Restart and Install Update'
);
if (
!checkForUpdateItem ||
!checkingForUpdateItem ||
!downloadingUpdateItem ||
!installUpdateItem
)
return;
checkForUpdateItem.visible = false;
checkingForUpdateItem.visible = false;
downloadingUpdateItem.visible = false;
installUpdateItem.visible = false;
switch (state) {
case 'idle':
case 'error':
case 'no-update-available':
checkForUpdateItem.visible = true;
break;
case 'checking':
checkingForUpdateItem.visible = true;
break;
case 'downloading':
downloadingUpdateItem.visible = true;
break;
case 'update-available':
installUpdateItem.visible = true;
break;
}
}
// Default list of menu items.
//
// Returns an Array of menu item Objects.
getDefaultTemplate() {
return [
{
label: 'Atom',
submenu: [
{
label: 'Check for Update',
metadata: { autoUpdate: true }
},
{
label: 'Reload',
accelerator: 'Command+R',
click: () => {
const window = this.focusedWindow();
if (window) window.reload();
}
},
{
label: 'Close Window',
accelerator: 'Command+Shift+W',
click: () => {
const window = this.focusedWindow();
if (window) window.close();
}
},
{
label: 'Toggle Dev Tools',
accelerator: 'Command+Alt+I',
click: () => {
const window = this.focusedWindow();
if (window) window.toggleDevTools();
}
},
{
label: 'Quit',
accelerator: 'Command+Q',
click: () => app.quit()
}
]
}
];
}
focusedWindow() {
return global.atomApplication
.getAllWindows()
.find(window => window.isFocused());
}
// Combines a menu template with the appropriate keystroke.
//
// template - An Object conforming to atom-shell's menu api but lacking
// accelerator and click properties.
// keystrokesByCommand - An Object where the keys are commands and the values
// are Arrays containing the keystroke.
//
// Returns a complete menu configuration object for atom-shell's menu API.
translateTemplate(template, keystrokesByCommand) {
template.forEach(item => {
if (item.metadata == null) item.metadata = {};
if (item.command) {
const keystrokes = keystrokesByCommand[item.command];
if (keystrokes && keystrokes.length > 0) {
const keystroke = keystrokes[0];
// Electron does not support multi-keystroke accelerators. Therefore,
// when the command maps to a multi-stroke key binding, show the
// keystrokes next to the item's label.
if (keystroke.includes(' ')) {
item.label += ` [${_.humanizeKeystroke(keystroke)}]`;
} else {
item.accelerator = MenuHelpers.acceleratorForKeystroke(keystroke);
}
}
item.click = () =>
global.atomApplication.sendCommand(item.command, item.commandDetail);
if (!/^application:/.test(item.command)) {
item.metadata.windowSpecific = true;
}
}
if (item.submenu)
this.translateTemplate(item.submenu, keystrokesByCommand);
});
return template;
}
};