Settings take effect with Apply/Close or Save (#134)

* Add a settings draft that holds edits until they are applied

The settings window is to stage its edits and apply them with Apply/Close
or Save instead of writing each one at once. SettingsDraft holds one
pending change per setting, reads through to the stored value, and on
apply writes everything first and then runs each effect key once, in the
order the settings were first changed. Form.committed registers its typed
entry with the draft so a click on Apply cannot lose it.

The spec explains the order and the settings that stay immediate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Stage the settings tabs' edits and apply them with Apply/Close or Save

Every tab state now reads through the window's draft and stages its edits
there instead of writing ChatPreferences: Station (all but the login
credentials), Log synch, TRX synch, Airscout, Notification, Shortcuts,
Beacon, Message handling and GUI (all but the language). The lists stage
their roster, the committed fields stage what their rule accepts.

Effects stay attached to their settings. The Win-Test listener and the DX
cluster server share one effect for switch and port that brings the running
service in line with the applied state, so applying both restarts it once,
on the new port. The own-QRG follower runs once for both sources.

Apply/Close applies and closes, Save applies and writes, Connect applies
before it connects, so the session uses what the tabs show. A field that
refuses its entry on the way stops the button with the reason shown.

Immediate as before: the login credentials, the language, the colours, the
file chooser, the test spot and the profile and worked-database actions.

Number fields keep the typed text while the staged value comes back as
Compose state; otherwise "1" became "1.0" before the next key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Ask about unapplied settings when the window is closed

Closing the settings window with its X, or through the Windows menu, while
edits wait in the draft now asks: apply them, discard them, or go back to
the window. Without pending edits the window closes as before. Fields and
list rows still holding a typed entry are committed first, so an entry
typed just before the X counts.

The request only sets Compose state; the window decides on its next frame,
so nothing waits on the event thread. A close from outside -- a profile
switch, the shutdown -- discards the draft: the preferences it belongs to
are about to be replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Document that settings take effect with Apply

PROJECT_CONTEXT records the draft and what stays immediate, the changelog
gets one entry under v1.50, and the configuration manual no longer says
that settings take effect while they are edited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Repair stored settings once before the settings window is built

The fallback band of the DX cluster was repaired to 144 inside a getter
that runs during composition, and the Win-Test broadcast address was
auto-detected in a tab state's constructor. Both wrote ChatPreferences
past the draft at a moment nobody could see. repairStoredSettings now does
both once, before any tab is built; the tab states only read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Ask before a profile switch or exit, and check fields before applying

A profile switch and the exit dropped pending settings edits silently. They
now go through SettingsWindow.settleThen: with edits pending, the window is
raised and asks Apply / Discard / Back; Apply applies them to the profile
still in force, Back cancels the switch or the exit. The question is Compose
state and the continuation is posted from the dialog's callback, so the
event thread never waits. Without pending edits both go on at once.

Fields are checked before anything is applied: a field still showing text
that does not parse, or that its rule refuses, refuses the apply and is
named in the notice, so the valid edits stay pending and Discard is still
possible. This covers number fields and the guarded external-interface
fields, which used to let Apply go ahead with the last valid value.

Every write and effect of an apply now runs on its own; failures are
collected and reported, a failed write stays pending and skips its effect,
and Save still writes the file when only an effect failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Check every drafted setting with a real draft, and rename write-through tests

DraftedSettingsTableTest walks every drafted setter of every tab with a
real SettingsDraft: unchanged after the edit, changed after apply,
unchanged after discard. The tab tests run with a draft that applies at
once and cannot tell a drafted setting from one that bypasses the draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Document the question on switch and exit, the field checks and the repairs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Decide the pending-settings question on the event thread, never trap

The question is now a SettingsQuestion decided in settle() on the calling
event thread instead of in the settings window's next frame, so an exit
from the main window does not depend on frames of a minimised settings
window; the window is raised and restored before the dialog is needed.

While a question is open, further requests are ignored: an X no longer
replaces the continuation of an exit or switch with a plain close. Each
continuation of settleThen runs at most once.

A field holding text it refuses no longer traps the operator: the X, the
switch and the exit show the question anyway, with the reason, Apply
disabled and Discard and Back available.

The apply-failure notice keeps the technical detail out of the sentence,
in parentheses after a translated one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Rsclub2_2andClaude Opus 5.5 authored and GitHub committed 2026-10-16 12:32:31 +02:00
1 parent c64e9a9014
commit f17ae0905f
50 files changed
+3079 -615

No files matched your search

+15
View File
@@ -276,6 +276,21 @@ section records only the durable architecture and operational boundaries.
## Important Decisions and Workarounds
- **Settings take effect with Apply, not while they are edited** (release 1.50,
`docs/superpowers/specs/2026-10-16-settings-apply-drafts-design.md`): the settings window
stages every edit in a `SettingsDraft` that the tab states read through; Apply/Close, Save
and Connect apply it; the X, a profile switch and the exit ask about what is pending first
(`SettingsWindow.settleThen`, Back cancels). Fields still holding text -- typed but not
committed, or invalid -- are checked before anything is applied, so a refusal leaves Discard
possible. Apply writes everything first and then runs each effect key once, in first-changed
order, so a listener or server whose switch and port both changed restarts once, on the new
port; a failing write or effect is reported and the rest still runs.
Immediate on purpose: login credentials (locked while connected), language, colours, and the
actions (profiles, worked database, test spot, file chooser, stylesheet export). A new
setting goes through `draft.setting(...)` and gets a row in `DraftedSettingsTableTest`; one
that writes `ChatPreferences` directly bypasses Apply. Corrections of stored values that are
no edit (fallback band, Win-Test broadcast detection) run once in `repairStoredSettings`
before the window is built, never inside a tab state.
- **The interface language has four kinds of text, and the rule is the addressee** (release
1.50): interface text is translated; protocol text (the pipe-framed opcodes in
`On4KstProtocol`, and the `/`-prefixed chat commands) and amateur shorthand (the
@@ -0,0 +1,167 @@
# Entwurf — Einstellungen frei bearbeiten, mit „Übernehmen" anwenden
Freigegeben vom Maintainer am 2026-10-16. Baut auf PR #128 auf (kein Festschreiben beim
Öffnen, gesperrte Anmeldefelder während einer Sitzung).
## Warum
Bis hierher schrieb jedes Steuerelement des Einstellungsfensters beim Ändern sofort in
`ChatPreferences`, und manche lösten dabei sofort eine Wirkung in der laufenden Sitzung aus:
der Bakentimer startete neu, ein UDP-Listener wurde neu gebunden, der lokale
DX-Cluster-Server gestartet oder gestoppt. Das war die Übernahme des JavaFX-Fensters, und es
hat zwei Folgen, die im Contest stören:
- **Jeder Zwischenstand wirkt.** Wer einen Port von 9871 auf 9872 ändert, tippt über 987 und
98 hinweg; wer zwei zusammengehörige Häkchen setzt, hat zwischendurch einen halben Stand
aktiv. Ein Fehlgriff wirkt sofort und lässt sich nicht verwerfen.
- **Es gibt kein „doch nicht".** Das Fenster hat keinen Weg zurück. „Übernehmen/Schließen"
schloss nur, und wer etwas ausprobiert hatte, musste es von Hand zurückdrehen.
Das neue Verhalten ist das, das ein Operateur von einem Einstellungsdialog erwartet:
bearbeiten, dann anwenden oder verwerfen.
## Verhalten
1. Das Öffnen des Fensters beginnt einen Entwurf. Felder, Häkchen und Auswahlen aller Reiter
schreiben in den Entwurf statt in die Einstellungen; das Fenster zeigt den Entwurfswert.
Bearbeiten geht auch während einer Sitzung.
2. **Übernehmen/Schließen** wendet alle ausstehenden Änderungen an — Schreibvorgang samt
Wirkung — und schließt das Fenster.
3. **Einstellungen speichern** wendet zuerst an, dann schreibt es die Datei. Das Fenster
bleibt offen.
4. Das **X** der Titelleiste (und das Schließen über das Fenster-Menü) fragt bei
ausstehenden Änderungen: Übernehmen, Verwerfen oder zurück zum Fenster. Ohne ausstehende
Änderungen schließt es wie bisher.
5. Ungültige Eingaben werden wie bisher am Feld abgewiesen und erreichen den Entwurf nie.
Steht beim Übernehmen, Speichern oder Verbinden noch ungültiger Text in einem Feld (rot),
gilt das als Abweisung: nichts wird angewendet, das Fenster bleibt offen, die Meldung nennt
das Feld. Sonst ginge still der letzte gültige Wert in Kraft, den der Operateur nicht mehr
sieht — und Verwerfen wäre nicht mehr möglich, weil die übrigen Änderungen schon wirkten.
Erst wenn kein Feld etwas abweist, wird angewendet. Damit ein solches Feld nicht
einsperrt, erscheint beim X, beim Profilwechsel und beim Beenden die Frage trotzdem: mit
dem Grund, Übernehmen gesperrt, Verwerfen und Zurück verfügbar.
6. **Verbinden** wendet ebenfalls zuerst an. Sonst verbände der Knopf mit dem gespeicherten
statt mit dem sichtbaren Serverport oder Zweitchat-Häkchen — der Operateur sieht den einen
Wert und bekommt den anderen.
7. **Profilwechsel und Programmende** stellen dieselbe Frage, bevor sie weitergehen
(`SettingsWindow.settleThen`): Übernehmen wendet die Änderungen auf das **bisherige**
Profil an — das, in dem sie gemacht wurden — und setzt den Wechsel bzw. das Beenden fort;
Verwerfen setzt ohne sie fort; Zurück bricht Wechsel bzw. Beenden ab. Ohne offene
Änderungen geht es sofort und wie bisher weiter. Solange eine Frage offen ist, werden
weitere Anfragen ignoriert — ein X darf die Fortsetzung eines Beendens nicht durch ein
bloßes Schließen ersetzen —, und jede Fortsetzung läuft höchstens einmal. Erst der anschließende Abbau
(`SettingsWindow.close`) und ein JVM-Shutdown-Hook verwerfen ohne Frage.
8. **Scheitert beim Anwenden ein Schreibvorgang oder eine Wirkung**, laufen alle übrigen
trotzdem; die Fehler stehen danach in einer Problem-Meldung, das Fenster bleibt offen. Eine
Einstellung, deren Schreibvorgang scheiterte, bleibt im Entwurf, ihre Wirkung läuft nicht.
Speichern schreibt die Datei, wenn alle Schreibvorgänge gelangen, auch wenn eine Wirkung
scheiterte — deren Einstellung ist gespeichert und gehört in die Datei.
## Was sofort wirkt, wie bisher
- **Farben** (Reiter Farben) und **Sprache** (Reiter GUI). Beide sind sichtbar, sobald sie
wirken; ein Entwurf hätte dort nichts zu zeigen.
- **Anmeldefelder** des Reiters Station: Rufzeichen, beide Namen, Locator, Passwort, beide
Chatkategorien. Sie sind laut #128 während einer Sitzung gesperrt; ohne Sitzung wirken sie
ohnehin erst beim nächsten Verbinden.
- **Aktionen**: Profil anlegen, duplizieren, umbenennen, löschen, aktivieren; die Aktionen
der Worked-Datenbank; der Test-Spot des DX-Clusters; die „Choose…"-Dateiauswahl;
der Export des Stylesheets. Das sind Handlungen, keine Werte.
- **Reparaturen gespeicherter Werte** (`repairStoredSettings`), einmal beim Öffnen des
Fensters, bevor ein Reiter gebaut wird: eine unlesbare Ausweichband-Vorgabe des
DX-Clusters wird zu 144, und die Win-Test-Broadcastadresse wird, solange sie noch der
Platzhalter 255.255.255.255 ist, durch die erkannte Subnetz-Adresse ersetzt. Beides schrieb
schon das JavaFX-Fenster beim Aufbau, beides ist keine Eingabe des Operateurs und hat
nichts zum Übernehmen oder Verwerfen — darum direkt in `ChatPreferences`. Früher geschah
das in den Reiterzuständen, eines davon beim Lesen während der Komposition; Compose liest
wann es will, und ein Schreiben dort ist leicht zu übersehen. Jetzt gibt es dafür einen
bekannten Zeitpunkt, und kein Reiterzustand schreibt beim Lesen oder Bauen.
Unverändert bleiben das Format von `preferences.xml` und die Knöpfe der Verbindungsleiste.
## Mechanismus
`SettingsDraft` ist ein Fensterobjekt, das einmal pro Öffnen entsteht und an alle
Reiterzustände geht. Er hält ausstehende Änderungen, eine pro Einstellung, unter einem
Schlüssel:
```kotlin
draft.stage(key, value, live = { prefs.getX() }, write = { prefs.setX(it) },
effect = ::restartSomething, effectKey = "something")
draft.valueOf(key) { prefs.getX() } // Entwurfswert oder der gespeicherte
draft.commitTypedEntries() // getippter Text der Felder → Entwurf, oder Abweisungen
draft.apply() // Schreiben, dann Wirkungen; liefert die Fehlschläge
draft.discard()
draft.hasPendingChanges // Compose-Zustand
```
- **Lesen geht durch den Entwurf.** Die Getter der Reiterzustände lesen
`draft.valueOf(key) { prefs… }`. Die Ablage ist Compose-Zustand, also zeichnet ein Feld nach
einer Änderung neu — die alten `revision`-Zähler der Reiter werden dafür nicht mehr gebraucht,
schaden aber nicht.
- **Zurück auf den gespeicherten Wert heißt: nichts steht aus.** Ein Wert gleich dem
gespeicherten nimmt den Eintrag heraus. Ein Häkchen hin und zurück lässt keine Frage beim
Schließen zurück und keinen Neustart beim Übernehmen.
- **`Form.committed`** bleibt wie in #128: Fokusverlust oder Enter rufen `commit`, und das
schreibt jetzt in den Entwurf. `current` liest über den Reiterzustand den Entwurfs- oder den
gespeicherten Wert. Damit gelten die drei Zusagen von #128 weiter: kein Festschreiben beim
Öffnen, kein doppeltes, kein verlorenes.
- **Listen** (Kurzbefehle, Textbausteine, beobachtete Rufzeichen) halten ihren Arbeitsstand
schon in `EditableListState`; der Entwurf merkt sich nur, dass die Liste geändert ist, und
`apply` füllt den Roster aus diesem Stand.
## Reihenfolge beim Anwenden
1. **Erst alle Schreibvorgänge**, in der Reihenfolge, in der die Einstellungen zum ersten Mal
geändert wurden.
2. **Dann die Wirkungen**, in derselben Reihenfolge, **jede Wirkung einmal**. Wirkungen tragen
einen Schlüssel; teilen sich zwei Einstellungen einen, läuft er einmal.
3. Dann ist der Entwurf leer.
Schreiben vor Wirken ist nicht Geschmack. Win-Test-Listener ein **und** neuer Port: liefe die
Wirkung zwischen den Schreibvorgängen, bände der Listener noch den alten Port. Erst alle
Werte, dann die Wirkungen heißt: jede Wirkung sieht den fertigen Stand.
Einmal pro Schlüssel ist ebenfalls nicht Geschmack. Zwei kurz nacheinander neu gebundene
Sockets auf demselben Port können am noch nicht freigegebenen ersten scheitern. Die
geteilten Schlüssel:
| Schlüssel | Einstellungen | Wirkung (auf dem fertigen Stand) |
|---|---|---|
| Win-Test-Listener | Listener ein/aus, Port | an → Neustart, aus → Stopp |
| DX-Cluster-Server | Server ein/aus, Port | nur angemeldet: an → Neustart, aus → Stopp |
| Eigene-QRG-Folger | UCXLog-QRG, Win-Test-QRG | Folger an, wenn eine der beiden Quellen an ist |
| Bakentimer | Intervall | Neustart |
| Mitschnitt in Datei | Häkchen | Fehlerprotokoll und Recorder öffnen oder schließen |
| Kurzbefehle / Textbausteine | Listen | Knöpfe bzw. Kontextmenüs neu bauen |
Die geteilten Wirkungen gleichen den laufenden Dienst an den fertigen Stand an, statt zu
fragen, welche der beiden Einstellungen sich geändert hat. Das deckt alle Fälle der sofortigen
Änderung ab: Einschalten startet (ein Neustart eines nicht laufenden Dienstes ist ein Start),
Ausschalten stoppt, ein neuer Port bei laufendem Dienst bindet neu. Ein neuer Port bei
abgeschaltetem Dienst ruft einen Stopp auf einem nicht laufenden Dienst; der tut nichts, wie
zuvor die ausgebliebene Wirkung.
Zwischen verschiedenen Schlüsseln gibt es keine Abhängigkeit; ihre Reihenfolge ist die der
ersten Änderung und damit dieselbe, in der die sofortigen Änderungen sie bisher ausgelöst
hätten.
## Fäden
Alles läuft auf dem AWT-Ereignisfaden, wie bisher die sofortigen Änderungen: Knopfdruck,
Dialogwahl, Menüeintrag. Gewartet wird dort nie. Die Entscheidung — weiter, fragen —
fällt in `SettingsQuestion.settle` sofort auf dem Ereignisfaden, nicht im nächsten Bild des
Einstellungsfensters: ob ein minimiertes Compose-Fenster weiter Bilder erzeugt, ist ohne
laufende Oberfläche nicht prüfbar, und ein Beenden über das Hauptfenster darf davon nicht
abhängen. Ohne offene Änderungen geht es sofort weiter. Gibt es etwas zu fragen, wird das
Fenster nach vorn geholt und, falls minimiert, wiederhergestellt; erst danach braucht es ein
Bild, nämlich für den Dialog. Die Fortsetzung (Wechsel, Beenden) läuft aus dem Rückruf des
Dialogs, über `EventQueue.invokeLater`, nicht im Klick-Handler selbst.
## Prüfung
Pro Einstellung (`DraftedSettingsTableTest`, mit echtem Entwurf): unverändert nach der
Eingabe, geändert nach Übernehmen, unverändert nach Verwerfen. Pro Fenster: Speichern wendet
an und schreibt; X, Profilwechsel und Programmende fragen bei offenen Änderungen; Zurück
bricht ab; ungültiger Text und Fehlschläge halten das Fenster offen; Bearbeiten während einer
Sitzung geht.