mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-19 13:53:01 +02:00
Etappen 3 to 6a of the migration (129 commits). Every window the operator sees is Compose; the JavaFX ones still exist beside them so the two could be compared against a live session, which is the only way the differences came to light. The toolchain first, then the settings window with its eleven tabs and 72 real preferences; then the multi-column table as a component, and with it the rules it needs -- stable row identity, a selection that survives a refresh, column order the operator can drag, and a repaint when the rows change rather than when the list instance does. Then the update and monitor windows, and the main window: 22 station columns, the filter bar, the selected-station panel, the message tabs, the priority bar, the sked controls, the status indicators and the timeline, assembled behind a splitter because Compose has none. The map stops being a WebView. A Compose canvas projects WebMercator tiles itself, with the clustering in core and an asynchronous fetcher in place of the local tile proxy; derive() becomes a pure Kotlin reimplementation instead of a reflection call into com.sun.javafx. Most of the volume here is parity the operator would notice rather than new behaviour: desktop density instead of Material's phone minimums, colours taken from the JavaFX stylesheets instead of Material's palette, row dividers, band columns, grid-square labels and their colours, the home marker and antenna beam, pinch-to-zoom under Wayland, and layouts that survive a window narrower than the chart they stack. Two lessons are worth more than the diff. A control bound to a value Compose cannot observe writes through and never redraws -- that one shipped repeatedly with a green build, because no test drew anything. And a layout that clips instead of wrapping is worse than one that is merely too narrow: half a word is the worst available outcome.
491 lines
32 KiB
Markdown
491 lines
32 KiB
Markdown
# Konzept: Ablösung von JavaFX durch Compose Multiplatform
|
|
|
|
> Status: Konzept, noch nicht umgesetzt. Zielrelease **1.50**. Alle Zeilenanker beziehen sich auf `nextMajorRelease/version1_50` und wurden gegen den Branch verifiziert (Stand 2026-09-25).
|
|
|
|
## Ziel
|
|
|
|
KST4Contest verlässt JavaFX und wird auf Compose Multiplatform umgestellt. Ergebnis ist eine wartbare, optisch aufgefrischte Desktop-Oberfläche bei **unverändertem Funktionsumfang**. Der fachliche Kern wird dabei von jeder UI-Technologie gelöst.
|
|
|
|
Drei Treiber, gleichrangig:
|
|
|
|
1. Die Oberfläche soll modern aussehen und sich modern anfühlen.
|
|
2. Der UI-Code soll wieder änderbar sein.
|
|
3. Die JavaFX-Abhängigkeit soll verschwinden.
|
|
|
|
## Nicht-Ziele
|
|
|
|
- **Kein UX-Neuentwurf.** 1.50 ist ein funktionsgleicher Port. Bedienabläufe, Fensteraufteilung und Funktionsumfang bleiben erhalten. Ein echter Neuentwurf der Bedienung ist Thema für 2.0.
|
|
- **Keine Mobile-App.** Die Modulgrenze wird so gezogen, dass ein späteres Mobile-Repo den Kern abgreifen könnte. Mehr nicht. Mobile ist in diesem Konzept Randbedingung, kein Lieferergebnis.
|
|
- **Keine Kotlin-Konvertierung des Kerns.** Siehe „Bestätigte Festlegungen".
|
|
- **Keine Protokolländerungen.** Kein Framing, kein Port, kein Transport, keine Normalisierung wird angefasst.
|
|
|
|
## Ausgangslage
|
|
|
|
Gemessen am Branchstand:
|
|
|
|
| Kennzahl | Wert |
|
|
|---|---|
|
|
| Java gesamt | 61.572 Zeilen, 196 Dateien |
|
|
| Kotlin | 0 Dateien |
|
|
| Dateien mit direktem JavaFX-Bezug | 33 |
|
|
| `Kst4ContestApplication.java` | 13.564 Zeilen, 387 Methoden |
|
|
| davon reines JavaFX-Gerüst | 82 `call()`-Cell-Factories, 64 `changed()`-Listener, 77 `handle()`-Eventhandler |
|
|
| `Platform.runLater` / `ObservableList` | 51 / 93 Vorkommen |
|
|
| Tests | 43 Dateien, 148 Testmethoden |
|
|
|
|
### Die JavaFX-Kopplung ist flacher als die Zahlen vermuten lassen
|
|
|
|
- `ChatMember`: **ein** gekoppeltes Feld, `StringProperty frequency` (`ChatMember.java:42`, Zugriff `:510`, `:516`, Neuanlage `:616`). Der Rest der Klasse ist reines Java.
|
|
- `ChatPreferences`: fünf Properties (`actualQTF` `:228`, `MYQRGFirstCat` `:266`, `MYQRGSecondCat` `:268`, `notify_optionalFrequencyPrefix` `:296`, `notify_DXCSrv_SpottersCallSign` `:298`) und vier `ObservableList<String>` für Listeneinstellungen (`:307`, `:308`, `:324`, `:325`).
|
|
- `ScoreService`: bereits sauber getrennt. Es existiert ein Snapshot-Modell, `topCandidatesFx` (`:45`, `:62`) ist explizit als FX-Projektion benannt, und der Klassenkommentar hält „No per-member Platform.runLater flooding" fest.
|
|
- `MessageBusManagementThread`: eine einzige Methodensignatur nimmt `ObservableList` entgegen (`:577`).
|
|
- `ChatController` ist der Brocken: sieben `ObservableList`-Felder (`:1164`, `:1742`, `:1759`, `:1768` mit der synchronisierten Hülle `:1775`, `:1776`, `:1777`, `:1817`) und eine `FilteredList`/`SortedList`-Pipeline (`:1745`, `:1749`, `:1751`, `:1756`, `:1772`, `:1773`).
|
|
|
|
Die Klasse dokumentiert ihr eigenes Muster bereits: der Kommentar ab `ChatController.java:1783` begründet, warum eine threadsichere Map die primäre Quelle ist und die `ObservableList` nur die gebündelte Projektion. Die hier vorgeschlagene Trennung setzt das fort, statt es zu erfinden.
|
|
|
|
### Die Karte hängt nur flach an Leaflet
|
|
|
|
`MapHtmlResources.java` nutzt aus der Leaflet-API lediglich `L.map`, `L.tileLayer`, `L.marker`, `L.divIcon`, `L.circleMarker`, `L.polyline`, `L.polygon`, `L.rectangle`, `L.layerGroup`, `L.latLng` und `L.latLngBounds`. Geometrie (`PathGeometryUtils`), Gitterplanung (`MaidenheadGridRenderPlanner`), Kachelbeschaffung (`TileProxyServer`), Terrain und Pfadanalyse liegen bereits vollständig in Java.
|
|
|
|
### Weitere betroffene Stellen
|
|
|
|
- `javafx.scene.media` wird ausschließlich in `Kst4ContestApplication.java` benutzt (`:33`, `:34`, `:6599`, `:6900`). MP3-Wiedergabe läuft ohnehin schon über `jlayer` in `PlayAudioUtils`.
|
|
- Lombok ist in `pom.xml` als Abhängigkeit deklariert, wird aber in keiner Quelldatei verwendet.
|
|
- `src/main/java/module-info.java` bindet das Projekt an JPMS; `pom.xml` enthält eine Enforcer-Regel, die die jpackage-Modulliste gegen Drift absichert.
|
|
|
|
## Bestätigte Festlegungen
|
|
|
|
| Punkt | Entscheidung |
|
|
|---|---|
|
|
| Zielrelease | 1.50. Die Compose-Migration findet auf `nextMajorRelease/version1_50` statt; 1.50 wird das Compose-Release |
|
|
| Funktionsumfang | Funktionsgleicher Port. UX-Neuentwurf frühestens 2.0 |
|
|
| Sprache Kern | `core` bleibt **Java 21**. Kotlin nur dort, wo Compose es erzwingt |
|
|
| Sprache UI | Kotlin, `app-desktop` |
|
|
| Build | Maven wird durch **Gradle** ersetzt. Compose Multiplatform wird offiziell nur mit Gradle unterstützt, Desktop-Packaging inbegriffen |
|
|
| Modulsystem | `module-info.java` entfällt. Compose Multiplatform liefert keine echten JPMS-Module |
|
|
| Packaging | Etappe 1 behält den direkten `jpackage`-Aufruf, nur aus Gradle statt aus Maven. Die Umstellung auf `compose.desktop.nativeDistributions` erfolgt in Etappe 3, wenn Kotlin und Compose ohnehin einziehen. DMG-Drag-Layout, Icon-Erzeugung, Flatpak und AUR werden dabei nachgezogen |
|
|
| Karte | Native Compose-Canvas-Umsetzung. Kein eingebettetes Chromium (KCEF/JCEF) |
|
|
| God-Klasse | `Kst4ContestApplication` wird in Phase 1 **nicht** zerschnitten. Compose löscht den Widget-Code ohnehin |
|
|
| Mobile | Nur als Modulgrenze berücksichtigt, nicht umgesetzt |
|
|
| Lombok | Entfällt (ungenutzt) |
|
|
| Entwickler | Marc (DO5AMF) und Philipp (DN9APW). Kein Termin |
|
|
|
|
## Zielarchitektur
|
|
|
|
```
|
|
kst4contest/
|
|
├── core/ Java 21, KEIN JavaFX, KEIN Compose
|
|
│ ├── model/ ChatMember, ChatMessage, ChatPreferences, Band …
|
|
│ ├── controller/ ChatController, MessageBus, Score, Reachability …
|
|
│ ├── net/ ON4KST, AirScout, Win-Test, UCXLog, PSTRotator, DXCluster
|
|
│ ├── map/ Geometrie, Terrain, Kachel-Proxy, Pfadanalyse
|
|
│ ├── audio/ Wiedergabe hinter einem Interface
|
|
│ └── observe/ Beobachtungs-Abstraktion
|
|
└── app-desktop/ Kotlin + Compose Multiplatform
|
|
├── ui/ Fenster, Panels, Tabellen, Dialoge
|
|
├── state/ UI-Zustand, Projektionen aus core
|
|
└── map/ Compose-Canvas-Karte
|
|
```
|
|
|
|
`core` hängt von keiner UI-Technologie ab. Das ist die Grenze, an der parallel gearbeitet werden kann, und dieselbe Grenze, die ein späteres Mobile-Repo abgreifen würde.
|
|
|
|
### Das Paket `core/observe`
|
|
|
|
Bewusst minimal. Kein Reaktiv-Framework, keine Bibliothek.
|
|
|
|
- **`ObservableValue<T>`** — `T get()`, `void addListener(Consumer<T>)`, `void removeListener(Consumer<T>)`.
|
|
- **`MutableValue<T> extends ObservableValue<T>`** — zusätzlich `void set(T)`.
|
|
- **`ObservableRoster<T>`** — **snapshotbasiert**: `List<T> snapshot()` liefert eine unveränderliche Liste; Listener erhalten die neue Liste. Es gibt **kein** inkrementelles Änderungsprotokoll. Das passt zum Neuzeichnungsmodell von Compose und ist ohne UI testbar.
|
|
- **`UiDispatcher`** — `void runOnUi(Runnable)` und `boolean isUiThread()`.
|
|
|
|
`UiDispatcher` ist der Schlüssel der gesamten Umstellung. In Phase 1 wird er von `Platform::runLater` und `Platform::isFxApplicationThread` implementiert, in Phase 2 vom Compose-Main-Dispatcher. Die Invariante aus `AGENTS.md` bleibt dabei wortgleich gültig: Worker-Threads mutieren keine UI-gebundenen Sammlungen, UI-sichtbare Änderungen laufen über den Dispatcher.
|
|
|
|
### Was aus der JavaFX-Pipeline wird
|
|
|
|
`FilteredList` und `SortedList` werden zu einer **reinen Funktion** in `core`: aus Mitgliederliste, Filterprädikaten und Komparator entsteht eine unveränderliche Ergebnisliste. Die Stationslisten-Logik wird damit erstmals ohne laufende UI testbar. Das ist der wesentliche fachliche Gewinn von Phase 1.
|
|
|
|
Die vorhandene Bündelung und Entprellung (`flushPendingChatMessagesToUi`, das Coalescing der Mitgliederliste) **bleibt erhalten**. Sie ist Leistungsschutz bei mehreren tausend ON4KST-Nutzern, kein JavaFX-Beiwerk.
|
|
|
|
## Etappen
|
|
|
|
Jede Etappe ist für sich abgeschlossen, lieferbar und bekommt einen eigenen Umsetzungsplan.
|
|
|
|
### Etappe 1 — Gradle, zwei Module, JPMS entfernen, Packaging
|
|
|
|
**Ziel:** Die unveränderte JavaFX-Anwendung baut, testet und paketiert unter Gradle.
|
|
|
|
**Umfang**
|
|
|
|
- Gradle-Wrapper ersetzt `mvnw`/`mvnw.cmd`.
|
|
- Aufteilung in die Module `core` und `app-desktop`; in dieser Etappe enthält `app-desktop` noch die JavaFX-Oberfläche.
|
|
- **`core` darf in dieser Etappe noch von JavaFX abhängen.** `ChatController`, `ChatMember`, `ChatPreferences` und `ScoreService` benutzen es; Etappe 2 entfernt die Abhängigkeit. Etappe 1 bleibt damit eine reine Build-Umstellung ohne fachliche Änderung.
|
|
- Paketnamen und `import`-Anweisungen ändern sich nicht. Gradle-Module erzwingen keine Paketumbenennung; die Aufteilung ist ein Verschieben von Verzeichnissen.
|
|
- `src/main/java/module-info.java` wird entfernt; die Enforcer-Regel zur jpackage-Modulliste in `pom.xml` entfällt mit ihr.
|
|
- SpotBugs- und PMD-Regelsätze wandern mit; `pmd-ruleset.xml` bleibt unverändert gültig.
|
|
- Lombok wird aus den Abhängigkeiten entfernt.
|
|
- Packaging für Windows, Linux und macOS ruft `jpackage` weiterhin direkt auf, künftig aus einer Gradle-Aufgabe. Das DMG-Drag-Layout, die Icon-Erzeugung, Flatpak und AUR bleiben inhaltlich unverändert; nur die Herkunft der Eingabedateien wechselt von `target/dist-libs` auf `app-desktop/build/dist-libs`.
|
|
- Die Projektversion wandert nach `gradle.properties`. Zehn Stellen in fünf Workflows und zwei PKGBUILDs lesen sie heute aus `pom.xml` und müssen umgestellt werden, bevor `pom.xml` entfällt.
|
|
- Die GitHub-Workflows unter `.github/` werden auf Gradle umgestellt.
|
|
|
|
**Abnahmekriterium:** Alle 148 vorhandenen Tests laufen unter Gradle. Auf allen drei Plattformen entsteht ein installierbares Paket, das sich wie das bisherige verhält. Stable-, Beta- und Nightly-Strecke funktionieren unverändert.
|
|
|
|
### Etappe 2 — `core` von JavaFX befreien
|
|
|
|
**Ziel:** `core` compiliert ohne JavaFX auf dem Klassenpfad. Die JavaFX-Oberfläche läuft unverändert und beweist, dass die Schnittstelle trägt.
|
|
|
|
**Umfang**
|
|
|
|
- `core/observe` entsteht wie oben beschrieben.
|
|
- `ChatMember.frequency` wird von `StringProperty` auf `MutableValue<String>` umgestellt (`ChatMember.java:42`, `:510`, `:516`, `:616`).
|
|
- Die fünf Properties und vier `ObservableList<String>` in `ChatPreferences` werden auf `MutableValue` beziehungsweise `ObservableRoster` umgestellt.
|
|
- Die sieben `ObservableList`-Felder in `ChatController` werden zu `ObservableRoster`; die `FilteredList`/`SortedList`-Pipeline wird durch die reine Filter-/Sortierfunktion ersetzt.
|
|
- `ScoreService` gibt seinen Snapshot über `ObservableRoster` heraus statt über `topCandidatesFx`; `Platform.runLater` wird durch `UiDispatcher` ersetzt.
|
|
- `MessageBusManagementThread.checkListForChatMemberIndexByCallSign` (`:577`) nimmt eine `List<ChatMember>` statt `ObservableList`.
|
|
- Audio wandert nach `core/audio` hinter ein Interface. `javafx.scene.media` (`Kst4ContestApplication.java:33`, `:34`, `:6599`, `:6900`) entfällt; MP3 über `jlayer`, WAV über `javax.sound.sampled`.
|
|
- `app-desktop` stellt die JavaFX-Implementierung von `UiDispatcher` bereit und adaptiert `ObservableRoster` auf `ObservableList` für die bestehenden `TableView`.
|
|
|
|
**Absicherung:** Jede herausgelöste Einheit bekommt **vorher** einen Test, der das heutige Verhalten festschreibt — insbesondere Filterung und Sortierung der Stationsliste, die heute in `FilteredList`/`SortedList` steckt und nicht testbar ist.
|
|
|
|
**Abnahmekriterium:** `core` hat keine JavaFX-Abhängigkeit mehr. Die Anwendung verhält sich unverändert: Stationsliste, Chat-Tabs, Prioritäten, Skeds, Cluster, Worked-Zustand, Profilwechsel.
|
|
|
|
### Etappe 3 — `DataTable` und Einstellungsfenster in Compose
|
|
|
|
**Ziel:** Das erste Compose-Fenster ist im Produkt.
|
|
|
|
**Umfang**
|
|
|
|
Der wesentliche Baustein dieser Etappe ist eine wiederverwendbare **`DataTable`**-Komponente. Compose kennt kein `TableView`, und die Anwendung hat fünf Tabellen: `TableView<ChatMember>`, `TableView<ChatMessage>`, `TableView<ClusterMessage>`, `TableView<String>` und die Top-Prioritätenliste. Alle brauchen dasselbe:
|
|
|
|
- Spaltenbreiten mit stabilen Tabellen- und Spalten-IDs, wie sie `preferences.xml` Version 7 bereits kennt;
|
|
- Sortierung nach Spalte;
|
|
- Zeilenauswahl und Kontextmenü;
|
|
- Zeilen- und Zell-Styling, heute `PrivateMessageRowStyleResolver`, `MessageTextTableCell`, `TruncatedTextTableCell`, `TruncatedTextTooltipSupport`.
|
|
|
|
Umgesetzt als `LazyColumn` mit eigener Kopfzeile. Wird diese Komponente gut, ist der Rest der Portierung Fleißarbeit; wird sie schlecht, zieht sich das durch die gesamte Anwendung.
|
|
|
|
Mit dieser Etappe ziehen Kotlin und das Compose-Gradle-Plugin ein. Das Packaging wird dabei von den direkten `jpackage`-Aufrufen auf `compose.desktop.nativeDistributions` umgestellt — der Zeitpunkt ist hier richtig, weil JavaFX dann nicht mehr über `jlink` gebündelt werden muss.
|
|
|
|
Danach das Einstellungsfenster mit seinen zwölf Reitern: Station, Log synch, TRX synch, Airscout, Notification, Shortcuts, Macros, Beacon, Messagehandling, Workedstn database, GUI, Profiles. Es ist in sich abgeschlossen, hat wenig Domänenkopplung und eignet sich deshalb als Lernstück.
|
|
|
|
**Abnahmekriterium:** Das Einstellungsfenster ist vollständig in Compose, alle zwölf Reiter sind funktionsgleich, und geänderte Einstellungen landen unverändert in `preferences.xml`.
|
|
|
|
> Nachzuschärfen nach Etappe 2: der genaue Zuschnitt der `DataTable`-Schnittstelle. Er hängt davon ab, wie sich `ObservableRoster` in der Praxis anfühlt.
|
|
|
|
### Etappe 4 — Update-Fenster und Monitor-Fenster
|
|
|
|
**Ziel:** Die zwei letzten Nebenfenster portiert; die Muster aus Etappe 3 sind an
|
|
Live-Daten bestätigt.
|
|
|
|
**Umfang zurechtgerückt nach Etappe 3b.** Die Spezifikation nannte hier ursprünglich
|
|
vier Dinge. Zwei sind bereits erledigt und entfallen:
|
|
|
|
| ursprünglich genannt | Stand |
|
|
|---|---|
|
|
| `OperatorProfilePickerDialog` | in Etappe 3a nach Compose portiert |
|
|
| `OperatorProfileSettingsPane` | gelöscht, ersetzt durch `ProfilesTab` (Etappe 3b) |
|
|
| Update-Fenster | offen, `Kst4ContestApplication.java:9458` |
|
|
| Monitor-Fenster für Cluster und QSOs | offen, `:9396` |
|
|
|
|
- Das **Update-Fenster** ist das einfachere: ein Textfeld, ein Fortschritt, zwei
|
|
Knöpfe. Es prüft vor allem, ob ein Compose-Fenster aus einem Hintergrundfaden
|
|
heraus sauber auf- und zugeht.
|
|
- Das **Monitor-Fenster** ist der eigentliche Prüfstein: es belastet die `DataTable`
|
|
erstmals mit **Live-Daten**. Genau dort saßen in Etappe 2 die Sortier- und
|
|
Fokusfehler, und zwar nicht in der Tabelle selbst, sondern in der Kopplung
|
|
zwischen Roster und Anzeige.
|
|
|
|
Die Layout-IDs für das Monitor-Fenster bleiben von denen des Hauptfensters getrennt,
|
|
wie in `PROJECT_CONTEXT.md` festgelegt.
|
|
|
|
**Abnahmekriterium:** Beide Fenster sind funktionsgleich. Fenstergrößen, Positionen
|
|
und Spaltenbreiten werden unverändert entprellt gespeichert und beim Herunterfahren
|
|
geschrieben. Das Monitor-Fenster hält eine Stunde Dauerlast aus dem Simulator aus,
|
|
ohne dass die Auswahl springt oder die Sortierung kippt.
|
|
|
|
> Nach Etappe 3b gelernt und hier anzuwenden: Ein Bedienelement, das an einem Wert
|
|
> hängt, der kein Compose-Zustand ist, zeichnet sich nicht neu. Bei Live-Daten fällt
|
|
> das nicht als „reagiert nicht" auf, sondern als veraltete Anzeige — schwerer zu
|
|
> bemerken und schlimmer in der Wirkung.
|
|
|
|
### Etappe 5 — Hauptfenster
|
|
|
|
**Ziel:** JavaFX wird nur noch für die Karte benötigt.
|
|
|
|
**Umfang**
|
|
|
|
- Stationsliste mit Filterleiste und Prioritätenspalten.
|
|
- Die drei Chat-Reiter „Public messages", „DXCluster messages", „QSO of the other".
|
|
- Der Info-Bereich zur ausgewählten Station (`generateFurtherInfoAbtSelectedCallsignBP`, `Kst4ContestApplication.java:707`) samt seinem vertikalen `SplitPane` und dessen gespeicherten Divider-Positionen.
|
|
- Die Top-Prioritätenliste (`initTopPriorityListPane`, `:4416`).
|
|
- Die Statusleiste mit ihren Indikatoren: Sked-Warnung (`:5868`), Verbindungszustand (`:5887`), Band-Upgrade (`:6045`).
|
|
- `TimelineView`.
|
|
- Menüleiste, einschließlich der macOS-Systemmenüleiste.
|
|
|
|
**Profilwechsel:** Der heutige Mechanismus reißt die Laufzeit ab und baut eine **neue** `Kst4ContestApplication`-Instanz auf, weil viele Bedienelemente inline initialisierte Instanzfelder sind. Dieser Grund entfällt in Compose — der Zustand hängt nicht mehr an Feldern einer Riesenklasse. Der Wechsel wird zum Austausch des Zustandsobjekts. `shutdownRuntime()` bleibt trotzdem verantwortlich für alles, was eine Verbindung überdauert: ON4KST-Supervisor, Sked-Erinnerung, Reachability-Executor, PSTRotator-Wiederholung, Kachel-Proxy, Zeitgeber. `ApplicationConstants.sessionRuntimeUniqueId` wird weiterhin nicht neu erzeugt.
|
|
|
|
**Abnahmekriterium:** Das Hauptfenster ist funktionsgleich. Der Profilwechsel im laufenden Betrieb funktioniert, ohne Listener doppelt zu registrieren und ohne Ressourcen zu halten.
|
|
|
|
> Nachzuschärfen nach Etappe 4: ob `Platform.setImplicitExit(false)` und `ApplicationRuntimeLauncher.exitApplication()` in der Compose-Fassung noch eine Entsprechung brauchen.
|
|
|
|
### Etappe 6 — Karte und Entfernung von JavaFX
|
|
|
|
**Ziel:** 1.50. Keine JavaFX-Abhängigkeit mehr im Projekt.
|
|
|
|
**Umfang**
|
|
|
|
Die Karte wird als Compose-Canvas neu umgesetzt:
|
|
|
|
- Web-Mercator-Projektion, Zoomstufen und Schwenken wie bisher;
|
|
- Kacheln über den bestehenden `TileProxyServer`;
|
|
- Marker, Linien, Polygone, Rechtecke und Kreismarker direkt gezeichnet, entsprechend der heutigen flachen Leaflet-Nutzung;
|
|
- Maidenhead-Gitter über den bestehenden `MaidenheadGridRenderPlanner`;
|
|
- Pfaddarstellung und Geländeprofil (`PathProfileChart`) als Compose-Zeichnung;
|
|
- bildschirmbasiertes Clustering als Vorschalt-Schritt vor dem Zeichnen. `GUIstationMapClusteringEnabled` bleibt eine Layout-Einstellung unterhalb `guiOptions` mit Vorgabewert `true`.
|
|
|
|
`MapHtmlResources`, `StationMapBridge` und die JavaScript-Brücke entfallen ersatzlos. `StationMapView` wird zur Compose-Komponente.
|
|
|
|
Zum Abschluss werden die JavaFX-Abhängigkeiten aus dem Build entfernt und `opens kst4contest.view.map to javafx.web` verliert seinen Gegenstand.
|
|
|
|
**Eine Abhängigkeit, die hier zerbricht.** `JavaFxStylesheet.derive` ruft heute
|
|
`com.sun.javafx.util.Utils.deriveColor` über Reflexion auf, weil eine 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 der
|
|
naheliegenden Formeln. Mit dem Entfernen von JavaFX greift die Rückfallnäherung — und
|
|
die ist messbar daneben. **Vor dem Entfernen** muss `derive` durch eine eigene,
|
|
gegen JavaFX gepinnte Umsetzung ersetzt werden, sonst verschieben sich alle
|
|
abgeleiteten Farben still: die Fensterfläche und die Innenfläche jedes Eingabefelds.
|
|
|
|
**Abnahmekriterium:** Die Karte zeigt dieselben Informationen wie die Leaflet-Fassung. Offline-DEM, Terrain-Pakete und der Profil-Cache funktionieren unverändert. Im Projekt existiert keine JavaFX-Abhängigkeit mehr.
|
|
|
|
> Nachzuschärfen nach Etappe 5: Leistungsverhalten des Canvas bei mehreren tausend Markern. Falls das Zeichnen nicht trägt, ist eine Vorabaggregation der Marker im Kern der Ausweg.
|
|
|
|
### Etappe 7 — Compose-Oberflächentests
|
|
|
|
**Ziel:** Die Zeichenebene bekommt ein Netz. Die Fehlerklasse, die in Etappe 3b
|
|
ausschließlich durch Hinsehen gefunden wurde, fällt künftig im Bau auf.
|
|
|
|
**Warum am Ende**
|
|
|
|
Die Oberfläche ändert sich durch Etappe 4 bis 6 weiter. Tests, die heute gegen die
|
|
Fenster aus Etappe 3b geschrieben würden, wären dreimal neu zu schreiben. Die
|
|
Beweislage wird jetzt festgehalten, die Tests entstehen, wenn die Flächen stehen.
|
|
|
|
**Beweislage: was in Etappe 3b kein Test gesehen hat**
|
|
|
|
Sämtliche folgenden Fehler standen gleichzeitig mit einem grünen Bau aus 382
|
|
Testfällen im Zweig. Keiner davon zeichnet, deshalb sah keiner etwas.
|
|
|
|
| Klasse | Erscheinung in Etappe 3b |
|
|
|---|---|
|
|
| Ein Bedienelement hängt an einem nicht beobachtbaren Wert und zeichnet sich nicht neu | Jeder Tastendruck in den Einstellungen wurde durchgeschrieben und nicht angezeigt; die Auswahl im Profil-Reiter wanderte nicht mit; die abgewählte Listenzeile behielt ihren Akzentrahmen |
|
|
| Ein Layout schneidet ab, statt umzubrechen | Der Reiterstreifen schnitt „Workedstn database" mitten im Wort ab; die Knopfleiste quetschte „Disconnect & close Chat" zu einer 540px hohen Farbsäule |
|
|
| Inhalt ist höher als das Fenster und unerreichbar | Der Reiterinhalt endete mitten im Formular, ohne Hinweis, dass mehr folgt |
|
|
| Farben stammen nicht aus der Stilvorlage | Die Fensterfläche nahm `-fx-base` statt `derive(-fx-base, 26.4%)`; der Akzent war Material-Violett statt des Grüns aus `.text-field .text` |
|
|
|
|
**Umfang**
|
|
|
|
- `org.jetbrains.compose.ui:ui-test-desktop` als Testabhängigkeit, benutzt über
|
|
`runComposeUiTest { }`. **Keine** JUnit-4-Brücke: das Projekt bleibt auf Jupiter,
|
|
und `runComposeUiTest` ist aus einem gewöhnlichen `@Test` heraus aufrufbar. Die
|
|
Funktion ist als `@ExperimentalTestApi` gekennzeichnet; die Einwilligung wird an
|
|
einer Stelle gebündelt und begründet.
|
|
- Je ein Rückfalltest für die vier Klassen oben, geschrieben gegen die dann
|
|
aktuellen Fenster.
|
|
- Die beiden Abdeckungsnetze aus Etappe 3b bleiben bestehen. Sie prüfen
|
|
**Text** — dass ein Setzer irgendwo aufgerufen wird. Diese Tests prüfen
|
|
**Verhalten** — dass der Wert auf dem Schirm ankommt. Das eine ersetzt das andere
|
|
nicht.
|
|
|
|
**Abnahmekriterium**
|
|
|
|
Für jede der vier Klassen wird der ursprüngliche Fehler **absichtlich wieder
|
|
eingebaut**, und der zugehörige Test muss fehlschlagen. Eine Prüfsammlung, die den
|
|
Fehler nicht fängt, für den sie geschrieben wurde, ist wertlos; das ist die einzige
|
|
Abnahme, die hier etwas aussagt.
|
|
|
|
> Zu klären vor Beginn: ob die Tests ohne Bildschirm laufen. Skiko zeichnet in einer
|
|
> Softwarefassung, aber das ist für diesen Bau nicht erprobt. Falls sie einen
|
|
> Bildschirm brauchen, laufen sie in der CI unter Xvfb oder gar nicht — dann wären
|
|
> sie nur lokal etwas wert, und das gehört vorher entschieden, nicht nachher
|
|
> festgestellt.
|
|
|
|
### Etappe 8 — Farben anpassbar machen
|
|
|
|
**Ziel:** Der Operateur bestimmt die Farben seines Clients, ohne Dateien im
|
|
Installationsverzeichnis anzufassen.
|
|
|
|
**Ausgangslage, die dafür spricht**
|
|
|
|
Die halbe Voraussetzung ist bereits da, nur nicht verdrahtet:
|
|
|
|
- `ApplicationFileUtils.copyResourceIfRequired` legt beim Start Kopien von
|
|
`KST4ContestDefaultDay.css` und `KST4ContestDefaultEvening.css` im
|
|
Anwendungsverzeichnis ab.
|
|
- **Gelesen wird diese Kopie nie.** `applyThemeStylesheet` übergibt den blanken
|
|
Dateinamen an `Scene.getStylesheets()`, und JavaFX löst relative Namen gegen den
|
|
Klassenpfad auf. Wer seine Kopie bearbeitet, sieht keinerlei Wirkung.
|
|
- `JavaFxStylesheet` liest aus demselben Grund die Klassenpfad-Ressource: solange
|
|
JavaFX die Kopie ignoriert, würde ein Compose-Fenster, das sie liest, als einziges
|
|
anders aussehen.
|
|
|
|
**Umfang**
|
|
|
|
- Die Kopie im Profilverzeichnis wird zur maßgeblichen Quelle. Ab Etappe 6 gibt es
|
|
kein JavaFX mehr, das dagegenhält — vorher muss beides dieselbe Datei lesen.
|
|
- Die Palette wird **pro Profil** gehalten. Ein Profil ist heute schon der Träger für
|
|
Einstellungen und Layout; Farben gehören in dieselbe Schublade.
|
|
- Eine Oberfläche zum Anpassen im Einstellungsfenster: die Handvoll Rollen, die
|
|
`JavaFxPalette` bereits benennt — Fläche, Feldinneres, Text, Akzent, Trennlinie —
|
|
statt eines freien CSS-Editors. Zurücksetzen auf die Auslieferung gehört dazu.
|
|
- Ein Kontrastwächter. Eine frei gewählte Textfarbe auf einer frei gewählten Fläche
|
|
kann unlesbar werden; die Oberfläche muss das sagen, bevor sie es speichert.
|
|
|
|
**Abnahmekriterium:** Eine geänderte Palette überlebt den Neustart, gilt für alle
|
|
Fenster gleichzeitig und ist pro Profil verschieden. Das Zurücksetzen stellt exakt die
|
|
ausgelieferten Farben her.
|
|
|
|
> Setzt Etappe 6 voraus, nicht wegen der Karte, sondern wegen `derive`: solange die
|
|
> Ableitung an JavaFX hängt, ist die Palette nicht frei wählbar, ohne dass dieselbe
|
|
> Rechnung in Compose nachgebaut ist. Siehe den Hinweis in Etappe 6.
|
|
|
|
### Etappe 9 — Mehrsprachigkeit
|
|
|
|
**Ziel:** Die Oberfläche spricht die Sprache des Operateurs. Deutsch und Englisch zum
|
|
Start.
|
|
|
|
**Umfang**
|
|
|
|
Grob 232 sichtbare Zeichenketten in den Compose-Quellen und rund 492 im verbliebenen
|
|
Java-Teil — die Zahl schrumpft mit jeder Etappe, weshalb diese Arbeit ans Ende gehört.
|
|
|
|
**Die eigentliche Schwierigkeit ist nicht das Übersetzen, sondern die Abgrenzung.**
|
|
Drei Sorten Text liegen heute nebeneinander im Quelltext und sehen gleich aus:
|
|
|
|
| Sorte | Beispiele | übersetzen? |
|
|
|---|---|---|
|
|
| Oberflächentext | „Save settings", „Active bands", „Callsign" | **ja** |
|
|
| Protokolltext | `/AWAY`, `/BACK`, `/CQ `, `SETNAME`, `MYQRG` | **nein, niemals** — das geht an den ON4KST-Server |
|
|
| Funkerkürzel | „Pse Sked?", „qrv 144.300", „tnx om, 73" | **nein** — das geht an andere Funker und ist international |
|
|
|
|
Ein übersetzter Protokollbefehl bricht die Verbindung, ein übersetztes Funkerkürzel
|
|
macht den Operateur unverständlich. Beides fällt in keinem Test auf und erst im
|
|
Contest, also unter Zeitdruck.
|
|
|
|
- Vor dem Herausziehen wird jede Zeichenkette einer der drei Sorten zugeordnet, und
|
|
die Zuordnung wird festgehalten — nicht im Kopf des Übersetzenden.
|
|
- Die Vorgabewerte in `ChatPreferences` (Beacon-Vorlagen, Antworttexte, Kurztasten)
|
|
sind Funkertext und bleiben, wie sie sind. Sie stehen zudem in der gespeicherten
|
|
XML jedes Operateurs; sie zu übersetzen würde bestehende Profile ändern.
|
|
- Ressourcenbündel pro Sprache, Auswahl im GUI-Reiter, Vorgabe aus der Systemsprache
|
|
mit Rückfall auf Englisch.
|
|
|
|
**Abnahmekriterium:** Die Oberfläche lässt sich umschalten und bleibt nach Neustart
|
|
umgeschaltet. **Kein einziger an den Server oder an andere Funker gehender Text
|
|
ändert sich mit der Sprache** — das ist gegen die Protokoll-Zeichenketten und die
|
|
`ChatPreferences`-Vorgabewerte zu prüfen, nicht durch Hinsehen.
|
|
|
|
## Invarianten — was unverändert bleiben muss
|
|
|
|
Diese Punkte sind in `AGENTS.md` und `PROJECT_CONTEXT.md` festgelegt und werden von der Umstellung nicht berührt.
|
|
|
|
**Chat-Identität**
|
|
|
|
- Vollständiges sichtbares Rufzeichen plus Kategorie bildet die Identität eines Chat-Teilnehmers.
|
|
- Basisruf-Normalisierung nur für explizit basisrufweite Funktionen.
|
|
- Worked-Zustand wird über Suffixvarianten desselben Basisrufs geteilt.
|
|
- Suffixe werden nicht global als Band, Kategorie oder Frequenz gedeutet.
|
|
|
|
**Bänder und Verfügbarkeit**
|
|
|
|
- Kategorien 2 und 3 sind die Hauptkategorien; unerwartete Werte müssen sicher fehlschlagen.
|
|
- `NOT-QRV` hat Vorrang vor positiven Band-Hinweisen.
|
|
- Fehlende Frequenz-, QRB- oder QTF-Werte bleiben unbekannt. `null` bedeutet nicht verfügbar, nicht null.
|
|
- Kein stilles Zurückfallen auf feste 144 MHz.
|
|
|
|
**Externe Schnittstellen**
|
|
|
|
- CR/LF-Framing, XML-Framing, Ports und Transporte bleiben unverändert.
|
|
- Die lokale DX-Cluster-Ausgabe behält ihre feste 75-Zeichen-Zeile, das DX-Rufzeichen ab Spalte 27, den 30-Zeichen-Kommentar ab Spalte 40 und die UTC-Zeit ab Spalte 71, gefolgt von zwei BEL und CRLF.
|
|
- Unerwartete Eingaben beenden keinen Verarbeitungs- oder UI-Thread.
|
|
|
|
**Persistenz**
|
|
|
|
- `preferences.xml` bleibt in Version 7, einschließlich der verwalteten Spaltenbreiten unterhalb `guiOptions` und der Regel, dass Elternspalten ihre Breite aus den Blattspalten ableiten.
|
|
- Selektives Schreiben aktualisiert die Datei auf der Platte, bewahrt unbekannte XML-Knoten und schreibt keine unbestätigten funktionalen Einstellungen. **Save Settings** bleibt der vollständige Schreiber.
|
|
- Vollständige und selektive Schreibvorgänge sind synchronisiert und ersetzen `preferences.xml` atomar.
|
|
- Operator-Profile behalten ihr Verhalten: Wurzelprofil flach, weitere Profile unter `profiles/<profileId>/`, `profiles.xml` wird verzögert angelegt, die Registrierung speichert niemals einen Pfad, ein neues Profil bekommt eine **leere** Datenbank.
|
|
- Das SQLite-Schema und die Worked-Semantik samt Dreitagesablauf bleiben unverändert.
|
|
- Simplelogfile-Verhalten bleibt unverändert, einschließlich des minütlichen Lesens und der Tatsache, dass daraus abgeleiteter Worked-Zustand nicht in SQLite landet.
|
|
|
|
**Quelltext-Konventionen**
|
|
|
|
- Kommentare und Javadoc ausschließlich auf Englisch.
|
|
- Log-, Protokoll- und API-Literale in ihrer kanonischen Form.
|
|
- Kommunikation mit Marc auf Deutsch; Commit-Nachrichten knapp auf Englisch.
|
|
|
|
## Risiken
|
|
|
|
| Risiko | Bewertung | Umgang |
|
|
|---|---|---|
|
|
| `DataTable` wird der Engpass | hoch | Etappe 3 baut sie zuerst und belastet sie in Etappe 4 mit Live-Daten, bevor das Hauptfenster davon abhängt |
|
|
| Packaging auf drei Plattformen neu | hoch | Etappe 1 macht das zuerst, mit unveränderter JavaFX-App als Vergleichsmaßstab |
|
|
| Canvas-Leistung bei vielen Markern | mittel | Ausweg ist Vorabaggregation im Kern; wird in Etappe 6 gemessen, nicht angenommen |
|
|
| Compose-Erfahrung fehlt bei beiden Entwicklern | mittel | Das Einstellungsfenster als erstes Lernstück ist bewusst ungefährlich gewählt |
|
|
| Funktionsgleichheit ist schwer nachweisbar | mittel | Vergleich Fenster für Fenster gegen die noch laufende JavaFX-Fassung; Protokoll und Persistenz sind durch `core`-Tests gedeckt |
|
|
| 1.50 verschiebt sich erheblich | angenommen | Bewusste Entscheidung. Kein Termin gesetzt |
|
|
| **JavaFX und Compose können auf macOS nicht koexistieren** | **eingetreten** | Am 28.09.2026 auf einem Mac nachgewiesen. Siehe den Abschnitt darunter; noch nicht entschieden, wie damit umgegangen wird |
|
|
|
|
## Eingetretenes Risiko: macOS trägt die Koexistenz nicht
|
|
|
|
Am 28.09.2026 auf einem Mac (macOS 27, arm64, Temurin 21) festgestellt: Übersetzen und
|
|
die vollständige Testreihe laufen dort sauber — **660 Testfälle, keine Fehler**, genau wie
|
|
auf Linux. Sobald aber ein Compose-Fenster geöffnet wird, hängt die Anwendung.
|
|
|
|
Thread-Abzug eines hängenden Starts:
|
|
|
|
```
|
|
main → wartet in Application.launch (JavaFX besitzt den Haupt-Runloop)
|
|
JavaFX Application Thread → wartet in OperatorProfilePickerWindow.showAndSelect
|
|
operator-profile-picker → hängt in sun.lwawt.macosx.LWCToolkit.initAppkit
|
|
```
|
|
|
|
Kein AWT-, AppKit- oder Skiko-Thread existiert. Auf macOS muss AppKit auf Thread 0
|
|
laufen; JavaFX nimmt diesen Runloop mit `Application.launch` in Besitz und gibt ihn nie
|
|
her, also wartet `initAppkit` auf einen Hauptthread, der nie wieder zuhört. X11 und
|
|
Wayland kennen diese Einschränkung nicht — deshalb ist es vier Etappen lang nicht
|
|
aufgefallen.
|
|
|
|
Der Profilwähler ist nur der Auslöser, weil er zuerst dran ist. Einstellungsfenster,
|
|
Monitor, Update-Fenster und Hauptfenster benutzen denselben `ComposeWindowHost`.
|
|
|
|
**Das trifft das Verfahren dieser Spezifikation im Kern:** der Vergleich Fenster für
|
|
Fenster gegen die laufende JavaFX-Fassung — der Umgang mit dem Risiko
|
|
„Funktionsgleichheit ist schwer nachweisbar" — setzt Koexistenz voraus, und die gibt es
|
|
auf macOS nicht.
|
|
|
|
Drei Wege:
|
|
|
|
1. **Startreihenfolge umdrehen.** AWT/Compose besitzt den Hauptthread, JavaFX wird
|
|
eingebettet gestartet (`Platform.startup` statt `Application.launch`, ggf. mit
|
|
`-Djavafx.embed.singleThread=true`). Wirkt sofort und für den Rest der Migration,
|
|
fasst aber den Anwendungsstart an und betrifft damit auch Linux. Als eigene Etappe
|
|
nach 5c zu führen, nicht hinein: sonst sind zwei riskante Umbauten gleichzeitig offen.
|
|
2. **macOS bis zum Ende zurückstellen.** Das Problem verschwindet mit JavaFX. Kostet
|
|
nichts, aber bis dahin ist auf macOS nichts zu testen — und Etappe 1 hat Packaging für
|
|
drei Plattformen ausdrücklich zuerst gemacht, um genau das zu vermeiden.
|
|
3. **Compose-Fenster auf macOS abschalten.** Arbeit, die später wieder herausfliegt.
|
|
|
|
|
|
## Offene Punkte
|
|
|
|
Diese Fragen werden bewusst erst beantwortet, wenn die vorangehende Etappe Erfahrung geliefert hat. Sie blockieren Etappe 1 und 2 nicht.
|
|
|
|
1. Genauer Zuschnitt der `DataTable`-Schnittstelle — nach Etappe 2.
|
|
2. Ob `Platform.setImplicitExit(false)` in Compose eine Entsprechung braucht — nach Etappe 4.
|
|
3. Aggregationsstrategie für Kartenmarker, falls das Canvas-Zeichnen nicht trägt — nach Etappe 5.
|
|
4. Ob das Geländeprofil als eigene Compose-Zeichnung oder über eine Diagrammbibliothek entsteht — nach Etappe 5.
|
|
5. Ob die Compose-Oberflächentests ohne Bildschirm laufen — vor Etappe 7.
|
|
6. Welcher der drei Wege beim macOS-Befund gegangen wird — zu entscheiden, bevor Etappe 6
|
|
JavaFX entfernt, denn Weg 1 wäre danach umsonst gebaut.
|
|
6. Ob die Farbpalette pro Profil oder pro Installation gilt — vor Etappe 8.
|