Files
kst4contest/docs/superpowers/plans/2026-09-27-etappe4-update-und-monitor-fenster.md
T
Rsclub2_2 cc8c90204e Draw the whole interface in Compose
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.
2026-10-05 22:33:28 +02:00

12 KiB

Umsetzungsplan Etappe 4 — Update-Fenster und Monitor-Fenster

Spezifikation: docs/superpowers/specs/2026-09-25-compose-migration-design.md, Etappe 4.

Was diese Etappe wirklich ist

Die Spezifikation nennt zwei Fenster. Beim Lesen des Quelltextes zeigt sich, dass das untertreibt:

Fenster Stelle braucht
Update :9458 ein Textgitter, einen Verweis, einen Baum mit Änderungsprotokoll und bekannten Fehlern, Größenspeicherung
Monitor :9396 initDXClusterTable mit 8 Spalten über ClusterMessage, initChatToOtherMSGTable mit 10 Spalten über ChatMessage

DataTable aus Etappe 3b kann eine Spalte Text — ListEntry(key, text). Die mehrspaltige Tabelle existiert nicht. Das Monitor-Fenster ist also nicht der Abnehmer einer fertigen Komponente, sondern ihr Anlass.

Etappe 5 braucht dieselbe Komponente für die Stationsliste mit Filterleiste und Prioritätenspalten. Was hier entsteht, trägt also drei Tabellen, nicht zwei. Der Zuschnitt wird danach entschieden, nicht nach dem, was für diese zwei Fenster gerade reicht — aber auch nicht darüber hinaus: die Stationsliste wird gelesen, bevor die Schnittstelle festgelegt wird, nicht danach.

Reihenfolge

Update-Fenster zuerst, wie besprochen. Es ist klein und klärt die Fenster-Mechanik aus einem Hintergrundfaden — dieselbe Stelle, an der in Etappe 3b der Profilwechsel scheiterte, weil closeOwnedStages ein Compose-Fenster nicht erreichte. Das Monitor- Fenster folgt mit dieser Gewissheit im Rücken, sonst käme die Fenster-Mechanik als zweite Fehlerquelle zu den Live-Daten dazu.

Aufgaben

1. UpdateWindowState mit Tests — erledigt

Umgesetzt in UpdateWindowState.kt, neun Testfälle. Beim Lesen kam heraus, dass das Fenster mehr enthält als die Spezifikation andeutet: neben dem Textgitter steht eine TreeView mit verborgener Wurzel und zwei Zweigen, „ChangeLog" und „Known bugs". Beide haben dieselbe Form — erstes Feld ist die Überschrift, der Rest sind Zeilen — also trägt ein Typ UpdateSection beide. Eine allgemeine Baumkomponente braucht es nicht: die Tiefe ist fest zwei.

Zwei Entscheidungen sind im Quelltext begründet: fehlende Update-Informationen gelten als „kein Update" (die Prüfung läuft beim Start, ein gescheiterter Abruf darf den Start nicht aufhalten), und eine Überschrift ohne Zeilen bleibt erhalten, statt weggefiltert zu werden.

Die ursprüngliche Beschreibung der Aufgabe:

Reine Fachlogik, ohne Oberfläche: Was wird angezeigt, und wann überhaupt. Die Bedingung steht heute bei :9583: getLatestVersionNumberOnServer() > APPLICATION_CURRENTVERSIONNUMBER. Dazu die vier Textfelder aus getUpdateInformation() und der Verweis auf die Veröffentlichungsseite.

Zu prüfen: gleiche Version zeigt nichts, ältere Serverversion zeigt nichts, fehlende Update-Information führt nicht zu einer Ausnahme beim Start.

2. Update-Fenster in Compose — erledigt und abgenommen

Titel „Update information", die sechs Beschriftungen wörtlich. Der Verweis öffnet den Systembrowser. Größe aus GUIstage_updateStage_SceneSizeHW.

Es folgt dem Design zur Laufzeit, wie SettingsWindow seit Etappe 3b, und es lässt sich von außen schließen. Beides ist keine Kür: ohne das erste steht ein helles Fenster neben einem dunklen, ohne das zweite überlebt es den Profilwechsel.

Abgenommen am laufenden Programm, Tag- und Abenddesign, einschließlich Umschalten zur Laufzeit über „Windows → Use dark mode design". Damit ist auch die neue applyDarkMode-Verdrahtung des Update-Fensters belegt.

3. Den JavaFX-Block entfernen und nachmessen — erledigt

stage_updateStage samt Feld, closeOwnedStages-Eintrag und registerThemedScene-Aufruf. Zeilenzahl von Kst4ContestApplication.java festhalten (Stand jetzt: 10 904).

Ergebnis: 10 904 → 10 802. Der Block war 145 Zeilen; zurück kamen 36 für openUpdateWindowIfAvailable, das nur noch die Entscheidung abfragt und den Systembrowser erreicht.

Die Fenstermechanik ist herausgezogen. ComposeWindowHost trägt jetzt das, was jedes Compose-Fenster neben JavaFX braucht: nicht den Prozess beenden, höchstens einmal öffnen, von außen schließbar sein, dem Design folgen. SettingsWindow wurde darauf umgestellt und behält seine Schnittstelle zur Java-Seite unverändert. Das war kein Aufräumen um seiner selbst willen: dieselbe Mechanik zum dritten Mal abzuschreiben hätte bedeutet, die close()-Verdrahtung zum dritten Mal von Hand zu treffen — und genau deren Fehlen war der Profilwechsel-Fehler aus Etappe 3b.

UpdateWindow.close() und applyDarkMode sind in closeOwnedStages und applyTheme eingetragen, nicht nachgereicht.

4. Die Stationsliste lesen, bevor die Tabellenschnittstelle steht — erledigt

Ergebnis: die drei Tabellen sind ungleich schwer.

Tabelle Zeilen Zellfabriken Zeilenfabrik Vergleicher
Monitor: DX-Cluster 183 1 0 0
Monitor: QSO der anderen 222 1 0 0
Stationsliste (Etappe 5) 854 13 1 4

Die beiden Monitor-Tabellen sind schlicht: Text in Spalten, keine Zeilenfärbung, keine eigene Sortierung. Nach ihnen allein zugeschnitten entstünde eine Komponente, die die Stationsliste nicht trägt — derselbe Fehler wie bei der DataTable in Etappe 3b.

Was die Stationsliste zusätzlich verlangt, aus dem Quelltext gelesen:

  • 22 Spalten: Callsign, Name, QRA, QRB, QTF, QRG, „AP [minutes / pot%]", Tropo, Score, Act, worked, wkdany, die Bänder 50/70/144/432/23/13/9/6/3, „NOT QRV @", Category.
  • Zellinhalt ist nicht gleich Zellenanzeige. Eine Zellfabrik liest den Wert und das Zeilenobjekt und Controllerzustand — Beispiel qraCol: Kürzen des Textes, Kurzhinweis aus Rasterfeld und „schon gearbeitet", Einfärbung nur wenn der Raster-Knopf aktiv ist.
  • Zeilenfärbung nach getCurrentPriorityScore() mit vier Schwellen: über 1000 rot („imminent sked"), ab 200 orange, ab 100 gelb, 0 und darunter ausgegraut. Die Farben unterscheiden sich nach Design — im Quelltext steht heute ein isDark-Zweig je Schwelle.
  • Eigene Vergleicher für vier Spalten; die übrigen sortieren nach Text.
  • 23 Spaltenbreiten über TableLayoutManager, entprellt gespeichert.

Daraus der Zuschnitt: die Schnittstelle wird für die Stationsliste entworfen, die Umsetzung nur so weit gebaut, wie Etappe 4 sie braucht. Eine Spalte ist damit:

  • eine feste Kennung (für die Breitenspeicherung),
  • eine Überschrift,
  • eine Funktion Zeile → angezeigter Text,
  • wahlweise ein Kurzhinweis aus (Zeile, Wert),
  • wahlweise eine Zellenfärbung aus (Zeile, Wert),
  • wahlweise ein Vergleicher.

Die Tabelle nimmt zusätzlich wahlweise eine Zeilenfärbung aus der Zeile. So braucht die Stationsliste in Etappe 5 keinen zweiten Zuschnitt, und die Monitor-Tabellen benutzen schlicht die einfachen Fälle.

Die ursprüngliche Beschreibung der Aufgabe:

initChatMemberTable und die Filterleiste aus Etappe 5 durchsehen und aufschreiben, was sie verlangt: Spaltentypen, Sortierung, Zeilenfärbung, Auswahlverhalten, Spaltenbreitenspeicherung. Erst danach die Schnittstelle festlegen.

Begründung: In Etappe 3b wurde die DataTable nach dem Einstellungsfenster zugeschnitten und kann deshalb keine zweite Spalte. Derselbe Fehler ein zweites Mal wäre vermeidbar gewesen.

5. Mehrspaltige Tabelle: Zustand und Regeln, mit Tests — erledigt

DataTableState mit zehn Testfällen. Die Zeilenidentität war die Kernentscheidung, und beim Lesen der drei Zeilentypen zeigte sich: es gibt zwei Sorten Liste, und eine Antwort reicht nicht.

Sorte Beispiele Identität
Entitätsliste Stationsliste, gearbeitete Stationen eigener Schlüssel, RowKeys.byValue mit dem rohen Rufzeichen
Ereignisliste DX-Cluster, QSO der anderen RowKeys.byReference

ClusterMessage und ChatMessage haben gar keinen Schlüssel. Einer aus Zeit, Absender und Text kollidiert, sobald jemand dasselbe Makro zweimal in derselben Sekunde schickt — genau die Falle „Text lässt Dubletten verschmelzen" aus Etappe 2. Zwei gleiche Nachrichten sind zwei Nachrichten. byReference vergibt Schlüssel beim ersten Sehen und merkt sie sich an der Objektreferenz; das trägt, weil die Roster bei jedem Schnappschuss dieselben Instanzen zurückgeben.

Es gibt bewusst keine Vorgabe. In Etappe 2 kamen die sichtbaren Fehler aus der Zeilenidentität; der Aufrufer zu zwingen, sie zu benennen, hält die Entscheidung dort, wo das Wissen sitzt.

Zwei weitere Regeln sind festgeschrieben:

  • Die Auswahl folgt der Zeile, nicht der Position. Kommt oben etwas dazu, bleibt die gewählte Station dieselbe. Verschwindet sie, wird die Auswahl gelöscht statt auf den Nachbarn zu rutschen — sonst zeigt der Operateur auf jemanden, den er nie gewählt hat.
  • Die Sortierung ist stabil. Gleiche Zeilen behalten ihre Ankunftsreihenfolge. Die JavaFX-SortedList fügte per Binärsuche ein und legte Gleichstände irgendwohin, weshalb Zeilen sprangen.

Die ursprüngliche Beschreibung der Aufgabe:

Ohne Oberfläche prüfbar: Spaltendefinition, Sortierung, Auswahl, Zeilenidentität.

Die Zeilenidentität ist der kritische Punkt. In Etappe 2 kamen die Fehler nicht aus der Tabelle, sondern aus der Kopplung zwischen Roster und Anzeige: ein Listenschlüssel darf nicht die Position sein (verschiebt sich beim Einfügen oben) und nicht der Text (fällt bei Dubletten zusammen). Das ist hier erneut zu entscheiden und festzuhalten.

Der Zustand hält alles, womit gemalt wird, in Compose-Zustand. Aus Etappe 3b: gewöhnliche Eigenschaften melden nichts, und bei Live-Daten fällt das nicht als „reagiert nicht" auf, sondern als veraltete Anzeige.

6. Die Tabelle als Compose-Komponente

Spaltenbreiten werden über die bestehende TableLayoutManager-Kennung entprellt gespeichert. Die Kennungen dx-cluster-monitor und qso-other-monitor bleiben getrennt von denen des Hauptfensters, wie PROJECT_CONTEXT.md festlegt.

7. Monitor-Fenster in Compose

Titel „Cluster & QSO of the other", geteilte Fläche mit den zwei Tabellen, Größe aus GUIclusterAndQSOMonStage_SceneSizeHW. Die Teilerposition wird gespeichert wie bisher.

8. Lasttest mit dem Simulator

Wie in Etappe 2, denn dort hat genau das die Sortier- und Fokusfehler gefunden — kein Test hat sie gesehen. Eine Stunde Dauerlast. Beobachtet wird: springt die Auswahl, kippt die Sortierung, bleibt die Anzeige aktuell, wächst der Speicherbedarf.

9. Den JavaFX-Monitorblock entfernen und nachmessen

Erst wenn der Lasttest steht.

Was unverändert bleiben muss

  • Layout-Kennungen getrennt von denen des Hauptfensters.
  • GUIclusterAndQSOMonStage_SceneSizeHW und GUIstage_updateStage_SceneSizeHW behalten Namen und Bedeutung; configVersion bleibt 7.
  • Die Update-Prüfung selbst wird nicht angefasst — nur ihre Anzeige.

Offene Punkte

  1. Die macOS-Systemmenüleiste. Geklärt. Compose kennt MenuBar() im Geltungsbereich von Window(), und auf macOS erscheint sie in der Systemmenüleiste am oberen Bildschirmrand, sobald das Fenster aktiv ist — genau das Verhalten, das setUseSystemMenuBar(true) heute erzeugt. Eine gemeinsame Leiste über mehrere Fenster gibt es nicht: jedes Fenster erklärt seine eigene.

    Das passt zum heutigen Vorgehen, nur anders herum. installSharedSystemMenuBar spiegelt die Menüs des Hauptfensters über Bindings.bindContent in eine unsichtbare Leiste jedes Nebenfensters. In Compose wird daraus eine gemeinsame Composable-Funktion, die jedes Fenster aufruft — eine Quelle, fünf Aufrufstellen.

    Ein Punkt bleibt zu beachten: die Menüeinträge werden heute aus onConnectionStateChanged aktiviert und deaktiviert (menuItemFileDisconnect, menuItemOptionsAwayBack und weitere). Deren Zustand muss beobachtbar sein, sonst wiederholt sich der Fehler aus Etappe 3b an einer Stelle, an der er besonders schlecht auffällt: ein Menüeintrag, der falsch aktiviert aussieht.

    Quelle: Menu bar, Kotlin Multiplatform Documentation

  2. Entprellte Spaltenbreiten. TableLayoutManager hängt an JavaFX-TableColumn. Ob die Compose-Tabelle ihn benutzt oder ein Gegenstück bekommt, entscheidet sich mit Aufgabe 4.