diff --git a/chrome.html b/chrome.html index e3d36bcc..b0cc4796 100644 --- a/chrome.html +++ b/chrome.html @@ -559,6 +559,27 @@ $("search").addEventListener("keydown", (e) => { if (e.key === "Enter") { const q = $("search").value.trim(); if (q) T.search(q); } }); $("back").onclick = () => T.back(); $("fwd").onclick = () => T.forward(); + // Hold Back/Forward (or right-click) for the history list, like Chrome. + // A hold longer than 450 ms opens the native menu and swallows the click + // that would otherwise fire on release. + for (const [id, dir] of [["back", "back"], ["fwd", "forward"]]) { + const btn = $(id); + let timer = null, held = false; + const open = () => { + held = true; + const r = btn.getBoundingClientRect(); + T.navHistoryMenu && T.navHistoryMenu(dir, { x: r.left, y: r.bottom }); + }; + btn.addEventListener("mousedown", (e) => { + if (e.button !== 0 || btn.disabled) return; + held = false; clearTimeout(timer); timer = setTimeout(open, 450); + }); + const cancel = () => { clearTimeout(timer); timer = null; }; + btn.addEventListener("mouseup", cancel); + btn.addEventListener("mouseleave", cancel); + btn.addEventListener("click", (e) => { if (held) { held = false; e.stopImmediatePropagation(); e.preventDefault(); } }, true); + btn.addEventListener("contextmenu", (e) => { e.preventDefault(); if (!btn.disabled) open(); held = false; }); + } // Shift-click on the reload button = hard reload (bypass cache), same // convention as Chrome/Firefox. Keyboard alternative: Ctrl+Shift+R. $("reload").onclick = (e) => T.reload(e.shiftKey); diff --git a/main.js b/main.js index 3c8b677c..af310bca 100644 --- a/main.js +++ b/main.js @@ -4189,6 +4189,32 @@ ipcMain.handle("registry-menu-popup", (e, rect) => { const y = Math.max(0, Math.round(chromeBounds.y + (rect?.y || 0))); popup.popup({ window: win, x, y }); }); +// Back/Forward history menu (press-and-hold or right-click on the buttons). +// Lists up to 15 entries in the requested direction, nearest first, from the +// active tab's navigation history; picking one jumps straight to it. +ipcMain.handle("nav-history-menu", (e, dir, rect) => { + if (chrome && e.sender !== chrome.webContents) throw new Error("nav-history-menu: untrusted sender"); + const t = activeTab(); const wc = t?.view?.webContents; + if (!wc) return false; + const nh = wc.navigationHistory; + const entries = nh.getAllEntries(); + const cur = nh.getActiveIndex(); + const picks = []; + if (dir === "forward") for (let i = cur + 1; i < entries.length && picks.length < 15; i++) picks.push(i); + else for (let i = cur - 1; i >= 0 && picks.length < 15; i--) picks.push(i); + if (!picks.length) return false; + const label = (en) => { + const title = String(en.title || "").trim(); + let host = ""; try { host = new URL(en.url).host; } catch {} + const text = title || en.url || ""; + return (text.length > 60 ? text.slice(0, 57) + "…" : text) + (host && title ? ` — ${host}` : ""); + }; + const template = picks.map((i) => ({ label: label(entries[i]), click: () => { try { nh.goToIndex(i); } catch {} } })); + const popup = Menu.buildFromTemplate(template); + const chromeBounds = chrome ? chrome.getBounds() : { x: 0, y: 0 }; + popup.popup({ window: win, x: Math.max(0, Math.round(chromeBounds.x + (rect?.x || 0))), y: Math.max(0, Math.round(chromeBounds.y + (rect?.y || 0))) }); + return true; +}); ipcMain.handle("collision-state", () => ({ policy: settings.collisionPolicy, byName: collisions.byName, diff --git a/preload.js b/preload.js index 8da50ec3..92e4840c 100644 --- a/preload.js +++ b/preload.js @@ -21,6 +21,9 @@ contextBridge.exposeInMainWorld("theseus", { goHome: () => ipcRenderer.invoke("go-home"), back: () => ipcRenderer.invoke("go-back"), forward: () => ipcRenderer.invoke("go-forward"), + // Press-and-hold / right-click on Back or Forward: native menu of the + // tab's history entries in that direction; picking one jumps to it. + navHistoryMenu: (dir, rect) => ipcRenderer.invoke("nav-history-menu", dir, rect), reload: (hard) => ipcRenderer.invoke("reload", !!hard), stop: () => ipcRenderer.invoke("stop"), toggleTor: () => ipcRenderer.invoke("toggle-tor"),