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

14 KiB
Raw Blame History

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.