diff --git a/bundled-addons/pdf-editor/index.js b/bundled-addons/pdf-editor/index.js index e7b36ca9..8902f0df 100644 --- a/bundled-addons/pdf-editor/index.js +++ b/bundled-addons/pdf-editor/index.js @@ -95,16 +95,57 @@ module.exports = { } // ---- launch paths ------------------------------------------------- - // Dock → "Open a PDF…": a bare editor tab. The editor's own drop zone - // and File → Open take it from there, so no bytes cross IPC at all. - api.onMessage("menu-select", (payload) => { + // Dock → the file picker, straight away. + // + // This used to open an empty editor whose own drop zone then asked for a + // file, which meant three clicks and two screens before the user reached + // their own documents: dock, menu item, Open, and only then the file + // browser. Asking here collapses it — the dock click IS the "open a file" + // gesture, so answer it with a file browser. + // + // The dialog is native, raised from the add-on's Node side, because a + // file:// page cannot open one without a user gesture of its own and a + // freshly-opened tab has no gesture to spend. + // + // Cancelling still lands you in the editor, empty. Someone who changed + // their mind about which file probably still wants the editor, and it is + // where drag-and-drop works. + api.onMessage("menu-select", async (payload) => { const item = String(payload && payload.id || ""); if (item !== "open") throw new Error(`unknown menu item: ${item}`); - api.openTab("editor.html"); - api.log("opened an empty editor tab"); - return { ok: true }; + const entry = await pickPdf(); + if (!entry) { api.openTab("editor.html"); api.log("picker cancelled — opened an empty editor"); return { ok: true, cancelled: true }; } + api.openTab("editor.html", { query: { doc: entry.id } }); + api.log(`picked ${entry.name} (${entry.size} bytes) → ${entry.id}`); + return { ok: true, id: entry.id }; }); + /** + * Native open dialog, then the bytes in scratch. Returns null when the + * user cancels or picks something that is not a PDF. + */ + async function pickPdf() { + const { dialog, BrowserWindow } = api.require("electron"); + // Parent it to Theseus so the dialog is attached to the window rather + // than floating loose behind it. + const parent = BrowserWindow.getFocusedWindow() || BrowserWindow.getAllWindows()[0] || null; + const opts = { + title: "Open a PDF", + properties: ["openFile"], + filters: [{ name: "PDF documents", extensions: ["pdf"] }, { name: "All files", extensions: ["*"] }], + }; + const res = parent ? await dialog.showOpenDialog(parent, opts) : await dialog.showOpenDialog(opts); + const file = !res.canceled && res.filePaths && res.filePaths[0]; + if (!file) return null; + const stat = await fs.promises.stat(file); + if (stat.size > MAX_BYTES) throw new Error(`${path.basename(file)} is ${(stat.size / 1048576).toFixed(0)} MB — over the ${MAX_BYTES / 1048576} MB cap`); + const buf = await fs.promises.readFile(file); + if (buf.subarray(0, 5).toString("latin1") !== "%PDF-") { + throw new Error(`${path.basename(file)} does not start with a PDF header`); + } + return writeScratch(buf, safeName(path.basename(file))); + } + // Right-click a link → fetch it here, hand the editor an id. api.onMessage("context-menu", async (payload) => { if (String(payload && payload.itemId || "") !== "open-link") return; diff --git a/chrome.html b/chrome.html index 84b94b2c..ad07070b 100644 --- a/chrome.html +++ b/chrome.html @@ -793,6 +793,14 @@ closeToolbarMenu(); const menu = lastMenus.find((m) => m.addonId === addonId); if (!menu) return; + // A menu with one entry is not a menu, it is a button wearing a dropdown. + // Popping it costs the user a second click and a second target to aim at + // for a choice that was never a choice, so dispatch the item directly. + if (menu.items && menu.items.length === 1 && T.addonMenuSelect) { + T.addonMenuSelect(addonId, menu.items[0].id) + .catch((err) => console.warn("addon menu item failed:", err?.message || err)); + return; + } openMenuAddonId = addonId; // Tint the anchor while the menu is showing; main clears it when the // native menu closes via the "toolbar-menu-closed" event.