feat(pdf-editor): the dock click is the "open a file" gesture, so answer it with one
Opening a PDF cost four clicks across two screens: the dock icon, a dropdown with a single entry in it, an editor with an empty drop zone, and finally the Open button that produced a file browser. Three of those were the program asking the user to confirm what they had already said by clicking a PDF icon. The dock item now raises the file browser itself. The dialog is native and 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 none to spend. The chosen file goes through the same scratch handoff a right-clicked link already used, so the editor opens with the document in it rather than with an invitation to find one. Cancelling still lands in the empty editor, which is where drag-and-drop works and is probably still where someone who changed their mind about the file wants to be. Second click removed in the chrome: a toolbar menu holding exactly one item is not a menu, it is a button wearing a dropdown, so it now dispatches directly. Falls through to the popup if the renderer has no addonMenuSelect. Not yet exercised end to end — TheseusNavigator/node_modules is empty while another session reinstalls it, so the CDP suites cannot boot Electron. Both files parse; run scratchpad/verify-pdf-editor/drive.mjs once the tree is back.
This commit is contained in:
parent
bf423ae579
commit
70ee7fc39b
2 changed files with 55 additions and 6 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue