Files
kst4contest/docs/superpowers/specs/2026-10-01-javafx-entfernen-design.md
T

287 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Entwurf — JavaFX aus KST4Contest entfernen
Vertieft: `docs/superpowers/specs/2026-09-25-compose-migration-design.md`, Etappe 6,
zweite Hälfte („Entfernung von JavaFX").
Die erste Hälfte — die Karte als Compose-Canvas — ist umgesetzt und abgenommen
(Plan entfernt nach der Ausführung). Dieser Entwurf
behandelt ausschließlich, was danach noch zwischen dem Projekt und einem Build ohne
`org.openjfx` steht.
## Ziel
Release 1.50 ohne jede JavaFX-Abhängigkeit. Erfolg ist nachprüfbar:
- `grep -rl "javafx\." core/src/main app-desktop/src/main` liefert nichts.
- Der `org.openjfx`-Block ist aus `app-desktop/build.gradle.kts` und
`gradle/libs.versions.toml` entfernt.
- `./gradlew clean build` bleibt grün (Stand heute: 766 Tests, 0 Fehler).
- Die Anwendung verhält sich unverändert: Ton, Dialoge, Farben, Fenstergrößen.
- Offline-DEM, Terrain-Pakete und der Profil-Cache funktionieren unverändert
(Abnahmekriterium der übergeordneten Spezifikation).
## Der Befund, der die Etappe umkrempelt
Die naheliegende Lesart — „11.655 Zeilen `Kst4ContestApplication` nach Compose
portieren" — ist falsch.
**Das JavaFX-Hauptfenster wird bei jedem Start vollständig aufgebaut und nie
angezeigt.** `primaryStage.setScene(...)` und `primaryStage.show()` sind beide
auskommentiert (`Kst4ContestApplication.java:9508`, `:9517`). Angezeigt wird
`MainWindowHost.show(...)` (`:10667`), also Compose. Die Szene
`scn_ChatwindowMainScene` wird gebaut (`:7548`), thematisiert und mit
Größen-Listenern versehen, aber an keine Bühne gehängt.
Der Aufbau läuft trotzdem: 93 `new TableView` / `new TableColumn`, Kontextmenüs,
Tastaturhandler. Das kostet Startzeit und hält Listener, ohne je sichtbar zu werden.
Ein zweiter Irrweg, den dieser Entwurf ausräumt: die **CW-Tonausgabe** sieht nach
einem harten Blocker aus, weil `Kst4ContestApplication` `javafx.scene.media.Media`
und `MediaPlayer` benutzt und 91 MP3-Dateien im Projekt liegen. Diese Kopie ist tot —
`playCWLauncher` bei `:6966` ruft niemand. Die **lebende** Ausgabe sitzt in
`core/src/main/java/kst4contest/utils/PlayAudioUtils.java:182`, wird aus
`MessageBusManagementThread.java:1105` gerufen und spielt über JLayer
(`javazoom.jl.player.Player`). Kein JavaFX, keine Ersatzentscheidung, keine neue
Abhängigkeit.
## Die lebende JavaFX-Fläche
| Was | Wo | Ersatz |
|---|---|---|
| Anwendungs-Lebenszyklus | `extends Application`, `launch(args)` (`:9806`), `start(Stage)` (`:7356`) | Compose-Anwendung aus einem einfachen `main` |
| UI-Thread-Zustellung | `JavaFxUiDispatcher` → `Platform.runLater` | AWT-Ereignisfaden |
| Dialoge | 10 × `new Alert(...)` | eigene Compose-Fenster über `ComposeWindowHost` |
| Farbableitung | **bereits erledigt** — siehe Korrektur unten | — |
| Fensterrahmen | `Screen` (Startgröße), Anwendungssymbol | Compose / AWT |
| Kandidatenfenster | eigene `Scene` + `stage.show()` (`:4855`–`:4860`), geöffnet aus dem **Compose**-Pfad (`:10707`) | Compose-Fenster |
| Systemmenüleiste | `installSharedSystemMenuBar(candidatesScene)` (`:4858`), `initMenuBar()` (`:5729`) | hängt am Kandidatenfenster, siehe Etappe 1 |
| Roster-Spiegel | `FxRosterBinding` nutzt `ObservableList`/`FXCollections` | **Status ungeklärt — siehe Etappe 1** |
Dateien mit JavaFX-Importen unter `src/main`, nach Größe:
```
11655 Kst4ContestApplication.java 49 Importe
1777 StationMapView.java 20
1161 PathProfileChart.java 4
480 StationMapBridge.java 5
376 TimelineView.java 11
353 JavaFxStylesheet.kt 1
326 MessageTextTableCell.java 11
280 TableLayoutManager.java 9
136 ComposeMenuActions.java 1
129 FxRosterBinding.java 2
123 GuiUtils.java 4
110 MessageVariableResolverTest.java 1 (Testdatei im main-Quellbaum)
109 TruncatedTextTableCell.java 4
106 ApplicationRuntimeLauncher.java 2
50 LayoutAutosave.java 3
31 JavaFxUiDispatcher.java 1
```
## Der Dreh- und Angelpunkt
`core` kennt JavaFX nicht. Es kennt die Schnittstelle `UiDispatcher`, und
`JavaFxUiDispatcher` ist die einzige Umsetzung, die sie an den JavaFX-Faden bindet.
**Diese 31 Zeilen sind der Hebel.** Wird die Zustellung auf den AWT-Ereignisfaden
umgestellt — dort läuft Compose Desktop ohnehin —, fällt die Thread-Kopplung zwischen
`core` und JavaFX in einem Stück. Lebenszyklus, Dialoge und Fensterrahmen hängen
daran, nicht umgekehrt.
Der Vertrag bleibt derselbe: `runOnUi` stellt zu und führt sofort aus, wenn es bereits
auf dem UI-Faden läuft; `isUiThread` beantwortet dieselbe Frage; eine geworfene
Ausnahme darf den Faden nicht reißen. Diese drei Punkte sind testbar, und genau das
macht den Austausch beherrschbar.
## Vorgehen: erst löschen, dann umhängen
Abgewogen wurden drei Wege.
**Erst umhängen, dann löschen** wurde verworfen: der Eingriff am Lebenszyklus fände
genau dann statt, wenn die Datei am unübersichtlichsten ist. Das Nebenwirkungsrisiko
wäre dort am höchsten, wo es am schlechtesten zu sehen ist.
**Zweiter Einstiegspunkt daneben**, umschaltbar, wurde verworfen: zwei Lebenszyklen in
einem Prozess mit globalem Zustand (`ApplicationRuntimeLauncher`, Profilwechsel), und
der Schalter wird selbst zur Sache, die gepflegt und getestet werden muss.
**Gewählt: erst löschen, dann umhängen.** Der tote Aufbau ist das Volumen und zugleich
das Risikoärmste. Danach steht eine kleine, lesbare Datei, und erst darin die
schwierige Operation.
## Etappen
### 1 — Feststellen, was lebt
Keine Löschung. Ergebnis ist eine belegte Liste.
Zu klären ist für jeden Teil des nie gezeigten Aufbaus, ob er Nebenwirkungen hat, auf
die der laufende Betrieb angewiesen ist. Drei Verdachtsfälle sind benannt:
- **`FxRosterBinding`** (`chatMemberListBinding`, erzeugt bei `:2513`, genullt bei
`:6877`). Es spiegelt den Roster in eine `ObservableList`. Speist es noch etwas, das
die Compose-Oberfläche liest, oder nur die tote Tabelle?
- **`registerThemedScene`** — drei Aufrufe. Hängt die Themenumschaltung der
Compose-Fenster daran?
- **`LayoutAutosave`** (`:7420`) — schreibt es Layoutwerte, die auch die
Compose-Fenster benutzen?
Dazu die veralteten Begründungen, die beim Lesen auffallen: der Kommentar bei
`:10446` rechtfertigt eigene Spalten-IDs damit, dass „beide Fenster auf dem Bildschirm
sind". Das stimmt seit dem Abklemmen nicht mehr.
**Abnahme:** eine Liste, die jeden Block des toten Aufbaus einem von zwei Töpfen
zuordnet — „folgenlos löschbar" oder „zu entflechten", letzteres mit der Stelle, die
davon abhängt.
### 2 — Toten Aufbau löschen
In Scheiben, jede für sich gebaut und geprüft. Eine Scheibe ist ein zusammenhängender
Block: die Stationstabelle, die Nachrichtentabellen, die Kontextmenüs, die
Filterleiste, die Szene samt Listenern.
Mit fällt die tote CW-Ausgabe (`:6966` ff., `musicList`, `mediaPlayer`) und damit der
Import von `javafx.scene.media`. Die 91 MP3-Dateien in `core/src/main/resources`
**bleiben** — sie gehören der lebenden Ausgabe in `PlayAudioUtils`.
**Abnahme:** `./gradlew clean build` grün nach jeder Scheibe; die Anwendung startet und
verhält sich unverändert; `Kst4ContestApplication` ist auf die lebende Fläche
zusammengeschrumpft.
### 3 — Den `derive`-Pin einfrieren
**Korrektur gegenüber der ersten Fassung dieses Entwurfs.** Dort stand, `derive` rufe
JavaFX über Reflexion und müsse ersetzt werden. Das ist falsch, und die übergeordnete
Spezifikation ist an dieser Stelle inzwischen veraltet: `JavaFxStylesheet.derive`
(`JavaFxStylesheet.kt:131` ff.) ist eine vollständige, JavaFX-freie Umsetzung. Das
einzige Vorkommen von „javafx" in der Datei steht in einem Kommentar, der noch die
alte Reflexionsfassung beschreibt.
Mehr noch: die Umsetzung ist bereits gegen echtes JavaFX gepinnt.
`JavaFxStylesheetTest.kt:165`–`:170` ruft `com.sun.javafx.util.Utils.deriveColor` per
Reflexion **im Test** und prüft, dass beide Fassungen übereinstimmen (`:48`–`:50`).
Die Reflexion sitzt also dort, wo sie hingehört.
Was bleibt, ist klein und trotzdem zeitkritisch: **dieser Vergleich hört auf zu
funktionieren, sobald JavaFX weg ist.** Der Test fiele dann stillschweigend auf die
Prüfung gegen sich selbst zurück. Die von JavaFX gelieferten Werte müssen vorher als
Daten eingecheckt werden, damit der Pin das Entfernen überlebt.
Der veraltete Kommentar bei `:116`–`:129` wird mitkorrigiert.
Zur Einordnung: die übergeordnete Spezifikation hält fest, dass eine freihändige
Nachbildung in Etappe 3b nachweislich falsch war: die Helligkeit, von der JavaFX
ableitet, ist nicht der HSB-Wert, sondern `0.3R + 0.59G + 0.11B`, und der positive
Zweig folgte keiner naheliegenden Formel.
Vorgehen: solange JavaFX im Prozess ist, die Paare, die der Test heute zur Laufzeit
von `deriveColor` holt, über den benutzten Wertebereich erzeugen und als Testdaten
einchecken. Der Test vergleicht danach gegen diese Daten.
**Abnahme:** die von JavaFX gelieferten Werte liegen als eingecheckte Daten vor; der
Test prüft `derive` gegen diese Daten und nicht mehr gegen einen Reflexionsaufruf; er
bleibt grün, wenn JavaFX nicht auf dem Klassenpfad ist.
### 4 — `UiDispatcher` umhängen
`JavaFxUiDispatcher` → eine Umsetzung auf dem AWT-Ereignisfaden.
**Abnahme:** Tests decken Zustellung von einem Fremdfaden, sofortige Ausführung auf
dem UI-Faden, korrekte Thread-Erkennung und ab, dass eine geworfene Ausnahme den Faden
nicht reißt. Die Anwendung läuft unverändert.
Dies ist die risikoreichste Etappe: `core` stellt über diese Schnittstelle **jede**
UI-sichtbare Änderung zu. Deshalb steht sie hinter dem Aufräumen und vor dem
Lebenszyklus.
### 5 — Lebenszyklus, Dialoge, Rahmen
- `Kst4ContestApplication extends Application` und `launch(args)` weichen einem
einfachen `main`, das die Compose-Anwendung startet.
- Die zehn `Alert`-Aufrufe werden eigene Compose-Fenster über `ComposeWindowHost`,
modal, wo sie es heute sind: die Bestätigungen liefern eine Antwort, die den
weiteren Ablauf steuert, und müssen weiter blockieren.
- `Screen` für die Startgröße und das Anwendungssymbol gehen an Compose beziehungsweise
AWT.
- Das **Kandidatenfenster** (`:4855`–`:4860`) wird ein Compose-Fenster. Es ist die
einzige JavaFX-Bühne, die tatsächlich erscheint, und sie wird aus dem Compose-Pfad
geöffnet (`:10707`): der „more"-Knopf neben den Prioritätsknöpfen. Solange es steht,
kann keine JavaFX-Abhängigkeit fallen.
**Abnahme:** die Anwendung startet ohne JavaFX-Bühne; jeder der zehn Dialoge erscheint
an derselben Stelle im Ablauf wie bisher und mit derselben Wirkung auf den Ablauf.
### 6 — Reste löschen und den Build befreien
- `StationMapView.java`, `StationMapBridge.java`, `MapHtmlResources.java` und die
Leaflet-Ressourcen unter `app-desktop/src/main/resources/web/leaflet/`.
- `PathProfileChart.java` und `TimelineView.java` — Compose-Gegenstücke existieren.
- `MessageTextTableCell.java`, `TruncatedTextTableCell.java` — kein Compose-Nutzer.
- `JavaFxUiDispatcher.java` — fällt in jedem Fall, sein Ersatz entsteht in Etappe 4.
- `FxRosterBinding.java`, `GuiUtils.java`, `LayoutAutosave.java`, soweit Etappe 1 sie
als entbehrlich ausweist.
- `MessageVariableResolverTest.java` liegt im **main**-Quellbaum und gehört nach
`src/test`.
- `app-desktop/build.gradle.kts` Zeilen 3, 9–11 und 19; `gradle/libs.versions.toml`
Zeilen 2–3 und 28.
**Eine Falle:** `TableLayoutManager.java` sieht genauso tot aus, wird aber von sechs
Compose-Dateien benutzt. Er muss seine JavaFX-Importe verlieren, nicht gelöscht werden.
`TileProxyServer` in `core` verliert mit `StationMapView` seinen einzigen Nutzer. Er
war eine Antwort auf TLS-Probleme der JavaFX-WebView in AppImage- und
Flatpak-Verpackung; der Compose-Canvas holt seine Kacheln selbst. Mit entfernen.
**Abnahme:** die drei Prüfungen aus dem Abschnitt „Ziel".
## Prüfung
Das Netz, auf das diese Etappe sich stützt, ist seit heute belastbar: `./gradlew clean
build` ist grün (766 Tests, 0 Fehler), erstmals seit dem 30. September. Jede
Rotfärbung während dieser Arbeit ist damit ein Signal und kein Rauschen.
Dazu kommen:
- die gepinnte `derive`-Tabelle (Etappe 3),
- echte Tests für den neuen Dispatcher (Etappe 4),
- die Compose-Layouttests aus der Kartenetappe, die die Fenster von 900×600 bis 1×1 px
abdecken,
- nach jeder Löschscheibe ein Start der Anwendung, weil kein Test beantwortet, ob ein
entfernter Listener gefehlt hat.
## Was unverändert bleibt
- Die Fachlogik in `core` und sämtliche Protokollformate.
- Die **Schnittstelle** `UiDispatcher`; nur ihre Umsetzung wird getauscht.
- Die Invarianten aus `AGENTS.md`: `ObservableRoster` ist der Speicher und die Listen
sind Spiegel; kein Worker-Faden fasst UI-Sammlungen an; Zoom, Auswahl, Fokus,
Sortierung und vorbelegter Text ändern sich nicht als Nebeneffekt.
- Die 91 MP3-Dateien und die CW-Ausgabe in `PlayAudioUtils`.
- Verpackung und Vertrieb, soweit sie nicht am JavaFX-Modulpfad hängen.
## Risiken
**Etappe 1 kann das Vorgehen kippen.** Ergibt die Prüfung, dass der „tote" Aufbau
lebende Nebenwirkungen hat, wird aus Löschen ein Entflechten und die Etappe wird
länger. Das ist der Grund, warum die Prüfung eine eigene Etappe ist und keine Annahme
im Plan.
**Der Dispatcher-Tausch wirkt überall.** Ein Fehler darin zeigt sich nicht als
Übersetzungsfehler, sondern als sporadisch nicht aktualisierte Oberfläche. Deshalb die
Tests für den Vertrag und nicht nur für die Anwendung.
**Die Dialoge sind Ablaufsteuerung, keine Dekoration.** Wer eine Bestätigung
nicht-blockierend macht, ändert den Ablauf. Jeder der zehn Aufrufe ist einzeln zu
lesen.
## Offene Punkte
- Der Status von `FxRosterBinding` ist Gegenstand von Etappe 1 und wird hier bewusst
nicht vorweggenommen.
- Ob `ApplicationRuntimeLauncher` und `ComposeMenuActions` nach dem Wegfall der Bühne
noch eine JavaFX-Berührung behalten, entscheidet sich in Etappe 5.
- Die macOS-Systemmenüleiste ist eine JavaFX-`MenuBar` (`:172`, `:5729`) und wird auf
die Szene des Kandidatenfensters gesetzt (`:4858`). Ob sie darüber hinaus an etwas
hängt — und was von ihr auf macOS tatsächlich sichtbar ist, seit das Hauptfenster
Compose ist — gehört in Etappe 1.