Files
kst4contest/docs/superpowers/plans/2026-09-28-etappe5c-task13-hauptfenster-zusammenbau.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

154 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Umsetzungsplan Etappe 5c, Task 13 — das Hauptfenster zusammensetzen
Task 13 stand im Etappenplan als **ein** Schritt: „das Hauptfenster zusammengesetzt, das
JavaFX-Fenster ersetzt und entfernt". Nach dem Vermessen der Vorlage ist das nicht ein
Schritt, sondern fünf. Dieser Plan sagt, welche, und warum die Reihenfolge so ist.
## Der Baum des JavaFX-Fensters
`start()` reicht von Zeile 7236 bis 9400 — **2164 Zeilen für ein Fenster**. Was darin
steht:
```
Stage (primaryStage) Größe aus GUIscn_ChatwindowMainSceneSizeHW,
└─ Scene auf den Schirm begrenzt
└─ BorderPane bPaneChatWindow
├─ top: FlowPane flwpne_StatusBar ✅ Task 10 + 11
└─ center: SplitPane links (waagerecht, 1 Trenner)
├─ SplitPane Nachrichtenteil (senkrecht, 4 Trenner)
│ ├─ privateMessageTable ❌ fehlt
│ ├─ FlowPane flwPane_textSnippets ❌ fehlt
│ ├─ VBox pnl_inputAndSendButtons → TimelineView ✅ Task 12
│ ├─ HBox textInputFlowPane ❌ fehlt
│ └─ TabPane bottomGlobalMessageTabPane ✅ Etappe 5b
└─ SplitPane rechts (senkrecht, 2 Trenner)
├─ BorderPane chatMemberTableBorderPane ✅ Etappe 5a
├─ topPriorityListPane ✅ Etappe 5b
└─ selectedCallSignFurtherInfoPane ✅ Etappe 5b
```
Sechs von zehn Bausteinen stehen. Die vier fehlenden sind nicht die kleinen.
## Was fehlt, mit Zeilen
| Fehlt | Vorlage | Bemerkung |
|---|---|---|
| Tabelle der gerichteten Nachrichten | `initChatprivateMSGTable` ab 3730 | eigener Bereich im Splitter, nicht in `MessageTabs` |
| Knopfreihe der Textschnipsel | `flwPane_textSnippets` 7890, `buttonFactory` 5233, `refreshShortcutButtons` 4541 | Der **Bestand** liegt schon in `ShortcutsTabState`; nur die Knöpfe fehlen |
| Sendezeile | `textInputFlowPane` 7886 | Eingabefeld, `TX`, `Clear`, Trenner, eigene QRG Haupt-/Zweitkategorie, eigener QTF |
| Kontextmenüs | `initChatMemberTableContextMenu` 3132, `insertTextSnippet` 7473 | Stationsliste **und** Nachrichtentabellen; Strg+1…Strg+0 für Schnipsel |
| Fensterrahmen | `getScreenAwareMainSceneSizeHW` 7178, `ensureStageFitsPrimaryScreen` 9396, `buildOperatorProfileTitleSuffix` 6595 | Größe merken und begrenzen, Titel mit Profilzusatz |
| **Splitter mit gemerkten Trennern** | 3 `SplitPane`, 7 Trenner | **Compose hat das nicht** — siehe unten |
## Das eine Stück, das Compose nicht mitbringt
`SplitPane` gibt es in Compose Multiplatform nicht. Sieben Trennerstellungen werden
heute in den Einstellungen gemerkt:
```java
GUImainWindowLeftSplitPane_dividerposition = {0.51}
GUImainWindowRightSplitPane_dividerposition = {0.53, 0.78}
GUImessageSectionSplitpane_dividerposition = {0.62, 0.7, 0.75, 0.9}
```
Das ist kein Beiwerk: ein Operator richtet sein Fenster einmal für seinen Schirm ein und
erwartet es beim nächsten Contest genauso. Wir brauchen also eine eigene, gezogene
Trennleiste mit gemerkter Stellung — dasselbe Muster wie die Spaltengriffe aus Etappe 4
(`Density.COLUMN_HANDLE_WIDTH`, Haarlinie in 5 dp Greiffläche), nur senkrecht wie
waagerecht und mit Anteilen statt Pixeln.
`ChatPreferences` liefert die Felder als `double[]`; die Compose-Fassung braucht sie als
Zustand, sonst zeichnet das Ziehen nicht mit. Das ist derselbe Fehler wie in Etappe 3b
und wird gleich als solcher behandelt.
## Aufteilung
### 13a — `SplitterState` und `SplitterPane`
Ein `n`-teiliger Splitter, waagerecht oder senkrecht, mit `n−1` Trennern als
Compose-Zustand, Mindestgrößen pro Bereich und Schreiben nach `ChatPreferences` beim
Loslassen (nicht bei jeder Mausbewegung — die JavaFX-Fassung hat bei jedem Pixel
`requestLayoutSave()` gerufen und dabei auf die Platte geschrieben).
**Tests:** Anteile summieren sich zu 1; ein Trenner schiebt keinen anderen vorbei; eine
Mindestgröße wird eingehalten; ein abgelehntes Ziehen schreibt nichts (das ist der
aufgenommene Kleinmangel aus Etappe 4 bei `resizeColumn`); `Snapshot.observe` auf die
Stellungen; ein Bestand mit zu wenigen gemerkten Werten fällt auf die Vorgaben zurück,
statt zu stürzen.
**Abnahme:** Prüfstand mit fünf Bereichen, Ziehen in beiden Richtungen.
### 13b — Sendezeile und Schnipsel-Knöpfe
Die Zeile, in der der Operator tatsächlich tippt. `ChatInputState` mit dem Text, dem
Ziel (gewählte Station oder Hauptkategorie), den beiden eigenen QRG-Feldern und dem QTF.
Darüber die Knopfreihe aus dem Schnipsel-Bestand, die sich neu baut, wenn die
Einstellungen sie ändern — `ShortcutsTabState` ruft dafür schon
`refreshShortcutButtons`, das heute noch ins Leere zeigt.
**Tests:** Eingabetaste sendet und leert; `Clear` leert ohne zu senden; die Zielauflösung
in der Reihenfolge der Vorlage (gewählte Station → Tabellenauswahl → `ScoreService` →
Hauptkategorie); ein Schnipsel wird an der Einfügemarke eingesetzt, nicht angehängt;
`Snapshot.observe` auf Text und Ziel.
**Risiko:** Das Feld hat in JavaFX den Fokus zurückgeholt (`requestFocus`,
`selectEnd`) — in Compose ist das ein `FocusRequester` und muss nach jedem Senden und
nach jedem Schnipsel wieder greifen. Genau hier hat Etappe 3b Fehler gehabt.
### 13c — Gerichtete Nachrichten und die Kontextmenüs
Die vierte Tabelle, mit `DataTableState` wie die anderen, und die Kontextmenüs, die es
in Compose noch gar nicht gibt: Rechtsklick auf eine Station (Einträge aus dem
gemerkten Bestand), auf eine Nachricht, und Strg+1…Strg+0 für die Schnipsel.
**Tests:** die Spalten; welche Einträge bei welcher Auswahl erscheinen; dass ein
Menüeintrag ohne Auswahl nichts sendet.
### 13d — Der Fensterrahmen
Größe aus den Einstellungen, auf die Arbeitsfläche des Schirms begrenzt (die Vorlage tut
das, weil ein Fenster von einem größeren Monitor sonst unbedienbar aufgeht), Speichern
bei Änderung, Titel aus Chatzustand und Profilzusatz, Schließen über `ComposeWindowHost`.
**Tests:** die Begrenzungsrechnung mit erfundenen Schirmgrößen (das ist reine Rechnung
und gehört nach `core`-Art getestet); der Titel bei gesetztem und fehlendem Profil.
### 13e — Zusammensetzen, ersetzen, entfernen
Erst hier entsteht `MainWindow.kt`, und erst hier fällt der JavaFX-Teil. Reihenfolge:
1. `MainWindow` zusammensetzen, **neben** dem JavaFX-Fenster startbar (beide offen,
Vergleich am Bildschirm — so sind in Etappe 4 die Unterschiede aufgefallen).
2. Abnahme durch den Operator, mit laufender Verbindung.
3. Erst danach `start()` ausräumen und die Zeilenzahl festhalten.
**Abnahme:** ein Contest-Ablauf von Hand — anmelden, Station wählen, tippen, senden,
Sked setzen, Profil wechseln — ohne dass ein Zuhörer doppelt hängt.
## Reihenfolge und Begründung
13a zuerst, weil ohne Splitter kein Fenster zusammengeht. 13b vor 13c, weil die
Sendezeile das ist, was der Operator die meiste Zeit benutzt; ein Fehler dort wiegt mehr
als eine fehlende Tabelle. 13d ist klein und unabhängig. 13e zuletzt und mit beiden
Fenstern offen.
## Was aus Etappe 4 und 5 mitkommt
- Nach **jedem** sichtbaren Teil starten und hinsehen. Elf Fehler dieser Umstellung sind
ausschließlich durch Hinsehen aufgefallen, keiner durch einen Test.
- `Snapshot.observe` für jede neue Ansicht.
- Kein Verhalten weglassen, um ein Darstellungsproblem zu umgehen.
- Vor dem Schreiben die Vorlage lesen, nicht nach dem Schreiben. Die falschen
Zuschreibungen dieser Etappe kamen alle daher, dass ich nach dem Namen geraten habe.
## Offene Entscheidungen
1. **Die vier Nachrichtenfilter** und **„Always on top"** sind aus Etappe 5b nach 5c
verschoben. Gehören sie in 13b (bei der Sendezeile) oder in 13c (bei den Tabellen)?
Vorschlag: 13c, weil sie Tabellenfilter sind.
2. Der Kleinmangel `GUIpnl_directedMSGWin_dividerposition` ist verwaist — die Vorlage
hat nur den `...Default`-Wert. In 13a wegwerfen oder stehen lassen?
Vorschlag: wegwerfen, mit einem Satz in der Einbuchung.
3. **Der dritte Sortierklick** (Sortierung löschen) fehlt seit Etappe 4. In 13c
mitnehmen oder eigene Kleinigkeit?