141 lines
4.5 KiB
JavaScript
141 lines
4.5 KiB
JavaScript
// 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 };
|