electron有提供context menu的事件,但是menu需要我们自己去创建,可以实现一些自定义的功能,还有就是i18n也是在创建过程中可以去实现。
创建简单的context menu
我们先来实现最简单的copy/paste/cut的三个context menu的功能,从这三个最简单的menu开始入手。
先创建一个mainWindow,然后在mainWindow的webContents上面监听context-menu事件,然后在事件中创建menu,最后调用menu.popup()方法弹出menu。copy/paste/cut是electron已经实现的功能,我们只需要调用相应的role就可以了, 不需要自己再去实现一次click事件。
// Modules to control application life and create native browser window
const { app, BrowserWindow, Menu } = require('electron')
const path = require('node:path')
function createWindow () {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
// and load the index.html of the app.
mainWindow.loadFile('index.html')
return mainWindow
// Open the DevTools.
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
const m = createWindow()
m.webContents.on("context-menu",(_event, props) => {
const { editFlags } = props;
const template = [
{
id: 'cut',
label: 'Cut',
role: 'cut',
enabled: editFlags.canCut,
},
{
id: 'copy',
label: 'Copy',
role: 'copy',
enabled: editFlags.canCopy,
},
{
id: 'paste',
label: 'Paste',
role: 'paste',
enabled: editFlags.canPaste,
}
];
const menu = Menu.buildFromTemplate(template);
menu.popup({});
})
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
这样我们就实现了最简单的copy/paste/cut的context menu功能, 效果如下图:
import { MenuItemConstructorOptions, Menu, clipboard, app } from 'electron';
class ContextMenuController {
private createDictionarySuggestions(
properties: SpellCheckProperties,
mainWindow: Electron.BrowserWindow,
): MenuItemConstructorOptions[] {
const hasText = properties.selectionText.length > 0;
if (!hasText || !properties.misspelledWord) {
return [];
}
if (properties.dictionarySuggestions.length === 0) {
return [
{
id: 'dictionarySuggestions',
label: 'No Guesses Found',
visible: true,
enabled: false,
},
];
}
return properties.dictionarySuggestions.map((suggestion) => ({
id: 'dictionarySuggestions',
label: suggestion,
visible: Boolean(
properties.isEditable && hasText && properties.misspelledWord,
),
click: (menuItem: Electron.MenuItem) => {
mainWindow.webContents.replaceMisspelling(menuItem.label);
},
}));
}
private createEditMenuItems(
properties: SpellCheckProperties,
): MenuItemConstructorOptions[] {
const hasText = properties.selectionText.trim().length > 0;
const can = (type: string) => properties.editFlags[`can${type}`] && hasText;
const template: MenuItemConstructorOptions[] = [
{
id: 'cut',
label: 'Cu&t',
role: 'cut',
enabled: can('Cut'),
visible: properties.isEditable,
},
{
id: 'copy',
label: '&Copy',
role: 'copy',
enabled: can('Copy'),
visible: properties.isEditable || hasText,
},
{
id: 'paste',
label: '&Paste',
role: 'paste',
enabled: properties.editFlags.canPaste,
visible: properties.isEditable,
},
];
// remove role from items that are not enabled
// https://github.com/electron/electron/issues/13554
template.forEach((item) => {
if (item.enabled === false) {
item.role = undefined;
}
});
return template;
}
private createSpellCheckMenuItem(
properties: SpellCheckProperties,
mainWindow: Electron.BrowserWindow,
): MenuItemConstructorOptions {
const hasText = properties.selectionText.length > 0;
return {
id: 'learnSpelling',
label: '&Learn Spelling',
visible: Boolean(
properties.isEditable && hasText && properties.misspelledWord,
),
click: () => {
mainWindow.webContents.session.addWordToSpellCheckerDictionary(
properties.misspelledWord,
);
},
};
}
private createLinkMenuItem(
properties: SpellCheckProperties,
): MenuItemConstructorOptions {
return {
id: 'copyLink',
label: 'Copy Lin&k',
visible: properties.linkURL.length > 0 && properties.mediaType === 'none',
click: () => {
clipboard.write({
bookmark: properties.linkText,
text: properties.linkURL,
});
},
};
}
private removeInvisibleItems(
template: MenuItemConstructorOptions[],
): MenuItemConstructorOptions[] {
const filtered = template.filter((item) => item.visible !== false);
return filtered.reduce((acc, curr, index, arr) => {
if (index === 0 && curr.type === 'separator') {
return acc;
}
if (curr.type === 'separator' && arr[index - 1]?.type === 'separator') {
return acc;
}
if (index === arr.length - 1 && curr.type === 'separator') {
return acc;
}
acc.push(curr);
return acc;
}, [] as MenuItemConstructorOptions[]);
}
private contextMenu(w: Electron.BrowserWindow) {
w.webContents.on('context-menu', (_event, properties) => {
const template: MenuItemConstructorOptions[] = [
...this.createDictionarySuggestions(properties, w),
{ type: 'separator' },
this.createSpellCheckMenuItem(properties, w),
{ type: 'separator' },
...this.createEditMenuItems(properties),
this.createLinkMenuItem(properties),
];
const filteredTemplate = this.removeInvisibleItems(template);
if (filteredTemplate.length > 0) {
const menu = Menu.buildFromTemplate(filteredTemplate);
menu.popup({});
}
});
}
init(mainWindow: Electron.BrowserWindow) {
// create context menu for main window
this.contextMenu(mainWindow);
// creqte context menu for all new windows
app.on('browser-window-created', (_event, window) => {
this.contextMenu(window);
});
};
}
export { ContextMenuController };