How to Create Custom Desktop Menus in Electron

WORKBENCH
3 min read

Here at DoltHub, we've been working hard turning our Dolt Workbench web application into a desktop app using Electron. In my previous blog, I talked about how to build an Electron app with Next.js. There may be situations where you need to modify the default Electron menus to add or remove items, enabling functionality specific to your application. We wanted to add menu items and create custom entries for the Dolt Workbench that navigate to specific features in the application. This blog will show you how to modify Electron menus to suit your app’s needs.

Creating a custom menu

Every Electron app has a main process, which acts as the application's entry point. It manages the app's lifecycle and communicates with renderer processes. This is where you will define a menu template, which is an array of options for constructing menu items. Then, we will use buildFromTemplate to generate the menu and setApplicationMenu to apply it to the application.

Creating a Basic Menu on macOS

On macOS, the menu is set as the application menu:

import { Menu, app } from "electron";

const name = app.getName();
const menuTemplate = [
  {
    label: name,
  },
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

Creating a Menu on Windows and Linux

On Windows and Linux, the menu will be set as each window's top menu.

import { BrowserWindow, Menu, app } from "electron";

const win = new BrowserWindow({
  width: 1280,
  height: 680,
  title: "Dolt-SQL-Workbench",
  webPreferences: {
    preload: __dirname + "/preload.js",
  },
});

const name = app.getName();
const menuTemplate = [
  {
    label: name,
  },
];

const menu = Menu.buildFromTemplate(template);
win.setMenu(menu);

Defining menu items

At the moment, our menu is empty. Let’s add a About and a Quit Menu Item. By using standard roles like about and quit, the app will automatically apply the built-in behaviors for these actions.

const name = app.getName();
const template = [
  {
    label: name,
    submenu: [
      {
        role: "about",
      },
      {
        type: "separator",
      },
      {
        role: "quit",
      },
    ],
  },
];

We can handle menu item clicks to perform specific actions. For example, to toggle the developer console, add a menu item with a keyboard shortcut.

Toggle developer tool

When the "Toggle Developer Tools" menu is clicked, call webContents.toggleDevTools(). The shortcut is set as Ctrl+Shift+I for Windows/Linux and Alt+Command+I for macOS.

const template = [
  {
    label: name,
    submenu: [
      {
        role: "quit",
      },
    ],
  },
  {
    label: "Tools",
    submenu: [
      {
        label: "Toggle Developer Tools",
        accelerator:
          process.platform === "darwin" ? "Alt+Command+I" : "Ctrl+Shift+I",
        click(_item, focusedWindow) {
          if (focusedWindow) focusedWindow.webContents.toggleDevTools();
        },
      },
    ],
  },
];

Enable menu item conditionally

You can use the enabled field to enable a menu item only in development mode.

const isDev = process.env.NODE_ENV === "development";

const template = [
  {
    label: "Tools",
    submenu: [
      {
        label: "Toggle Developer Tools",
        enabled: isDev,
        accelerator:
          process.platform === "darwin" ? "Alt+Command+I" : "Ctrl+Shift+I",
        click(_item, focusedWindow) {
          if (focusedWindow) focusedWindow.webContents.toggleDevTools();
        },
      },
    ],
  },
];

To navigate to specific features using the menu, you need to communicate between the main and renderer process using IPC.

In Electron, the ipcMain and ipcRenderer modules allow processes to send messages through custom "channels". We will add a "Commit Graph" item our application menu as an example. When the "Commit Graph" menu is clicked, the main process sends a message to the renderer process. In the preload script, we expose an onMenuClicked handler to the renderer process. The renderer process listens for this event and navigates to the commit graph page when triggered.

commit graph menu

Sending the message from the menu

In the main process, we create a custom menu that sends an IPC message to the renderer using the webContents.send API. For example, when the "Commit Graph" menu item is clicked, it sends a "commit-graph" message through the menu-clicked channel:

const template = [
  {
    label: "Commit Graph",
    accelerator: "CmdOrCtrl+G",
    click: () => win.webContents.send("menu-clicked", "commit-graph"),
  },
];

Exposing ipcRender.on via preload

Since the renderer process doesn’t have direct access to Node.js or Electron modules, we can use the contextBridge API in the preload script to expose specific APIs.

// in preload.ts

const handler = {
  onMenuClicked: (callback: (value: string) => {}) =>
    ipcRenderer.on("menu-clicked", (_event, value) => callback(value)),
};

contextBridge.exposeInMainWorld("ipc", handler);

export type IpcHandler = typeof handler;

After loading the preload script, the renderer process can access to the window.ipc.onMenuClicked() function.

Handling navigation in the renderer process

In the renderer, we create a hook to handle navigation based on the menu message. This hook listens for the menu-clicked event and shows the commit graph when the event is triggered and "commit-graph" message is received.

export default function useElectronMenu(params: DatabasePageParams) {
  const router = useRouter();
  const { defaultBranchName } = useDefaultBranch(params);

  window.ipc.onMenuClicked(async (value: string) => {
    const paramsWithRef = {
      ...params,
      refName: params.refName || defaultBranchName,
    };
    switch (value) {
      case "commit-graph": {
        const { href, as } = commitGraph(paramsWithRef);
        router.push(href, as).catch(console.error);
        break;
      }
      default:
        break;
    }
  });
}

Conclusion

Customizing dynamic menu items in Electron is a simple yet powerful way to enhance your app. If you have any requests for more Dolt Workbench menu items or general questions about Electron, feel free to create an issue on GitHub or reach out to us on Discord.

SHARE

JOIN THE DATA EVOLUTION

Get started with Dolt

Or join our mailing list to get product updates.