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.
4.3 KiB
Umsetzungsplan Etappe 5 — Hauptfenster
Spezifikation: docs/superpowers/specs/2026-09-25-compose-migration-design.md, Etappe 5.
Der Umfang, gemessen
| Teil | Zeilen | Besonderheit |
|---|---|---|
Stationsliste initChatMemberTable |
854 | 22 Spalten, 13 Zellfabriken, 1 Zeilenfabrik, 4 Vergleicher |
| Filterleiste | ~977 | 37 Bedienelemente, davon 25 Umschaltknöpfe |
Infobereich generateFurtherInfoAbtSelectedCallsignBP |
881 | eigener Teiler mit gespeicherter Position |
| Menüleiste (File, Options, Windows, Help) | ~343 | macOS-Systemmenüleiste |
TimelineView |
376 | eigene Datei, eigene Zeichnung |
| Top-Prioritätenliste | 69 | |
| Statusanzeigen | ~40 | Sked-Warnung, Verbindung, Band-Upgrade |
Über 3500 Zeilen. Etappe 3 war kleiner und wurde geteilt.
Der Infobereich ist so groß wie die Stationsliste. In der Spezifikation steht er als einer von sieben Aufzählungspunkten; das untertreibt ihn um eine Größenordnung.
Aufteilung in 5a, 5b und 5c
Nicht aus Ordnungsliebe, sondern weil eine Etappe lieferbar sein soll. Nach jedem Abschnitt läuft die Anwendung, und du kannst abnehmen, bevor der nächste anfängt.
5a — Stationsliste und Filterleiste
Der Kern. Die mehrspaltige Tabelle aus Etappe 4 wurde für diese Liste zugeschnitten
und trägt hier ihre erste echte Last: 22 Spalten, Zellfärbung aus Zeile und Wert,
Zeilenfärbung nach getCurrentPriorityScore() über vier Schwellen, eigene Vergleicher,
23 gespeicherte Spaltenbreiten.
Die 25 Umschaltknöpfe der Filterleiste sind kein Formular, sondern Zustand mit Regeln — welche Filter einander ausschließen, welche sich stapeln. Die Regeln gehören in einen testbaren Zustand, bevor ein Knopf gezeichnet wird.
Abnahme: Die Liste zeigt dieselben Stationen in derselben Reihenfolge wie JavaFX, die Filter wirken gleich, Spaltenbreiten überleben den Neustart.
5b — Infobereich, Top-Prioritätenliste, Chat-Reiter
Der Infobereich hängt an der Auswahl der Liste aus 5a — deshalb danach und nicht davor. Sein Teiler wird hier einmal richtig gebaut; im Monitor-Fenster habe ich ihn bewusst weggelassen, statt ihn zweimal halb zu bauen.
Abnahme: Auswahl in der Liste füllt den Infobereich wie bisher, Teilerpositionen werden entprellt gespeichert.
5c — Menüleiste, Statusleiste, TimelineView, Profilwechsel
Die Menüleiste wird eine gemeinsame Composable-Funktion, die jedes Fenster aufruft — so verlangt es Compose, und so bekommen die Compose-Fenster auf macOS ihre Systemmenüleiste zurück, die sie seit Etappe 3b nicht haben.
Der Profilwechsel ändert seine Natur. Heute reißt er die Laufzeit ab und baut eine
neue Kst4ContestApplication auf, weil viele Bedienelemente inline erzeugte
Instanzfelder sind. Dieser Grund entfällt; der Wechsel wird zum Austausch des
Zustandsobjekts. shutdownRuntime() bleibt trotzdem verantwortlich für alles, was eine
Verbindung überdauert.
Abnahme: Profilwechsel im laufenden Betrieb, ohne doppelt angemeldete Zuhörer und ohne gehaltene Ressourcen. Auf macOS erscheint die Menüleiste bei jedem Fenster.
Was aus Etappe 4 mitgenommen wird
Vier Fehler dieser Etappe kamen ausschließlich durch Hinsehen ans Licht, keiner durch einen Test, und der Prüfer fand nur einen Teil davon:
- Ein Zwischenspeicher, der den Zustand nicht mehr las, und eine Tabelle, die deshalb nie wieder zeichnete.
- Eine Kopfzeile, die die ganze Tabellenhöhe nahm.
- Eine Liste, die bei jeder neuen Zeile vom Kopf wegdriftete.
- Ein Menüeintrag, der seinen Umschalter verloren hatte.
Daraus drei Regeln für diese Etappe:
- Was gezeichnet wird, muss beim Lesen Zustand berühren. Der
Snapshot.observe-Test aus Etappe 4 fängt genau das und wird hier für jede neue Ansicht angelegt. - Nach jedem sichtbaren Teil wird gestartet und hingesehen, nicht erst am Ende des Abschnitts.
- Kein Verhalten wird weggelassen, um ein Darstellungsproblem zu umgehen. Der verlorene Umschalter war genau dieser Tausch.
Offene Punkte
- Ob
Platform.setImplicitExit(false)undApplicationRuntimeLauncher.exitApplication()in der Compose-Fassung noch eine Entsprechung brauchen — laut Spezifikation nach Etappe 4 zu klären, also jetzt. - Ob die Filterleiste ihre 25 Knöpfe behält oder ob mehrere davon derselbe Zustand sind. Vor dem Zeichnen zu lesen, nicht danach.