// Eigenes Menüfenster für das Tray-Symbol. // Das native Electron-Menü zeichnet unter Windows keine Hover-Markierung; dieses kleine Fenster // (renderer/tray-menu.html) übernimmt Aussehen und Markierung selbst. const { BrowserWindow, screen, nativeTheme, ipcMain } = require('electron'); const WIDTH = 286; // Fensterbreite inkl. 8 px Rand für den Schatten const DEFAULT_HEIGHT = 320; const MARGIN = 8; function clamp(value, min, max) { return Math.max(min, Math.min(value, Math.max(min, max))); } // Position des Menüs: am Mauszeiger, innerhalb des sichtbaren Bereichs. Liegt der Zeiger in der // Taskleiste (unter/über dem Arbeitsbereich), sitzt das Menü direkt daneben. function computePosition(point, workArea, width, height) { const x = clamp(point.x - Math.round(width / 2), workArea.x + MARGIN, workArea.x + workArea.width - width - MARGIN); let y; if (point.y >= workArea.y + workArea.height) y = workArea.y + workArea.height - height - MARGIN; else if (point.y <= workArea.y) y = workArea.y + MARGIN; else y = clamp(point.y - Math.round(height / 2), workArea.y + MARGIN, workArea.y + workArea.height - height - MARGIN); return { x, y }; } function createTrayPopup({ htmlPath, preloadPath, getModel, onAction }) { let win = null; let height = DEFAULT_HEIGHT; let shownAt = 0; let handlersRegistered = false; const own = (event) => win && !win.isDestroyed() && event.sender === win.webContents; function registerHandlers() { if (handlersRegistered) return; handlersRegistered = true; ipcMain.on('tray-menu:action', (event, id) => { if (!own(event)) return; hide(); try { onAction(String(id)); } catch (e) { console.warn('[Tray] Aktion fehlgeschlagen:', e.message); } }); ipcMain.on('tray-menu:close', (event) => { if (own(event)) hide(); }); ipcMain.on('tray-menu:resize', (event, contentHeight) => { if (!own(event)) return; const h = Math.round(Number(contentHeight)); if (!(h > 40 && h < 900)) return; height = h; if (win.isVisible()) { const { x, y } = computePosition(screen.getCursorScreenPoint(), screen.getDisplayNearestPoint(screen.getCursorScreenPoint()).workArea, WIDTH, height); win.setBounds({ x, y, width: WIDTH, height }); } else { win.setContentSize(WIDTH, height); } }); } function ensureWindow() { if (win && !win.isDestroyed()) return win; registerHandlers(); win = new BrowserWindow({ width: WIDTH, height, show: false, frame: false, transparent: true, resizable: false, movable: false, minimizable: false, maximizable: false, fullscreenable: false, skipTaskbar: true, alwaysOnTop: true, focusable: true, hasShadow: false, webPreferences: { preload: preloadPath, contextIsolation: true, nodeIntegration: false, sandbox: true, spellcheck: false } }); win.setAlwaysOnTop(true, 'pop-up-menu'); win.on('blur', () => { if (Date.now() - shownAt > 250) hide(); }); win.on('closed', () => { win = null; }); win.loadFile(htmlPath); return win; } function push() { if (!win || win.isDestroyed()) return; win.webContents.send('tray-menu-state', { items: getModel(), dark: nativeTheme.shouldUseDarkColors }); } function show() { const w = ensureWindow(); const send = () => { push(); const point = screen.getCursorScreenPoint(); const { x, y } = computePosition(point, screen.getDisplayNearestPoint(point).workArea, WIDTH, height); w.setBounds({ x, y, width: WIDTH, height }); shownAt = Date.now(); w.show(); w.focus(); }; if (w.webContents.isLoading()) w.webContents.once('did-finish-load', send); else send(); } function hide() { if (win && !win.isDestroyed() && win.isVisible()) win.hide(); } function refresh() { if (win && !win.isDestroyed() && win.isVisible()) push(); } // Lädt das versteckte Fenster im Voraus. Der erste Start des Renderers dauert mehrere Sekunden; // ohne Vorladen würde der erste Rechtsklick auf das Tray-Symbol entsprechend hängen. function warmUp() { ensureWindow(); } function destroy() { if (win && !win.isDestroyed()) win.destroy(); win = null; } return { show, warmUp, hide, refresh, destroy, isVisible: () => !!win && !win.isDestroyed() && win.isVisible(), getWindow: () => (win && !win.isDestroyed() ? win : null) }; } module.exports = { createTrayPopup, computePosition };