Added OS context menu for linux

This commit is contained in:
2026-02-25 11:57:31 +13:00
parent 8b87a07d1b
commit adefa1706e
4 changed files with 164 additions and 32 deletions
+93 -15
View File
@@ -293,7 +293,6 @@ function getDesktopViewState(win) {
function createMenuPopupWindow(parentWin) {
const menuWin = new BrowserWindow({
parent: parentWin,
modal: false,
frame: false,
transparent: true,
@@ -301,7 +300,8 @@ function createMenuPopupWindow(parentWin) {
show: false,
alwaysOnTop: true,
skipTaskbar: true,
focusable: true,
focusable: false,
fullscreenable: false,
hasShadow: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
@@ -314,6 +314,7 @@ function createMenuPopupWindow(parentWin) {
menuWin.setMenu(null);
try { menuWin.setAlwaysOnTop(true, 'pop-up-menu'); } catch {}
try { menuWin.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); } catch {}
const hideMenu = () => {
if (!menuWin.isDestroyed()) menuWin.hide();
@@ -334,20 +335,45 @@ function createMenuPopupWindow(parentWin) {
}
function positionMenuPopup(parentWin, menuWin, anchorRect) {
if (!parentWin || !menuWin || !anchorRect) return;
const contentBounds = parentWin.getContentBounds();
const display = screen.getDisplayMatching(contentBounds);
const workArea = display?.workArea || contentBounds;
if (!parentWin || !menuWin) return;
const width = MENU_POPUP_SIZE.width;
const height = MENU_POPUP_SIZE.height;
let x = Math.round(contentBounds.x + anchorRect.x + anchorRect.width - width);
let y = Math.round(contentBounds.y + anchorRect.y + anchorRect.height + 6);
if (x < workArea.x) x = workArea.x;
if (y < workArea.y) y = workArea.y;
if (x + width > workArea.x + workArea.width) x = workArea.x + workArea.width - width;
if (y + height > workArea.y + workArea.height) y = workArea.y + workArea.height - height;
const parentBounds = parentWin.getBounds();
const contentBounds = parentWin.getContentBounds();
const rect = anchorRect && Number.isFinite(anchorRect.x) && Number.isFinite(anchorRect.y)
? anchorRect
: null;
let x;
let y;
if (rect) {
x = Math.round(contentBounds.x + rect.x + rect.width - width);
y = Math.round(contentBounds.y + rect.y + rect.height + 6);
} else {
x = Math.round(parentBounds.x + parentBounds.width - width - 12);
y = Math.round(parentBounds.y + 52);
}
const display = screen.getDisplayNearestPoint({ x, y });
const workArea = display?.workArea || { x: 0, y: 0, width: 1920, height: 1080 };
if (x < workArea.x) x = workArea.x + 6;
if (x + width > workArea.x + workArea.width) x = workArea.x + workArea.width - width - 6;
if (y < workArea.y) y = workArea.y + 6;
if (y + height > workArea.y + workArea.height) {
const aboveY = rect
? Math.round(contentBounds.y + rect.y - height - 6)
: Math.round(parentBounds.y + parentBounds.height - height - 12);
if (aboveY >= workArea.y) {
y = aboveY;
} else {
y = workArea.y + workArea.height - height - 6;
}
}
menuWin.setBounds({ x, y, width, height }, false);
}
@@ -2117,7 +2143,35 @@ ipcMain.handle('browserview-set-bounds', (event, bounds) => {
}
});
// Overlay menu (to sit above BrowserView)
// Native popup menu for the burger button — renders above BrowserView on all platforms
ipcMain.handle('show-app-menu', (event, payload = {}) => {
try {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) return;
const zoomFactor = win.webContents.getZoomFactor?.() || 1;
const zoomLabel = `${Math.round(zoomFactor * 100)}%`;
const template = [
{ label: 'Settings', click: () => win.webContents.send('menu-command', { cmd: 'open-settings' }) },
{ type: 'separator' },
{ label: '\u{1F3AE} Big Picture Mode', click: () => win.webContents.send('menu-command', { cmd: 'big-picture' }) },
{ type: 'separator' },
{ label: 'Toggle Developer Tools', click: () => win.webContents.send('menu-command', { cmd: 'toggle-devtools' }) },
{ type: 'separator' },
{ label: `Zoom: ${zoomLabel}`, enabled: false },
{ label: 'Zoom In', accelerator: 'CmdOrCtrl+=', click: () => win.webContents.send('menu-command', { cmd: 'zoom-in' }) },
{ label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', click: () => win.webContents.send('menu-command', { cmd: 'zoom-out' }) },
{ type: 'separator' },
{ label: 'Hard Reload (Ignore Cache)', click: () => win.webContents.send('menu-command', { cmd: 'hard-reload' }) },
{ label: 'Reload Fresh (Add Cache-Buster)', click: () => win.webContents.send('menu-command', { cmd: 'fresh-reload' }) },
];
const menu = Menu.buildFromTemplate(template);
menu.popup({ window: win, x: payload.x, y: payload.y });
} catch (err) {
console.error('[show-app-menu] Error:', err);
}
});
// Overlay menu (to sit above BrowserView) — kept for backwards compat but no longer primary
ipcMain.on('menu-popup-toggle', (event, payload = {}) => {
try {
const parentWin = BrowserWindow.fromWebContents(event.sender);
@@ -2134,7 +2188,24 @@ ipcMain.on('menu-popup-toggle', (event, payload = {}) => {
return;
}
positionMenuPopup(parentWin, menuWin, payload.anchorRect);
const anchorRect = payload?.anchorRect;
const anchorScreenPoint = payload?.anchorScreenPoint;
if (anchorScreenPoint && Number.isFinite(anchorScreenPoint.x) && Number.isFinite(anchorScreenPoint.y)) {
const width = MENU_POPUP_SIZE.width;
const height = MENU_POPUP_SIZE.height;
let x = Math.round(anchorScreenPoint.x - width);
let y = Math.round(anchorScreenPoint.y + 6);
const display = screen.getDisplayNearestPoint({ x, y });
const workArea = display?.workArea || { x: 0, y: 0, width: 1920, height: 1080 };
if (x < workArea.x) x = workArea.x + 6;
if (x + width > workArea.x + workArea.width) x = workArea.x + workArea.width - width - 6;
if (y < workArea.y) y = workArea.y + 6;
if (y + height > workArea.y + workArea.height) y = workArea.y + workArea.height - height - 6;
menuWin.setBounds({ x, y, width, height }, false);
} else {
positionMenuPopup(parentWin, menuWin, anchorRect);
}
const initPayload = { theme: payload.theme || null };
const sendInit = () => {
@@ -2148,8 +2219,15 @@ ipcMain.on('menu-popup-toggle', (event, payload = {}) => {
}
} catch {}
try {
if (typeof menuWin.showInactive === 'function') {
menuWin.showInactive();
} else {
menuWin.show();
menuWin.focus();
}
} catch {
menuWin.show();
}
} catch {}
});
+4 -1
View File
@@ -58,7 +58,10 @@
"icon": "assets/images/Logos/Nebula-Favicon.ico"
},
"linux": {
"icon": "assets/images/Logos/Nebula-Favicon.png"
"icon": "assets/images/Logos/Nebula-Favicon.png",
"target": [
"AppImage"
]
}
}
}
+1 -1
View File
@@ -88,7 +88,7 @@
<div class="menu-wrapper">
<button id="menu-btn"></button>
<div id="menu-popup" class="hidden">
<button onclick="openSettings()">Settings</button>
<button id="open-settings-btn">Settings</button>
<!-- You can add more options here -->
<button id="bigpicture-btn" title="Launch Big Picture Mode (Controller/Steam Deck UI)">🎮 Big Picture Mode</button>
<button id="devtools-btn" title="Open / Close Developer Tools">Toggle Developer Tools</button>
+65 -14
View File
@@ -477,8 +477,7 @@ ipcRenderer.on('browserview-host-message', (payload) => {
});
// Commands from the overlay menu window
ipcRenderer.on('menu-command', (payload) => {
const cmd = payload?.cmd;
function runMenuCommand(cmd) {
if (!cmd) return;
switch (cmd) {
case 'open-settings':
@@ -508,6 +507,45 @@ ipcRenderer.on('menu-command', (payload) => {
default:
break;
}
}
const isLinux = document.body.classList.contains('platform-linux');
function hideMainMenuPopup() {
if (!isLinux) {
// Windows/macOS: hide the popup window
ipcRenderer.send('menu-popup-hide');
}
}
function showNativeAppMenu() {
if (!menuBtn) return;
const rect = menuBtn.getBoundingClientRect();
if (isLinux) {
// Linux: use native OS menu (renders above BrowserView reliably)
ipcRenderer.invoke('show-app-menu', {
x: Math.round(rect.right - 200),
y: Math.round(rect.bottom + 4)
}).catch(() => {});
} else {
// Windows/macOS: use the custom popup window
const theme = currentThemeColors ? { colors: currentThemeColors } : null;
ipcRenderer.send('menu-popup-toggle', {
anchorRect: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
anchorScreenPoint: {
x: Math.round(window.screenX + rect.right),
y: Math.round(window.screenY + rect.bottom)
},
theme
});
}
}
ipcRenderer.on('menu-command', (payload) => {
const cmd = payload?.cmd;
runMenuCommand(cmd);
hideMainMenuPopup();
});
// Auto-open on download start is disabled by design now.
@@ -1145,18 +1183,12 @@ let ringSvgEl = null;
// Open/close on button click; stop propagation so outside-click handler doesn't immediately close it
menuBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (!menuBtn) return;
const rect = menuBtn.getBoundingClientRect();
const theme = currentThemeColors ? { colors: currentThemeColors } : null;
ipcRenderer.send('menu-popup-toggle', {
anchorRect: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
theme
});
showNativeAppMenu();
});
// Close on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') ipcRenderer.send('menu-popup-hide');
if (e.key === 'Escape') hideMainMenuPopup();
if (e.key === 'Escape' && downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) {
hideDownloadsPopup();
}
@@ -1167,7 +1199,7 @@ ipcRenderer.on('browserview-event', (payload) => {
if (!payload || !payload.type) return;
const { tabId, type } = payload;
if (type === 'focus') {
ipcRenderer.send('menu-popup-hide');
hideMainMenuPopup();
if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) hideDownloadsPopup();
return;
}
@@ -1276,14 +1308,33 @@ window.addEventListener('DOMContentLoaded', () => {
bigPictureBtn.addEventListener('click', async () => {
try {
await window.bigPictureAPI.launch();
// Close the overlay menu
ipcRenderer.send('menu-popup-hide');
hideMainMenuPopup();
} catch (e) {
console.error('Failed to launch Big Picture Mode:', e);
}
});
}
if (menuPopup) {
menuPopup.addEventListener('click', (e) => {
const button = e.target instanceof HTMLElement ? e.target.closest('button') : null;
if (!button) return;
const id = button.id;
if (!id) return;
if (id !== 'zoom-in-btn' && id !== 'zoom-out-btn') {
hideMainMenuPopup();
}
e.stopPropagation();
});
}
document.addEventListener('click', (e) => {
if (!menuPopup || menuPopup.classList.contains('hidden')) return;
if (menuWrapper && !menuWrapper.contains(e.target)) {
hideMainMenuPopup();
}
});
// Cache back/forward buttons for faster updates (no need to add listeners - already in HTML)
backBtnCached = document.querySelector('.nav-left button:nth-child(1)');
fwdBtnCached = document.querySelector('.nav-left button:nth-child(2)');
@@ -1462,7 +1513,7 @@ try {
function attachCloseMenuOnInteract(el) {
if (!el) return;
const closeIfOpen = () => {
ipcRenderer.send('menu-popup-hide');
hideMainMenuPopup();
if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) {
hideDownloadsPopup();
}