mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-20 14:23:08 +02:00
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.
This commit is contained in:
173 files changed
+26500
-4305
No files matched your search
@@ -0,0 +1,248 @@
|
||||
# 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](https://kotlinlang.org/docs/multiplatform/compose-desktop-menu-bar.html)
|
||||
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.
|
||||
Reference in new issue
Block a user