From bec47845bec9e2325a7fec88430fcd1d9573b4a1 Mon Sep 17 00:00:00 2001 From: Aditya Toshniwal Date: Mon, 21 Jul 2025 15:07:17 +0530 Subject: [PATCH] Add support for showing pgAdmin shortcuts on Electron menus. #1923 --- runtime/src/js/downloader.js | 1 + runtime/src/js/menu.js | 39 +++++++++++++++++-- runtime/src/js/pgadmin.js | 2 +- runtime/src/js/pgadmin_preload.js | 2 +- .../browser/static/js/MainMenuFactory.js | 11 ++++-- web/pgadmin/browser/static/js/browser.js | 1 + web/pgadmin/static/bundle/app.js | 4 ++ web/pgadmin/static/js/components/Menu.jsx | 1 - web/pgadmin/static/js/helpers/Menu.js | 1 + 9 files changed, 52 insertions(+), 10 deletions(-) diff --git a/runtime/src/js/downloader.js b/runtime/src/js/downloader.js index 71d95b789..3ef59ec6d 100644 --- a/runtime/src/js/downloader.js +++ b/runtime/src/js/downloader.js @@ -10,6 +10,7 @@ import { app, ipcMain, dialog, BrowserWindow, shell } from 'electron'; import fs from 'fs'; import path from 'path'; +import Buffer from 'buffer'; import { setBadge, clearBadge, clearProgress, setProgress } from './progress.js'; import { writeServerLog } from './misc.js'; diff --git a/runtime/src/js/menu.js b/runtime/src/js/menu.js index 8a3b4c8d8..df9f89876 100644 --- a/runtime/src/js/menu.js +++ b/runtime/src/js/menu.js @@ -7,12 +7,28 @@ // ////////////////////////////////////////////////////////////// -import { app, Menu, ipcMain, BrowserWindow } from 'electron'; +import { app, Menu, ipcMain, BrowserWindow, globalShortcut } from 'electron'; const isMac = process.platform == 'darwin'; const isLinux = process.platform == 'linux'; let mainMenu; +// Use to convert shortcut to accelerator for electron. +function convertShortcutToAccelerator({ control, meta, shift, alt, key } = {}) { + // Store active modifier keys into an array. + const mods = [ + control && 'Ctrl', + meta && 'Cmd', + shift && 'Shift', + alt && 'Alt', + ].filter(Boolean); // Remove any falsy values + // Get the actual key character and convert to uppercase. + const k = key?.char?.toUpperCase(); + if (!k) return; + // Combine modifiers and key into a single string. + return [...mods, k].join('+'); +} + function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) { const template = []; @@ -24,13 +40,27 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) { const smName = `${menuItem.name}_${subMenuItem.name}`; return { ...subMenuItem, - click: ()=>{ + accelerator: convertShortcutToAccelerator(subMenuItem.shortcut), + click: (_menuItem, _browserWindow, event)=>{ + if(event?.triggeredByAccelerator) { + // We will ignore the click event if it is triggered by an accelerator. + // We use accelerator to only show the shortcut title in the menu. + // The actual shortcut is already handled by pgAdmin. + return; + } pgAdminMainScreen.webContents.send('menu-click', smName); }, submenu: subMenuItem.submenu?.map((deeperSubMenuItem)=>{ return { ...deeperSubMenuItem, - click: ()=>{ + accelerator: convertShortcutToAccelerator(deeperSubMenuItem.shortcut), + click: (_menuItem, _browserWindow, event)=>{ + if(event?.triggeredByAccelerator) { + // We will ignore the click event if it is triggered by an accelerator. + // We use accelerator to only show the shortcut title in the menu. + // The actual shortcut is already handled by pgAdmin. + return; + } pgAdminMainScreen.webContents.send('menu-click', `${smName}_${deeperSubMenuItem.name}`); }, }; @@ -109,6 +139,9 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) { export function setupMenu(pgAdminMainScreen, callbacks={}) { ipcMain.on('setMenus', (event, menus)=>{ mainMenu = buildMenu(menus, pgAdminMainScreen, callbacks); + // this is important because the shortcuts are registered multiple times + // when the menu is set multiple times using accelerators. + globalShortcut.unregisterAll(); if(isMac) { Menu.setApplicationMenu(mainMenu); } else { diff --git a/runtime/src/js/pgadmin.js b/runtime/src/js/pgadmin.js index 2b2ef8e72..c1b855d61 100644 --- a/runtime/src/js/pgadmin.js +++ b/runtime/src/js/pgadmin.js @@ -411,7 +411,7 @@ ipcMain.on('log', (_e, text) => ()=>{ }); ipcMain.on('focus', (e) => { app.focus({steal: true}); - const callerWindow = BrowserWindow.fromWebContents(e.sender) + const callerWindow = BrowserWindow.fromWebContents(e.sender); if (callerWindow) { if (callerWindow.isMinimized()) callerWindow.restore(); callerWindow.focus(); diff --git a/runtime/src/js/pgadmin_preload.js b/runtime/src/js/pgadmin_preload.js index e28a215f7..2e3575dc9 100644 --- a/runtime/src/js/pgadmin_preload.js +++ b/runtime/src/js/pgadmin_preload.js @@ -31,5 +31,5 @@ contextBridge.exposeInMainWorld('electronUI', { downloadStreamSaveTotal: (...args) => ipcRenderer.send('download-stream-save-total', ...args), downloadStreamSaveEnd: (...args) => ipcRenderer.send('download-stream-save-end', ...args), downloadBase64UrlData: (...args) => ipcRenderer.invoke('download-base64-url-data', ...args), - downloadTextData: (...args) => ipcRenderer.invoke('download-text-data', ...args) + downloadTextData: (...args) => ipcRenderer.invoke('download-text-data', ...args), }); \ No newline at end of file diff --git a/web/pgadmin/browser/static/js/MainMenuFactory.js b/web/pgadmin/browser/static/js/MainMenuFactory.js index 39d6ff0ba..d01768481 100644 --- a/web/pgadmin/browser/static/js/MainMenuFactory.js +++ b/web/pgadmin/browser/static/js/MainMenuFactory.js @@ -46,6 +46,12 @@ export default class MainMenuFactory { }); } + static listenToElectronMenuClick() { + window.electronUI?.onMenuClick((menuName)=>{ + MainMenuFactory.electronCallbacks[menuName]?.(); + }); + } + static createMainMenus() { pgAdmin.Browser.MainMenus = []; MAIN_MENUS.forEach((_menu) => { @@ -61,10 +67,6 @@ export default class MainMenuFactory { // enable disable will take care of dynamic menus. MainMenuFactory.enableDisableMenus(); - window.electronUI?.onMenuClick((menuName)=>{ - MainMenuFactory.electronCallbacks[menuName]?.(); - }); - window.electronUI?.setMenus(MainMenuFactory.toElectron()); } @@ -117,6 +119,7 @@ export default class MainMenuFactory { }; let allMenus = pgAdmin.Browser?.all_menus_cache || {}; Object.values(allMenus).forEach(updateShortcuts); + MainMenuFactory.createMainMenus(); }; // Assign and Update menu shortcuts using preference. diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 3e734eb1c..3510524a1 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -375,6 +375,7 @@ define('pgadmin.browser', [ _m.callback = () => { showQuickSearch(); }; + _m.shortcut_preference =['browser', 'open_quick_search']; } return { diff --git a/web/pgadmin/static/bundle/app.js b/web/pgadmin/static/bundle/app.js index d53d42fb2..54956e2df 100644 --- a/web/pgadmin/static/bundle/app.js +++ b/web/pgadmin/static/bundle/app.js @@ -51,6 +51,10 @@ define('app', [ // Create menus after all modules are initialized. MainMenuFactory.createMainMenus(); + // Listen to menu click events and callback pgAdmin js code. + // This will be internally ignored if not running in electron. + MainMenuFactory.listenToElectronMenuClick(); + const root = ReactDOM.createRoot(document.querySelector('#root')); root.render( diff --git a/web/pgadmin/static/js/components/Menu.jsx b/web/pgadmin/static/js/components/Menu.jsx index b92da4965..aef6d36a1 100644 --- a/web/pgadmin/static/js/components/Menu.jsx +++ b/web/pgadmin/static/js/components/Menu.jsx @@ -85,7 +85,6 @@ export const PgMenuItem = (({hasCheck=false, checked=false, accesskey, shortcut,