I added two menus to Read it easy. "New Pane" and "Expand Center Panes." These functions have already been implemented with button UIs in the renderer process.
Menu needs IPC #
The menu module of Electron is in the main process, so adding menus to an Electron app needs IPC (Inter Process Communication) between the main and the renderer. IPC from the renderer to the main uses ipcRenderer.send
and ipcMain.on
. When from the main to the renderer, an IPC message uses webcontents.send
and ipcRenderer.on
. Be careful not to use ipcMain.send
, which doesn't exist. Use webcontents.send
.
State management for the menu is complicated #
The function "Expand Center Panes" has to detect two states. One is the number of panes. This function shows its button in the center pane only if the number of panes is 3 or more. If 2 or less, the button has to be hidden. The other is the state of enabled/disabled. In the state of enabled, the center pane is expanded.
Additionally, the menu also has to detect two states, enabled
and checked
. The menu "Expand Center Panes" is greyed out and unclickable if the number of panes is 2 or less. This state is not enabled
. If the center pane is expanded, the menu shows a check mark✔️. This state is checked
. Both enabled
and checked
are the instance properties of the MenuItem
class.
The issue is to manage the states in both the renderer and the main. For example, when clicking the "New Pane" button and increasing by a pane to 3 panes, the function "Expand Center Panes" detects the number of panes and shows the "Expand Center Panes" button in the renderer process. Then in the main process, the "Expand Center Panes" menu detects enabled
and gets active and clickable. Adding menus need to synchronize the renderer with the main or the main with the renderer. It is very complicated.
Make a state-transition table #
A state-transition table is very helpful for this issue. I made it below.
the number of panes | enabled(Menu) | checked(Menu) | Expand Center Panes button | Center pane |
---|---|---|---|---|
2 -> 3 | true | false | visible | normal |
3 | true | true | visible | expanded |
3 | true | false | visible | normal |
3 -> 2 | false | false | hidden | normal |
The operation flow in the table is below.
- Add a new pane
- Expand the center pane
- Unexpand the center pane(= Toggle the "Expand Center Panes" function)
- Delete a pane
If a user uses the menu, which belongs to the main, a IPC message should be sent from the main to the renderer. On the other hand, if a user clicks the button, which belongs to the renderer, an IPC message should be sent from the renderer to the main. It's clear.
The parts of the code in the main process are below. Both handleExpandPanesEnable
and handleExpandPanesCheck
receive true or false as an argument and set the value to the enabled
or checked
properties in the "Expand Center Panes" menu.
import { app, BrowserWindow, Menu, ipcMain } from 'electron';
let mainWindow;
const template = [
{
label: 'Pane',
submenu: [
{
label: 'New Pane',
click: () => { mainWindow.webContents.send('menu:new-pane'); },
},
{
label: 'Expand Center Panes',
type: 'checkbox',
enabled: false,
click: () => { mainWindow.webContents.send('menu:expand-center-panes'); },
},
],
},
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
app.on('ready', () => {
mainWindow = createMainWindow();
// Receives an IPC message from the renderer
ipcMain.on('menu:pane-expand-enable', (event, boolean) => { handleExpandPanesEnable(boolean);} );
ipcMain.on('menu:pane-expand-check', (event, boolean) => { handleExpandPanesCheck(boolean);} );
});
let windowsExpandPanesMenu = menu.items[3].submenu.items[1];
let macExpandPanesMenu = menu.items[4].submenu.items[1];
function handleExpandPanesEnable(boolean) {
isMac ? macExpandPanesMenu.enabled = boolean : windowsExpandPanesMenu.enabled = boolean;
}
function handleExpandPanesCheck(boolean) {
isMac ? macExpandPanesMenu.checked = boolean : windowsExpandPanesMenu.checked = boolean;
}
The code in the preload is below.
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
addNewPaneByMenu: (callback) => ipcRenderer.on('menu:new-pane', callback),
expandCenterPanesByMenu: (callback) => ipcRenderer.on('menu:expand-center-panes', callback),
toggleExpandPaneMenu: (boolean) => ipcRenderer.send('menu:pane-expand-enable', boolean),
toggleExpandChecked: (boolean) => ipcRenderer.send('menu:pane-expand-check', boolean),
});
The parts of the code in the renderer are below. When the renderer receives an IPC message by ipcRenderer.on('menu:expand-center-panes', callback)
, fire toggleExpander
and expand/unexpand the center pane.
window.electronAPI.expandCenterPanesByMenu(() => {
// Toggle "Expand Center Pane"
toggleExpander();
});
Summary #
Implementing a menu in an Electron app often needs IPC. If also needs state management, it is very complicated. I strongly recommend making a state-transition table or a truth table before coding.