import { Menu, app, shell, type MenuItemConstructorOptions } from 'electron'; import type { MenuCommand } from '../shared/ipc'; import { VERWEISADRESSEN } from '../shared/verweise'; import { SCHRIFT_STANDARD, SCHRIFT_STUFEN } from './schriftgroesse'; /** * Anwendungsmenue. * * Der Altbestand rief `Menu.setApplicationMenu(null)`. Damit entfielen nicht nur * die Menuepunkte, sondern auch die von Electron ueber das Menue bereitgestellten * Standardbefehle: Kopieren, Einfuegen, Ausschneiden und Alles markieren * funktionierten in den Eingabefeldern nur noch eingeschraenkt, und es gab * keinen Weg mehr, das Fenster neu zu laden. */ /** * Zugriff des Menues auf die Schriftgroesse. * * Das Menue kennt die Stufen, aber nicht, wo sie gesichert werden und wie sie * ans Fenster kommen - das entscheidet der Hauptprozess. */ export interface SchriftgroessenSteuerung { /** Gegenwaertige Stufe in Prozent. */ aktuell(): number; /** Setzt genau diese Stufe. */ setze(prozent: number): void; /** Eine Stufe groesser (`1`) oder kleiner (`-1`). */ schritt(richtung: 1 | -1): void; } /** * Kennung des Menueeintrags einer Stufe. * * Der Hauptprozess braucht sie, um die Auswahlmarke nachzufuehren, wenn die * Stufe ueber "Vergroessern", "Verkleinern" oder Strg+Mausrad geaendert wurde - * also ohne dass jemand den Eintrag selbst angeklickt hat. */ export function schriftgroesseMenueId(prozent: number): string { return `schriftgroesse-${prozent}`; } /** * Inhalt des Menues, ohne Electron-Bindung. * * Getrennt von `buildMenu`, damit Beschriftungen, Tastenkuerzel und die * Auswahlmarke der Schriftgroesse pruefbar sind, ohne ein Fenster zu oeffnen. */ export function menuevorlage( send: (command: MenuCommand) => void, isDevelopment: boolean, schrift: SchriftgroessenSteuerung, version: string, oeffneSeite: (url: string) => void, speichereProtokoll: () => void, ): MenuItemConstructorOptions[] { const aktuelleStufe = schrift.aktuell(); const template: MenuItemConstructorOptions[] = [ { label: '&Datei', submenu: [ { label: 'Neues Projekt', accelerator: 'CmdOrCtrl+N', click: () => { send('neu'); }, }, { label: 'Projekt öffnen...', accelerator: 'CmdOrCtrl+O', click: () => { send('oeffnen'); }, }, { type: 'separator' }, { label: 'Speichern', accelerator: 'CmdOrCtrl+S', click: () => { send('speichern'); }, }, { label: 'Speichern unter...', accelerator: 'CmdOrCtrl+Shift+S', click: () => { send('speichern-unter'); }, }, { type: 'separator' }, { label: 'Planunterlagen ausgeben...', accelerator: 'CmdOrCtrl+P', click: () => { send('export'); }, }, { type: 'separator' }, { role: 'quit', label: 'Beenden' }, ], }, { label: '&Bearbeiten', submenu: [ /* * Ein Hinweis zur Bedienbarkeit, ohne Bezug auf ein Kriterium: * Diese beiden Eintraege hiessen "Rückgängig" und "Wiederherstellen" - * wortgleich mit den Schaltflaechen der Kopfzeile, die etwas voellig * anderes tun. Hier ist es die Rolle von Chromium: Sie nimmt die letzte * TEXTEINGABE im gerade bearbeiteten Feld zurueck. Die Kopfzeile nimmt * dagegen die letzte PROJEKTAENDERUNG zurueck - eine geloeschte * Signalgruppe, eine verschobene Phase. * * Welche der beiden bei Strg+Z zum Zuge kommt, entscheidet der * Anzeigeprozess: `src/main.ts` laesst den Tastendruck in einem * Textfeld durch (dann greift dieser Menuepunkt) und faengt ihn sonst * ab (dann greift die Projektaenderung). Die Absicht ist also richtig - * nur die gleichlautenden Beschriftungen liessen den Anwender im * Menue etwas anderes erwarten, als er bekam. */ { role: 'undo', label: 'Texteingabe rückgängig' }, { role: 'redo', label: 'Texteingabe wiederherstellen' }, { type: 'separator' }, { role: 'cut', label: 'Ausschneiden' }, { role: 'copy', label: 'Kopieren' }, { role: 'paste', label: 'Einfügen' }, { role: 'selectAll', label: 'Alles markieren' }, ], }, { label: '&Ansicht', submenu: [ /* * Fassung 5.7.0, Befund L11 (EN 301 549 Kap. 11.7): Vergroessern gab es * zuvor nur ueber die Chromium-Rollen `zoomIn` und `zoomOut`. Die * brachten zwar Strg+Plus mit, aber keine benannten Stufen, keine * Anzeige der eingestellten Groesse und vor allem keine Sicherung: Nach * jedem Start stand die Anwendung wieder auf 100 %. * * Die Rollen sind deshalb durch eigene Eintraege ersetzt. Alle drei Wege * - Auswahlliste, "Vergroessern/Verkleinern" und Strg+Mausrad * (main.ts, Ereignis `zoom-changed`) - laufen jetzt durch dieselbe * Stufenliste. Zwei Buchfuehrungen ueber dieselbe Groesse liefen sonst * auseinander, und die Auswahlmarke zeigte auf eine Stufe, die gar nicht * eingestellt war. */ { label: 'Schriftgröße', submenu: SCHRIFT_STUFEN.map((stufe) => ({ id: schriftgroesseMenueId(stufe), label: stufe === SCHRIFT_STANDARD ? `${stufe} % (Standard)` : `${stufe} %`, type: 'radio' as const, checked: stufe === aktuelleStufe, click: () => { schrift.setze(stufe); }, })), }, { type: 'separator' }, { label: 'Vergrößern', accelerator: 'CmdOrCtrl+Plus', click: () => { schrift.schritt(1); }, }, /* * Zweiter Weg zum Vergroessern. * * Die abgeloeste Chromium-Rolle `zoomIn` brachte Strg+Plus UND Strg+= * mit. Auf einer Belegung, bei der "+" die Umschalttaste verlangt - US, * UK, viele Notebooktastaturen -, ist Strg+= der Weg, der mit EINER * Hand geht. Mit dem eigenen Eintrag entfiel er, und Befund L11 ist ein * Zugaenglichkeitsbefund: Tastaturzugang ist sein Gegenstand. * * Als eigener, verborgener Eintrag: Electron nimmt je Eintrag genau * einen Accelerator. `visible: false` haelt ihn aus dem aufgeklappten * Menue heraus - er waere dort ein zweiter "Vergroessern"-Eintrag ohne * eigenen Zweck -, das Tastenkuerzel bleibt trotzdem wirksam. */ { label: 'Vergrößern', accelerator: 'CmdOrCtrl+=', visible: false, click: () => { schrift.schritt(1); }, }, { label: 'Verkleinern', accelerator: 'CmdOrCtrl+-', click: () => { schrift.schritt(-1); }, }, { label: 'Standardgröße', accelerator: 'CmdOrCtrl+0', click: () => { schrift.setze(SCHRIFT_STANDARD); }, }, { type: 'separator' }, { role: 'togglefullscreen', label: 'Vollbild' }, { type: 'separator' }, { role: 'reload', label: 'Neu laden' }, ...(isDevelopment ? ([ { role: 'toggleDevTools', label: 'Entwicklerwerkzeuge' }, ] as MenuItemConstructorOptions[]) : []), ], }, { label: '&Hilfe', submenu: [ { label: 'Kurzhilfe', accelerator: 'F1', click: () => { send('hilfe'); }, }, /* * Eine Kopie des Fehlerprotokolls an einen Ort eigener Wahl. * * Bis 5.42.1 nannte die Kurzhilfe allein den Pfad der Datei und bat, * sie einer Fehlermeldung beizulegen; die beiden Fehlerdialoge in * main.ts nennen denselben Pfad als Ort der Einzelheiten. Seit 5.43.0 * nennt die Kurzhilfe zuerst diesen Eintrag; die beiden Fehlerdialoge * nennen ihn nach dem Pfad. Je nach Installationsart findet der * Anwender die Datei dort nicht, wenn %APPDATA%\lsa-planer-professional * vorher nicht bestand: Dann leitet Windows neue Dateien unter * %APPDATA% in einen eigenen Bereich um, und `app.getPath('userData')` * nennt den unumgeleiteten Pfad. Bestand der Ordner - vom Setup oder * vom tragbaren Programm -, landen auch neue Dateien dort, und der Pfad * stimmt (gemessen am 17.09.2026 unter Windows 11; * docs/datenschutz.md). Das Programm selbst liest die Datei dort, wo es * sie kennt, und schreibt sie dahin, wo der Anwender sie sucht. * * Ganz im Hauptprozess (`speichereProtokollkopie` in main.ts): kein * IPC-Kanal und kein Rendereranteil. Die Quelle ist fest, das Ziel * kommt allein aus dem Speichern-Dialog und geht nicht in die * Freigabeliste des Renderers ein. * * Das Auslassungszeichen (U+2026) sagt, dass ein Dialog folgt. Die * Fehlerdialoge in main.ts nennen den Eintrag mit genau dieser * Beschriftung. */ { label: 'Fehlerprotokoll speichern …', click: () => { speichereProtokoll(); }, }, { type: 'separator' }, /* * Die Akte des Programms auf der Internetseite des Herausgebers. Die * Adresse steht in `shared/verweise.ts` und nicht hier: Bis 5.43.0 * stand sie hier ein zweites Mal, als Zeichenkette neben der Tafel. */ { label: 'Internetseite des Herstellers', click: () => { oeffneSeite(VERWEISADRESSEN.internetseite); }, }, /* * Der Verweis auf die Unterstuetzungsseite - freiwillig und still. * * KEIN DIALOG BEIM START, kein Zaehler, keine Wiedervorlage: ein * dauerhafter, auffindbarer Eintrag und sonst nichts. Fachsoftware, * die bettelt, verliert Vertrauen, und dieses Programm wird fuer * Unterlagen benutzt, die nach Paragraph 45 StVO angeordnet werden. * * Derselbe Weg wie die Internetseite darueber - `oeffneSeite` gibt die * Adresse an `shell.openExternal`. Der Kanal `oeffneVerweis` mit * Schluessel statt Adresse gilt fuer den Renderer; hier steht die * Adresse ohnehin schon im Hauptprozess. */ { label: 'Unterstützen', click: () => { oeffneSeite('https://ko-fi.com/olafwillerding'); }, }, /* * Der Eintrag oeffnet das "Über"-Fenster im Renderer, nicht mehr die * Internetseite. * * Bis 5.14.0 oeffnete er dieselbe Internetseite wie der Eintrag * unmittelbar darueber. Zwei Eintraege mit derselben Wirkung, und * die Frage eines Anwenders, wo sich die * Lizenzen im Programm ansehen lassen, war mit "gar nicht" zu * beantworten. Die Internetseite behaelt ihren eigenen Eintrag; dieser * fuehrt jetzt zu Fassung, Urheber, Lizenz und Fremdbestandteilen * (src/ui/ueber.ts). */ { label: `Über LSA-Planer Professional ${version}`, click: () => { send('ueber'); }, }, ], }, ]; return template; } export function buildMenu( send: (command: MenuCommand) => void, isDevelopment: boolean, schrift: SchriftgroessenSteuerung, speichereProtokoll: () => void, ): Menu { return Menu.buildFromTemplate( menuevorlage( send, isDevelopment, schrift, app.getVersion(), (url) => { void shell.openExternal(url); }, speichereProtokoll, ), ); }