Add support for showing pgAdmin shortcuts on Electron menus. #1923

pull/8978/head
Aditya Toshniwal 2025-07-21 15:07:17 +05:30
parent 6dc5807192
commit bec47845be
9 changed files with 52 additions and 10 deletions

View File

@ -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';

View File

@ -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 {

View File

@ -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();

View File

@ -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),
});

View File

@ -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.

View File

@ -375,6 +375,7 @@ define('pgadmin.browser', [
_m.callback = () => {
showQuickSearch();
};
_m.shortcut_preference =['browser', 'open_quick_search'];
}
return {

View File

@ -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(
<Theme>

View File

@ -85,7 +85,6 @@ export const PgMenuItem = (({hasCheck=false, checked=false, accesskey, shortcut,
<Box
sx={{
marginLeft: 'auto',
fontSize: '0.8em',
paddingLeft: '12px',
display: 'flex',
gap: '1px',

View File

@ -155,6 +155,7 @@ export class MenuItem {
priority: this.priority,
type: [true, false].includes(this.checked) ? 'checkbox' : this.type,
checked: this.checked,
shortcut: this.shortcut,
};
}