Electron Adventures: Episode 90: Dock Menu
Tags: electron, javascript, svelte, osx
Published: 2021-11-09 01:40:17

Electron apps by default are basically web pages with minimal wrapper, but it's possible to extend them with a lot of OS-specific functionality.

Let's add OSX Dock menu to our app.

I'll also finally stop quitting the app when the last window is closed. I know pretty much every Electron tutorial does this out of the box for OSX, but this is absolutely NOT the right behavior for every app, and you should not mindlessly copy and paste that code. Most apps have just one window, and if you close that window, that's because your intention is to close the app. Only multi-document app on OSX should really reasonably stay running after their last window is closed.

The only changes will be in `index.js`, the rest of the app stays identical to what we had before.

### Are we on OSX?

The check is very simple, and we could inline in everywhere, but let's create a variable for it:

```javascript
let isOSX = (process.platform === "darwin")
```

### Start the app

We want to call `startApp()` when the app is ready. Also when all windows are closed, if we're not on OSX, we still want to just quit the app - all this Dock menu logic will be OSX specific.

```javascript
app.on("window-all-closed", () => {
  if (!isOSX) {
    app.quit()
  }
})

app.on("ready", startApp)
```

### Setup Dock Menu and reactivation logic

Custom part of our Dock Menu is very simple and static, so we can just pass static data to `Menu.buildFromTemplate`:

```javascript
let dockMenu = Menu.buildFromTemplate([
  {
    label: "Open files",
    click() { openFiles() }
  }
])

async function startApp() {
  if (isOSX) {
    app.dock.setMenu(dockMenu)
  }
  await openFiles()
  if (isOSX) {
    app.on("activate", function() {
      if (BrowserWindow.getAllWindows().length === 0) {
        openFiles()
      }
    })
  }
}
```

`startApp()` has two pieces of OSX-specific logic. First, we create Dock menu only on OSX, as other systems don't have such concept.

Second, we setup some logic to popup `openFiles()` dialog if app is reactivated while it has no windows open. We only do this after initial `await openFiles()` finishes, so we don't popup multiple `openFiles` dialogs at once.

### Everything else:

And everything else is just as before:

```javascript
let { app, BrowserWindow, dialog, Menu } = require("electron")
let settings = require("electron-settings")

function createWindow(path) {
  let key = `windowState-${path}`
  let windowState = settings.getSync(key) || { width: 1024, height: 768 }

  let qs = new URLSearchParams({ path }).toString();
  let win = new BrowserWindow({
    ...windowState,
    webPreferences: {
      preload: `${__dirname}/preload.js`,
    },
  })

  function saveSettings() {
    windowState = win.getBounds()
    console.log("SAVING", path, windowState)
    settings.setSync(key, windowState)
  }

  win.on("resize", saveSettings)
  win.on("move", saveSettings)
  win.on("close", saveSettings)

  win.loadURL(`http://localhost:5000/?${qs}`)
}

async function openFiles() {
  let { canceled, filePaths } = await dialog.showOpenDialog({
    properties: ["openFile", "multiSelections", "showHiddenFiles"],
    filters: [
      { name: "CSV files", extensions: ["csv"] },
      { name: "All Files", extensions: ["*"] }
    ],
    message: "Select a CSV file to open",
    defaultPath: `${__dirname}/samples`,
  })
  if (canceled && !isOSX) {
    app.quit()
  }
  for (let path of filePaths) {
    createWindow(path)
  }
}
```

One thing to note is that it's possible to open the same document multiple times. This is generally a good thing, as user might want to view different portions of the same document in multiple windows. However as our saved windows sizes and positions are keyed by document path, it is currently not deterministic which size and position will be restored if user closes it all and tries to reopen it later. Even in such cases, it's still reasonable behavior.

### Results

Here's the Dock menu if a few windows are open:

![Episode 90 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rhunijs3tlkwa2o99spg.png)

In the next episode, we'll see if we can integrate native file drag and drop.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-90-dock-menu).