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.