From f319384fdc6c9c95d0d8d98cc0efb295417c309d Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Fri, 15 Aug 2025 18:49:09 +1200 Subject: [PATCH] Switch to native Electron context menu Replaces the custom HTML context menu with a native Electron menu for webview and window areas. Adds context menu handling in main and preload scripts, relays commands to renderer, and updates renderer logic to support new menu actions. Improves integration and user experience by leveraging platform-native menus. --- main.js | 78 ++++++++++++++++++++++++++++++++++++++++++- preload.js | 12 +++++++ renderer/script.js | 82 ++++++++++++++++------------------------------ site-history.json | 5 +++ 4 files changed, 123 insertions(+), 54 deletions(-) diff --git a/main.js b/main.js index 1e94df2..2958f6c 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, ipcMain, session, screen, shell, dialog } = require('electron'); +const { app, BrowserWindow, ipcMain, session, screen, shell, dialog, Menu, clipboard } = require('electron'); const { pathToFileURL } = require('url'); const fs = require('fs'); const path = require('path'); @@ -612,3 +612,79 @@ ipcMain.handle('show-open-file-dialog', async () => { return null; } }); + +// Helper to build and show a native context menu for a given webContents + params +function buildAndShowContextMenu(sender, params = {}) { + try { + const embedder = sender.hostWebContents || sender; + const template = []; + + template.push( + { label: 'Back', enabled: sender.canGoBack?.(), click: () => { try { sender.goBack(); } catch {} } }, + { label: 'Forward', enabled: sender.canGoForward?.(), click: () => { try { sender.goForward(); } catch {} } }, + { label: 'Reload', click: () => { try { sender.reload(); } catch {} } }, + { type: 'separator' } + ); + + // Link actions + const linkURL = params.linkURL && params.linkURL.startsWith('http') ? params.linkURL : undefined; + if (linkURL) { + template.push( + { label: 'Open Link in New Tab', click: () => embedder.send('context-menu-command', { cmd: 'open-link-new-tab', url: linkURL }) }, + { label: 'Open Link Externally', click: () => shell.openExternal(linkURL).catch(()=>{}) }, + { label: 'Copy Link Address', click: () => clipboard.writeText(linkURL) }, + { type: 'separator' } + ); + } + + // Image actions + const imageURL = (params.mediaType === 'image' && params.srcURL) ? params.srcURL : (params.imgURL || undefined); + if (imageURL) { + template.push( + { label: 'Open Image in New Tab', click: () => embedder.send('context-menu-command', { cmd: 'open-image-new-tab', url: imageURL }) }, + { label: 'Copy Image Address', click: () => clipboard.writeText(imageURL) }, + { type: 'separator' } + ); + } + + // Text / editable + if (params.isEditable) { + template.push( + { label: 'Undo', role: 'undo' }, + { label: 'Redo', role: 'redo' }, + { type: 'separator' }, + { label: 'Cut', role: 'cut' }, + { label: 'Copy', role: 'copy' }, + { label: 'Paste', role: 'paste' }, + { label: 'Select All', role: 'selectAll' }, + { type: 'separator' } + ); + } else if (params.selectionText) { + template.push( + { label: 'Copy', role: 'copy' }, + { label: 'Select All', role: 'selectAll' }, + { type: 'separator' } + ); + } + + template.push({ label: 'Inspect Element', click: () => { try { sender.inspectElement(params.x ?? params.clientX, params.y ?? params.clientY); } catch {} } }); + + const menu = Menu.buildFromTemplate(template); + const win = BrowserWindow.fromWebContents(embedder); + if (win) menu.popup({ window: win }); + } catch (err) { + console.error('Failed to build context menu:', err); + } +} + +// IPC trigger (legacy / renderer-requested) +ipcMain.handle('show-context-menu', (event, params = {}) => { + buildAndShowContextMenu(event.sender, params); +}); + +// Automatic native context menu for any webContents (windows + webviews) +app.on('web-contents-created', (event, contents) => { + contents.on('context-menu', (e, params) => { + buildAndShowContextMenu(contents, params); + }); +}); diff --git a/preload.js b/preload.js index bc93181..91d7b49 100644 --- a/preload.js +++ b/preload.js @@ -63,6 +63,13 @@ const electronAPI = { console.error('IPC openLocalFile error:', err); return null; } + }, + showContextMenu: (params) => { + try { + return ipcRenderer.invoke('show-context-menu', params); + } catch (err) { + console.error('IPC showContextMenu error:', err); + } } }; @@ -105,4 +112,9 @@ contextBridge.exposeInMainWorld('bookmarksAPI', bookmarksAPI); // Minimal about API for settings page contextBridge.exposeInMainWorld('aboutAPI', { getInfo: () => ipcRenderer.invoke('get-about-info') +}); + +// Relay context-menu commands from main to active renderer context (open new tabs etc.) +ipcRenderer.on('context-menu-command', (event, payload) => { + window.dispatchEvent(new CustomEvent('nebula-context-command', { detail: payload })); }); \ No newline at end of file diff --git a/renderer/script.js b/renderer/script.js index 7301a93..31e6f9b 100644 --- a/renderer/script.js +++ b/renderer/script.js @@ -36,9 +36,7 @@ const urlBox = document.getElementById('url'); const tabBarEl = document.getElementById('tab-bar'); const webviewsEl = document.getElementById('webviews'); const menuPopup = document.getElementById('menu-popup'); -const contextMenu = document.getElementById('context-menu'); -const menuItems = contextMenu ? contextMenu.querySelectorAll('li') : []; -let lastContextPos = { x: 0, y: 0 }; +// (Removed old custom HTML context menu in favor of native Electron menu) // Select all text on focus and prevent mouseup from deselecting urlBox.addEventListener('focus', () => { @@ -997,55 +995,33 @@ try { console.warn('fs or remote modules unavailable in renderer:', err); } -// 4) unify context-menu wiring -function showContextMenu(x,y) { - if (!contextMenu) return; - lastContextPos = { x, y }; - contextMenu.style.top = `${y}px`; - contextMenu.style.left = `${x}px`; - contextMenu.classList.add('visible'); -} -document.addEventListener('contextmenu', e => { - if (e.target.tagName==='WEBVIEW' || e.composedPath().some(el=>el.id==='webviews')) { - e.preventDefault(); - showContextMenu(e.clientX, e.clientY); +// Native context menu: delegate to main via preload API +document.addEventListener('contextmenu', (e) => { + // Determine if inside a webview or general renderer area + const inWebviewArea = e.target.tagName === 'WEBVIEW' || e.composedPath().some(el => el.id === 'webviews'); + if (!inWebviewArea) return; // Let default OS menu appear in text inputs etc. if desired + e.preventDefault(); + + // Try to extract link/image/selection info (limited for , better done inside page but sandboxed) + const selection = window.getSelection()?.toString() || ''; + window.electronAPI?.showContextMenu({ + clientX: e.clientX, + clientY: e.clientY, + selectionText: selection, + isEditable: false + }); +}); + +// Handle commands from main process triggered by context menu +window.addEventListener('nebula-context-command', (e) => { + const { cmd, url } = e.detail || {}; + if (!cmd) return; + switch (cmd) { + case 'open-link-new-tab': + if (url) createTab(url); + break; + case 'open-image-new-tab': + if (url) createTab(url); + break; } }); -document.addEventListener('click', ()=> contextMenu && contextMenu.classList.remove('visible')); -if (remote && fs) { - menuItems.forEach(item => item.addEventListener('click', async evt => { - const action = item.dataset.action; - const win = remote.getCurrentWindow(); - - switch (action) { - case 'save-page': { - const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'page.html' }); - if (!canceled && filePath) win.webContents.savePage(filePath, 'HTMLComplete'); - break; - } - case 'select-all': - document.execCommand('selectAll'); - break; - case 'screenshot': { - const image = await win.webContents.capturePage(); - const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'screenshot.png' }); - if (!canceled && filePath) fs.writeFileSync(filePath, image.toPNG()); - break; - } - case 'view-source': { - const html = document.documentElement.outerHTML; - const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'source.html' }); - if (!canceled && filePath) fs.writeFileSync(filePath, html); - break; - } - case 'inspect-accessibility': - win.webContents.inspectAccessibilityNode(lastContextPos.x, lastContextPos.y); - break; - case 'inspect-element': - win.webContents.inspectElement(lastContextPos.x, lastContextPos.y); - break; - } - - contextMenu.classList.remove('visible'); - })); -} diff --git a/site-history.json b/site-history.json index f5af1c9..4887cdb 100644 --- a/site-history.json +++ b/site-history.json @@ -1,4 +1,9 @@ [ + "https://www.google.com/search?sca_esv=8740a35e22b44344&q=google&source=lnms&fbs=AIIjpHzThbnmQ2WmOKxM311CRlKFRYIYkDZQGNzKWZOtgUvrU8IkX2D8ZljVyZBwLc67VO5Vh9BmSq-tJTelkfgGaeLOhsWoMcpPaMobUKT2lDeoy4baWd3FunAQvdgUkx-O2UqTNd3rElthg_q_RDuOd63_-9VEOzcZa8DOthTdfufpgCAS8atIRQu6ndbQbff19E3EbkrdgATyQCGbkaHPxI6YLnT-EcRkSXiWTOApoudKAVtFgl4&sa=X&ved=2ahUKEwiP8MW4nIyPAxXwr1YBHfDEE0wQ0pQJegQICRAB&biw=2544&bih=1251&dpr=1.5", + "https://www.google.com/search?sca_esv=8740a35e22b44344&udm=2&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZ1Y6MJ25_tmWITc7uy4KIeuYzzFkfneXafNx6OMdA4MQRJc_t_TQjwHYrzlkIauOKj9nSuujpEIbB1x32lFLEvBmmX-p1UI3WlSFH86-EF1CpFR0tZjCgi5bM20K3xHOK3droXh0yMXraJ5han3x4rkl9Co5S6JKPNx1fHkXHoy-qehbRF1XGgIa6fKwyF5LNOJ-3xQ&q=google&sa=X&ved=2ahUKEwij2L2QnIyPAxVjsVYBHf8oASsQtKgLegQIHhAB&cshid=1755240501153677&biw=2544&bih=1251&dpr=1.5", + "https://www.google.com/search?q=google#cobssid=s", + "https://www.google.com/", + "https://www.google.com/search?q=google", "https://www.whatismybrowser.com/", "https://www.google.com/search?q=what%20is%20my%20browser" ] \ No newline at end of file