lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ electron menu.ts

11,7 KB Rohdatei
electron/menu.ts — 330 Zeilen
1 import { Menu, app, shell, type MenuItemConstructorOptions } from 'electron';
2 import type { MenuCommand } from '../shared/ipc';
3 import { VERWEISADRESSEN } from '../shared/verweise';
4 import { SCHRIFT_STANDARD, SCHRIFT_STUFEN } from './schriftgroesse';
5
6 /**
7 * Anwendungsmenue.
8 *
9 * Der Altbestand rief `Menu.setApplicationMenu(null)`. Damit entfielen nicht nur
10 * die Menuepunkte, sondern auch die von Electron ueber das Menue bereitgestellten
11 * Standardbefehle: Kopieren, Einfuegen, Ausschneiden und Alles markieren
12 * funktionierten in den Eingabefeldern nur noch eingeschraenkt, und es gab
13 * keinen Weg mehr, das Fenster neu zu laden.
14 */
15
16 /**
17 * Zugriff des Menues auf die Schriftgroesse.
18 *
19 * Das Menue kennt die Stufen, aber nicht, wo sie gesichert werden und wie sie
20 * ans Fenster kommen - das entscheidet der Hauptprozess.
21 */
22 export interface SchriftgroessenSteuerung {
23 /** Gegenwaertige Stufe in Prozent. */
24 aktuell(): number;
25 /** Setzt genau diese Stufe. */
26 setze(prozent: number): void;
27 /** Eine Stufe groesser (`1`) oder kleiner (`-1`). */
28 schritt(richtung: 1 | -1): void;
29 }
30
31 /**
32 * Kennung des Menueeintrags einer Stufe.
33 *
34 * Der Hauptprozess braucht sie, um die Auswahlmarke nachzufuehren, wenn die
35 * Stufe ueber "Vergroessern", "Verkleinern" oder Strg+Mausrad geaendert wurde -
36 * also ohne dass jemand den Eintrag selbst angeklickt hat.
37 */
38 export function schriftgroesseMenueId(prozent: number): string {
39 return `schriftgroesse-${prozent}`;
40 }
41
42 /**
43 * Inhalt des Menues, ohne Electron-Bindung.
44 *
45 * Getrennt von `buildMenu`, damit Beschriftungen, Tastenkuerzel und die
46 * Auswahlmarke der Schriftgroesse pruefbar sind, ohne ein Fenster zu oeffnen.
47 */
48 export function menuevorlage(
49 send: (command: MenuCommand) => void,
50 isDevelopment: boolean,
51 schrift: SchriftgroessenSteuerung,
52 version: string,
53 oeffneSeite: (url: string) => void,
54 speichereProtokoll: () => void,
55 ): MenuItemConstructorOptions[] {
56 const aktuelleStufe = schrift.aktuell();
57 const template: MenuItemConstructorOptions[] = [
58 {
59 label: '&Datei',
60 submenu: [
61 {
62 label: 'Neues Projekt',
63 accelerator: 'CmdOrCtrl+N',
64 click: () => {
65 send('neu');
66 },
67 },
68 {
69 label: 'Projekt öffnen...',
70 accelerator: 'CmdOrCtrl+O',
71 click: () => {
72 send('oeffnen');
73 },
74 },
75 { type: 'separator' },
76 {
77 label: 'Speichern',
78 accelerator: 'CmdOrCtrl+S',
79 click: () => {
80 send('speichern');
81 },
82 },
83 {
84 label: 'Speichern unter...',
85 accelerator: 'CmdOrCtrl+Shift+S',
86 click: () => {
87 send('speichern-unter');
88 },
89 },
90 { type: 'separator' },
91 {
92 label: 'Planunterlagen ausgeben...',
93 accelerator: 'CmdOrCtrl+P',
94 click: () => {
95 send('export');
96 },
97 },
98 { type: 'separator' },
99 { role: 'quit', label: 'Beenden' },
100 ],
101 },
102 {
103 label: '&Bearbeiten',
104 submenu: [
105 /*
106 * Ein Hinweis zur Bedienbarkeit, ohne Bezug auf ein Kriterium:
107 * Diese beiden Eintraege hiessen "Rückgängig" und "Wiederherstellen" -
108 * wortgleich mit den Schaltflaechen der Kopfzeile, die etwas voellig
109 * anderes tun. Hier ist es die Rolle von Chromium: Sie nimmt die letzte
110 * TEXTEINGABE im gerade bearbeiteten Feld zurueck. Die Kopfzeile nimmt
111 * dagegen die letzte PROJEKTAENDERUNG zurueck - eine geloeschte
112 * Signalgruppe, eine verschobene Phase.
113 *
114 * Welche der beiden bei Strg+Z zum Zuge kommt, entscheidet der
115 * Anzeigeprozess: `src/main.ts` laesst den Tastendruck in einem
116 * Textfeld durch (dann greift dieser Menuepunkt) und faengt ihn sonst
117 * ab (dann greift die Projektaenderung). Die Absicht ist also richtig -
118 * nur die gleichlautenden Beschriftungen liessen den Anwender im
119 * Menue etwas anderes erwarten, als er bekam.
120 */
121 { role: 'undo', label: 'Texteingabe rückgängig' },
122 { role: 'redo', label: 'Texteingabe wiederherstellen' },
123 { type: 'separator' },
124 { role: 'cut', label: 'Ausschneiden' },
125 { role: 'copy', label: 'Kopieren' },
126 { role: 'paste', label: 'Einfügen' },
127 { role: 'selectAll', label: 'Alles markieren' },
128 ],
129 },
130 {
131 label: '&Ansicht',
132 submenu: [
133 /*
134 * Fassung 5.7.0, Befund L11 (EN 301 549 Kap. 11.7): Vergroessern gab es
135 * zuvor nur ueber die Chromium-Rollen `zoomIn` und `zoomOut`. Die
136 * brachten zwar Strg+Plus mit, aber keine benannten Stufen, keine
137 * Anzeige der eingestellten Groesse und vor allem keine Sicherung: Nach
138 * jedem Start stand die Anwendung wieder auf 100 %.
139 *
140 * Die Rollen sind deshalb durch eigene Eintraege ersetzt. Alle drei Wege
141 * - Auswahlliste, "Vergroessern/Verkleinern" und Strg+Mausrad
142 * (main.ts, Ereignis `zoom-changed`) - laufen jetzt durch dieselbe
143 * Stufenliste. Zwei Buchfuehrungen ueber dieselbe Groesse liefen sonst
144 * auseinander, und die Auswahlmarke zeigte auf eine Stufe, die gar nicht
145 * eingestellt war.
146 */
147 {
148 label: 'Schriftgröße',
149 submenu: SCHRIFT_STUFEN.map((stufe) => ({
150 id: schriftgroesseMenueId(stufe),
151 label: stufe === SCHRIFT_STANDARD ? `${stufe} % (Standard)` : `${stufe} %`,
152 type: 'radio' as const,
153 checked: stufe === aktuelleStufe,
154 click: () => {
155 schrift.setze(stufe);
156 },
157 })),
158 },
159 { type: 'separator' },
160 {
161 label: 'Vergrößern',
162 accelerator: 'CmdOrCtrl+Plus',
163 click: () => {
164 schrift.schritt(1);
165 },
166 },
167 /*
168 * Zweiter Weg zum Vergroessern.
169 *
170 * Die abgeloeste Chromium-Rolle `zoomIn` brachte Strg+Plus UND Strg+=
171 * mit. Auf einer Belegung, bei der "+" die Umschalttaste verlangt - US,
172 * UK, viele Notebooktastaturen -, ist Strg+= der Weg, der mit EINER
173 * Hand geht. Mit dem eigenen Eintrag entfiel er, und Befund L11 ist ein
174 * Zugaenglichkeitsbefund: Tastaturzugang ist sein Gegenstand.
175 *
176 * Als eigener, verborgener Eintrag: Electron nimmt je Eintrag genau
177 * einen Accelerator. `visible: false` haelt ihn aus dem aufgeklappten
178 * Menue heraus - er waere dort ein zweiter "Vergroessern"-Eintrag ohne
179 * eigenen Zweck -, das Tastenkuerzel bleibt trotzdem wirksam.
180 */
181 {
182 label: 'Vergrößern',
183 accelerator: 'CmdOrCtrl+=',
184 visible: false,
185 click: () => {
186 schrift.schritt(1);
187 },
188 },
189 {
190 label: 'Verkleinern',
191 accelerator: 'CmdOrCtrl+-',
192 click: () => {
193 schrift.schritt(-1);
194 },
195 },
196 {
197 label: 'Standardgröße',
198 accelerator: 'CmdOrCtrl+0',
199 click: () => {
200 schrift.setze(SCHRIFT_STANDARD);
201 },
202 },
203 { type: 'separator' },
204 { role: 'togglefullscreen', label: 'Vollbild' },
205 { type: 'separator' },
206 { role: 'reload', label: 'Neu laden' },
207 ...(isDevelopment
208 ? ([
209 { role: 'toggleDevTools', label: 'Entwicklerwerkzeuge' },
210 ] as MenuItemConstructorOptions[])
211 : []),
212 ],
213 },
214 {
215 label: '&Hilfe',
216 submenu: [
217 {
218 label: 'Kurzhilfe',
219 accelerator: 'F1',
220 click: () => {
221 send('hilfe');
222 },
223 },
224 /*
225 * Eine Kopie des Fehlerprotokolls an einen Ort eigener Wahl.
226 *
227 * Bis 5.42.1 nannte die Kurzhilfe allein den Pfad der Datei und bat,
228 * sie einer Fehlermeldung beizulegen; die beiden Fehlerdialoge in
229 * main.ts nennen denselben Pfad als Ort der Einzelheiten. Seit 5.43.0
230 * nennt die Kurzhilfe zuerst diesen Eintrag; die beiden Fehlerdialoge
231 * nennen ihn nach dem Pfad. Je nach Installationsart findet der
232 * Anwender die Datei dort nicht, wenn %APPDATA%\lsa-planer-professional
233 * vorher nicht bestand: Dann leitet Windows neue Dateien unter
234 * %APPDATA% in einen eigenen Bereich um, und `app.getPath('userData')`
235 * nennt den unumgeleiteten Pfad. Bestand der Ordner - vom Setup oder
236 * vom tragbaren Programm -, landen auch neue Dateien dort, und der Pfad
237 * stimmt (gemessen am 17.09.2026 unter Windows 11;
238 * docs/datenschutz.md). Das Programm selbst liest die Datei dort, wo es
239 * sie kennt, und schreibt sie dahin, wo der Anwender sie sucht.
240 *
241 * Ganz im Hauptprozess (`speichereProtokollkopie` in main.ts): kein
242 * IPC-Kanal und kein Rendereranteil. Die Quelle ist fest, das Ziel
243 * kommt allein aus dem Speichern-Dialog und geht nicht in die
244 * Freigabeliste des Renderers ein.
245 *
246 * Das Auslassungszeichen (U+2026) sagt, dass ein Dialog folgt. Die
247 * Fehlerdialoge in main.ts nennen den Eintrag mit genau dieser
248 * Beschriftung.
249 */
250 {
251 label: 'Fehlerprotokoll speichern …',
252 click: () => {
253 speichereProtokoll();
254 },
255 },
256 { type: 'separator' },
257 /*
258 * Die Akte des Programms auf der Internetseite des Herausgebers. Die
259 * Adresse steht in `shared/verweise.ts` und nicht hier: Bis 5.43.0
260 * stand sie hier ein zweites Mal, als Zeichenkette neben der Tafel.
261 */
262 {
263 label: 'Internetseite des Herstellers',
264 click: () => {
265 oeffneSeite(VERWEISADRESSEN.internetseite);
266 },
267 },
268 /*
269 * Der Verweis auf die Unterstuetzungsseite - freiwillig und still.
270 *
271 * KEIN DIALOG BEIM START, kein Zaehler, keine Wiedervorlage: ein
272 * dauerhafter, auffindbarer Eintrag und sonst nichts. Fachsoftware,
273 * die bettelt, verliert Vertrauen, und dieses Programm wird fuer
274 * Unterlagen benutzt, die nach Paragraph 45 StVO angeordnet werden.
275 *
276 * Derselbe Weg wie die Internetseite darueber - `oeffneSeite` gibt die
277 * Adresse an `shell.openExternal`. Der Kanal `oeffneVerweis` mit
278 * Schluessel statt Adresse gilt fuer den Renderer; hier steht die
279 * Adresse ohnehin schon im Hauptprozess.
280 */
281 {
282 label: 'Unterstützen',
283 click: () => {
284 oeffneSeite('https://ko-fi.com/olafwillerding');
285 },
286 },
287 /*
288 * Der Eintrag oeffnet das "Über"-Fenster im Renderer, nicht mehr die
289 * Internetseite.
290 *
291 * Bis 5.14.0 oeffnete er dieselbe Internetseite wie der Eintrag
292 * unmittelbar darueber. Zwei Eintraege mit derselben Wirkung, und
293 * die Frage eines Anwenders, wo sich die
294 * Lizenzen im Programm ansehen lassen, war mit "gar nicht" zu
295 * beantworten. Die Internetseite behaelt ihren eigenen Eintrag; dieser
296 * fuehrt jetzt zu Fassung, Urheber, Lizenz und Fremdbestandteilen
297 * (src/ui/ueber.ts).
298 */
299 {
300 label: `Über LSA-Planer Professional ${version}`,
301 click: () => {
302 send('ueber');
303 },
304 },
305 ],
306 },
307 ];
308
309 return template;
310 }
311
312 export function buildMenu(
313 send: (command: MenuCommand) => void,
314 isDevelopment: boolean,
315 schrift: SchriftgroessenSteuerung,
316 speichereProtokoll: () => void,
317 ): Menu {
318 return Menu.buildFromTemplate(
319 menuevorlage(
320 send,
321 isDevelopment,
322 schrift,
323 app.getVersion(),
324 (url) => {
325 void shell.openExternal(url);
326 },
327 speichereProtokoll,
328 ),
329 );
330 }