lsa-planer

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

/ electron schriftgroesse.ts

8,6 KB Rohdatei
electron/schriftgroesse.ts — 211 Zeilen
1 import { readFileSync, writeFileSync } from 'node:fs';
2
3 /**
4 * Schriftgroesse des Anwendungsfensters - Stufen, Pruefung und Sicherung.
5 *
6 * Fassung 5.7.0, Befund L11 (EN 301 549 Kap. 11.7, "Nutzereinstellungen"):
7 * Vergroessern war bis hierher ausschliesslich ueber die Chromium-Vorgabe
8 * Strg+Plus moeglich, und die eingestellte Stufe hielt genau eine Sitzung. Sie
9 * war nach jedem Programmstart, nach jedem "Neu laden" und nach jedem Neustart
10 * aus einem Absturz heraus (main.ts, `mainWindow.reload()`) wieder auf 100 %.
11 * Wer 175 % braucht - in Behoerden die Kerngruppe -, stellte sie also mehrmals
12 * taeglich neu ein. WCAG 1.4.4 ist davon nicht beruehrt: Die Vergroesserung war
13 * moeglich. 11.7 verlangt darueber hinaus, dass eine getroffene Einstellung
14 * erhalten bleibt.
15 *
16 * WARUM DIE STUFE IM HAUPTPROZESS LIEGT UND NICHT IN `AppSettings`
17 *
18 * Die Programmeinstellungen des Anzeigeprozesses (Erscheinungsbild,
19 * Selbstsicherung, Sprache, Fundstellen) liegen im localStorage und werden ueber
20 * `loadAppSettings()` gelesen - der Hauptprozess kommt dort nicht heran. Die
21 * Vergroesserungsstufe muss aber schon feststehen, BEVOR das Fenster entsteht:
22 * Sie geht als `webPreferences.zoomFactor` in `new BrowserWindow(...)` ein,
23 * damit die erste Darstellung bereits in der richtigen Groesse erscheint statt
24 * sichtbar von 100 % auf 175 % zu springen. Sie ist ausserdem keine Eigenschaft
25 * des Dokuments, sondern des Fensters - wie Fenstergroesse und Fensterlage, die
26 * kuenftig in dieselbe Datei gehoeren.
27 *
28 * Die Art der Sicherung folgt bewusst `loadAppSettings()`: fester Satz zulaessiger
29 * Werte, jede Abweichung faellt auf die Vorgabe zurueck, ein Lesefehler bleibt
30 * folgenlos. Fremde Schluessel der Datei bleiben beim Schreiben erhalten, damit
31 * eine spaetere Fenstereinstellung nicht die hier gesicherte ueberschreibt.
32 */
33
34 /**
35 * Waehlbare Stufen in Prozent.
36 *
37 * 200 % ist die von WCAG 1.4.4 geforderte Vergroesserung, 250 % die Reserve fuer
38 * Anwender mit staerkerer Sehbehinderung; darunter bleiben 75 % und 90 %
39 * erhalten, weil Chromiums "Verkleinern" bisher unter 100 % gehen konnte und
40 * dieser Weg nicht verloren gehen darf.
41 */
42 export const SCHRIFT_STUFEN: readonly number[] = [75, 90, 100, 125, 150, 175, 200, 250];
43
44 export const SCHRIFT_STANDARD = 100;
45
46 /** Name der Datei im Benutzerdatenverzeichnis. */
47 export const ANSICHT_DATEINAME = 'ansicht.json';
48
49 /** Schluessel in der Datei. */
50 const SCHLUESSEL = 'schriftgroesseProzent';
51
52 /**
53 * Naechstgelegene zulaessige Stufe.
54 *
55 * Alles, was keine Zahl ist, wird zur Vorgabe. Ein von Hand eingetragener oder
56 * ueber Strg+Mausrad entstandener Zwischenwert wird auf die naechste Stufe
57 * gerundet - so bleibt es bei genau einer Buchfuehrung ueber die Groesse.
58 */
59 export function gueltigeStufe(wert: unknown): number {
60 if (typeof wert !== 'number' || !Number.isFinite(wert)) return SCHRIFT_STANDARD;
61 return SCHRIFT_STUFEN[stufenIndex(wert)] ?? SCHRIFT_STANDARD;
62 }
63
64 /** Eine Stufe groesser (`1`) oder kleiner (`-1`); an den Enden bleibt es stehen. */
65 export function nachbarstufe(aktuell: number, richtung: 1 | -1): number {
66 const ziel = stufenIndex(aktuell) + richtung;
67 const begrenzt = Math.min(SCHRIFT_STUFEN.length - 1, Math.max(0, ziel));
68 return SCHRIFT_STUFEN[begrenzt] ?? SCHRIFT_STANDARD;
69 }
70
71 function stufenIndex(prozent: number): number {
72 let beste = 0;
73 let abstand = Number.POSITIVE_INFINITY;
74 for (let i = 0; i < SCHRIFT_STUFEN.length; i += 1) {
75 const stufe = SCHRIFT_STUFEN[i];
76 if (stufe === undefined) continue;
77 const naehe = Math.abs(stufe - prozent);
78 if (naehe < abstand) {
79 abstand = naehe;
80 beste = i;
81 }
82 }
83 return beste;
84 }
85
86 /** Liest die gesicherte Stufe. Schlaegt niemals fehl. */
87 export function liesSchriftgroesse(datei: string): number {
88 return gueltigeStufe(leseDatei(datei)[SCHLUESSEL]);
89 }
90
91 /**
92 * Sichert die Stufe. Schlaegt niemals fehl - eine Ansichtseinstellung ist
93 * nichts, wofuer die Anwendung stehenbleiben darf.
94 *
95 * Zurueck kommt, ob wirklich geschrieben wurde. Der Anwender erfaehrt von einem
96 * Fehlschlag nichts, die Buchfuehrung in `schriftsicherung` aber schon: Ein
97 * Fehlschlag, der als Erfolg verbucht wird, kostet die Einstellung fuer die
98 * ganze Sitzung (Befund 11).
99 */
100 export function schreibeSchriftgroesse(datei: string, prozent: number): boolean {
101 const inhalt = { ...leseDatei(datei), [SCHLUESSEL]: gueltigeStufe(prozent) };
102 try {
103 writeFileSync(datei, `${JSON.stringify(inhalt, null, 2)}\n`, 'utf8');
104 return true;
105 } catch {
106 /* Kein Schreibrecht, Datentraeger voll: Die Anwendung laeuft ohne Sicherung weiter. */
107 return false;
108 }
109 }
110
111 /**
112 * Verzoegerte Sicherung - eine Schreibung je Bedienvorgang, nicht je Raste.
113 *
114 * BEFUND (Fassung 5.7.0): `zoom-changed` feuert bei jeder Raste von
115 * Strg+Mausrad, und jede Raste lief bis hierher in ein `writeFileSync` im
116 * HAUPTPROZESS - also in genau den Prozess, der die Fenster bedient. Wer das
117 * Rad einmal durchdreht, loeste ein Dutzend Schreibungen aus; auf einem
118 * servergespeicherten Benutzerprofil, in Behoerden der Regelfall, blockiert
119 * jede davon die Oberflaeche. Geschrieben wurde ausserdem auch dann, wenn sich
120 * gar nichts geaendert hatte: An beiden Enden der Stufenliste gibt
121 * `nachbarstufe` denselben Wert zurueck.
122 *
123 * Zwei Massnahmen, beide hier, damit jeder Weg zur Stufe sie bekommt:
124 *
125 * - `plane()` schreibt nur, wenn der Wert vom zuletzt gesicherten abweicht.
126 * - Es wartet die Ruhezeit ab. Wer weiterdreht, verschiebt sie; erst wenn er
127 * aufhoert, geht eine einzige Schreibung heraus.
128 *
129 * `jetzt()` fuer den Programmschluss: Eine Einstellung, die eine halbe Sekunde
130 * vor dem Beenden getroffen wurde, darf nicht verlorengehen.
131 *
132 * Der zuletzt gesicherte Wert wird gemerkt und nicht aus der Datei gelesen -
133 * ein Lesevorgang je Raste waere derselbe Fehler mit umgekehrtem Vorzeichen.
134 * Gemerkt wird er erst, wenn das Schreiben wirklich gelungen ist: Sonst stuende
135 * ein misslungener Versuch als gesichert in der Buchfuehrung, und `jetzt()`
136 * haette beim Programmende nichts mehr nachzuholen (Befund 11).
137 */
138 export interface Schriftsicherung {
139 /** Merkt die Stufe vor; geschrieben wird nach der Ruhezeit. */
140 plane: (prozent: number) => void;
141 /** Schreibt sofort, was noch aussteht - fuer das Programmende. */
142 jetzt: () => void;
143 }
144
145 export const SICHERUNG_RUHEZEIT_MS = 750;
146
147 export function schriftsicherung(
148 datei: string,
149 gesichert: number,
150 ruhezeitMs: number = SICHERUNG_RUHEZEIT_MS,
151 ): Schriftsicherung {
152 let letzteGesicherte = gueltigeStufe(gesichert);
153 let offen: number | null = null;
154 let uhr: ReturnType<typeof setTimeout> | null = null;
155
156 const schreibe = (): void => {
157 if (uhr !== null) {
158 clearTimeout(uhr);
159 uhr = null;
160 }
161 if (offen === null) return;
162 const wert = offen;
163 // Erst der Versuch, dann die Buchfuehrung. Misslingt das Schreiben - kein
164 // Schreibrecht, Profilabgleich, Virenwaechter -, bleibt der Wert
165 // vorgemerkt: `jetzt()` beim Programmende und jede weitere Raste versuchen
166 // es dann noch einmal. Andersherum galt er als gesichert, obwohl nichts auf
167 // dem Datentraeger stand, und die Einstellung war fuer die ganze Sitzung
168 // verloren.
169 if (!schreibeSchriftgroesse(datei, wert)) return;
170 offen = null;
171 letzteGesicherte = wert;
172 };
173
174 return {
175 plane: (prozent) => {
176 const wert = gueltigeStufe(prozent);
177 if (wert === letzteGesicherte) {
178 // Auch eine bereits vorgemerkte Aenderung faellt weg, wenn der Anwender
179 // wieder auf dem gesicherten Wert landet - hin und zurueck ist keine
180 // Aenderung.
181 offen = null;
182 if (uhr !== null) {
183 clearTimeout(uhr);
184 uhr = null;
185 }
186 return;
187 }
188 offen = wert;
189 if (uhr !== null) clearTimeout(uhr);
190 uhr = setTimeout(schreibe, ruhezeitMs);
191 // Ein Zeitgeber im Hauptprozess darf das Beenden nicht hinauszoegern.
192 // Das Fragezeichen beanstandet der Linter, weil Node den Zeitgeber als
193 // Timeout mit unref fuehrt. Es bleibt: Unter einer Testnachbildung oder
194 // einem Browser-setTimeout kommt eine blosse Zahl zurueck, und dann
195 // duerfte das Ablegen des Schriftgrads nicht daran scheitern.
196 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- unref gibt es nur bei Node-Zeitgebern; siehe darueber
197 uhr.unref?.();
198 },
199 jetzt: schreibe,
200 };
201 }
202
203 function leseDatei(datei: string): Record<string, unknown> {
204 try {
205 const gelesen: unknown = JSON.parse(readFileSync(datei, 'utf8'));
206 if (gelesen === null || typeof gelesen !== 'object' || Array.isArray(gelesen)) return {};
207 return gelesen as Record<string, unknown>;
208 } catch {
209 return {};
210 }
211 }