Electron Adventures: Episode 92: Dock Drag and Drop

I wanted to be able to drag and drop CSV files from Finder onto the dock icon for the app to open them.

The first issue this runs into is that such integrations only work if we tell OSX about them through Info.plist, and that's only possible for packaged apps, so I need to go through all the steps from episode 80 for packaging Electron Svelte app.

Info.plist

First, let's create Info.plist. I'm not really sure how these work, but I found some examples online, and replaced whatever was in them with CSV and text/csv to tell the system these are the file types we support.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleDocumentTypes</key>
  <array>
    <dict>
      <key>CFBundleTypeExtensions</key>
      <array>
        <string>csv</string>
      </array>
      <key>CFBundleTypeIconFile</key>
      <string>electron.icns</string>
      <key>CFBundleTypeMIMETypes</key>
      <array>
        <string>text/csv</string>
      </array>
      <key>CFBundleTypeName</key>
      <string>CSV file</string>
      <key>CFBundleTypeOSTypes</key>
      <array>
        <string>CSV</string>
      </array>
      <key>CFBundleTypeRole</key>
      <string>Editor</string>
    </dict>
  </array>
</dict>
</plist>

package.json

{
  "name": "episode-92-dock-drag-and-drop",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv public --no-clear",
    "electron": "electron .",
    "forge-start": "electron-forge start",
    "package": "electron-forge package",
    "make": "electron-forge make"
  },
  "devDependencies": {
    "@electron-forge/cli": "^6.0.0-beta.61",
    "@electron-forge/maker-deb": "^6.0.0-beta.61",
    "@electron-forge/maker-rpm": "^6.0.0-beta.61",
    "@electron-forge/maker-squirrel": "^6.0.0-beta.61",
    "@electron-forge/maker-zip": "^6.0.0-beta.61",
    "@rollup/plugin-commonjs": "^17.0.0",
    "@rollup/plugin-node-resolve": "^11.0.0",
    "electron": "^13.1.8",
    "rollup": "^2.3.4",
    "rollup-plugin-css-only": "^3.1.0",
    "rollup-plugin-livereload": "^2.0.0",
    "rollup-plugin-svelte": "^7.0.0",
    "rollup-plugin-terser": "^7.0.0",
    "svelte": "^3.0.0"
  },
  "dependencies": {
    "d3-dsv": "^3.0.1",
    "electron-log": "^4.4.1",
    "electron-settings": "^4.0.2",
    "electron-squirrel-startup": "^1.0.0",
    "sirv-cli": "^1.0.0"
  },
  "config": {
    "forge": {
      "packagerConfig": {
        "extendInfo": "Info.plist"
      },
      "makers": [
        {
          "name": "@electron-forge/maker-squirrel",
          "config": {
            "name": "episode_92_dock_drag_and_drop"
          }
        },
        {
          "name": "@electron-forge/maker-zip",
          "platforms": [
            "darwin"
          ]
        },
        {
          "name": "@electron-forge/maker-deb",
          "config": {}
        },
        {
          "name": "@electron-forge/maker-rpm",
          "config": {}
        }
      ]
    }
  }
}

For package.json we need to follow the same steps as back in episode 80, but then we also need to add the config.forge.packagerConfig.extendInfo property to tell the packager to use the Info.plist file.

index.js

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

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

function createWindow(path) {
  log.info("Creating window for", 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()
    log.info("Saving window position", path, windowState)
    settings.setSync(key, windowState)
  }

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

  if (app.isPackaged) {
    win.loadFile(`${__dirname}/public/index.html`, {query: {path}})
  } else {
    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)
  }
}

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()
      }
    })
  }
}

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

app.on("ready", startApp)

app.on("open-file", (event, path) => {
  log.info("Opening file through drag and drop to Dock", path)
  createWindow(path)
})

This file needed two changes. First, the change we actually want - a handler for open-file event. It gets passed path as the second argument, and we just use that to open a new window.

The other change is that we want to pass query string to the app, regardless of if it's in packaged or development mode. Honestly I find it embarassing that Electron doesn't just have a builtin way to do this without switching logic, but this works:

  if (app.isPackaged) {
    win.loadFile(`${__dirname}/public/index.html`, {query: {path}})
  } else {
    let qs = new URLSearchParams({ path }).toString()
    win.loadURL(`http://localhost:5000/?${qs}`)
  }

Application logs

All the events are logged to ~/Library/Logs/episode-92-dock-drag-and-drop/main.log, so you can see such wonderful messages as:

[2021-11-09 13:55:05.520] [info]  Opening file through drag and drop to Dock /Users/taw/electron-adventures/episode-92-dock-drag-and-drop/samples/07-lover.csv
[2021-11-09 13:55:05.521] [info]  Creating window for /Users/taw/electron-adventures/episode-92-dock-drag-and-drop/samples/07-lover.csv

Results

As usual, all the code for the episode is here.

For the next episode, we'll see if we can get Opal Ruby working with Electron.