mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-10 19:25:32 +02:00
Persist table layouts just after changing the real used sizes and show truncated cell tooltips in the whole applications tables
This commit is contained in:
@@ -50,6 +50,16 @@ JavaFX ObservableList / UI state
|
|||||||
|
|
||||||
`MessageBusManagementThread` must not directly iterate or mutate UI-bound JavaFX collections. UI-visible changes should cross the controller/UI boundary and run on the JavaFX Application Thread.
|
`MessageBusManagementThread` must not directly iterate or mutate UI-bound JavaFX collections. UI-visible changes should cross the controller/UI boundary and run on the JavaFX Application Thread.
|
||||||
|
|
||||||
|
## Configuration and Layout Persistence
|
||||||
|
|
||||||
|
- `preferences.xml` configuration version 6 stores optional managed leaf-column widths below `guiOptions`, identified by stable table and column IDs. Parent-column widths remain derived from their leaf columns.
|
||||||
|
- Stored widths take precedence. Without a usable entry, a managed column is sized once when meaningful table data first becomes available. Message and similar free-text columns use a flexible initial width instead of following the longest value.
|
||||||
|
- Main-window and separate-monitor DXCluster/QSO tables use distinct layout IDs even though they share the underlying message stores.
|
||||||
|
- Window sizes and positions, relevant divider positions and managed column widths are selectively autosaved after a short debounce. A pending write is flushed during application shutdown.
|
||||||
|
- Selective layout writes update the XML already on disk, preserve unknown XML nodes and must not persist unconfirmed functional settings from the current UI. **Save Settings** remains the full settings writer and includes the current layout.
|
||||||
|
- Full and selective writes are synchronized and replace `preferences.xml` atomically. Missing, unknown or malformed width entries do not prevent loading and fall back to initial sizing.
|
||||||
|
- Older configuration files require no migration. Older KST4Contest versions can ignore the additional elements; a complete rewrite by such a version may discard column widths without invalidating the remaining file.
|
||||||
|
|
||||||
## External Interfaces
|
## External Interfaces
|
||||||
|
|
||||||
Treat current implementation/tests and authoritative upstream documentation as source of truth before modifying any interface.
|
Treat current implementation/tests and authoritative upstream documentation as source of truth before modifying any interface.
|
||||||
|
|||||||
@@ -152,7 +152,15 @@ Der Wert beeinflusst unter anderem:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Nachrichtentabellen
|
## Tabellenbreiten und gekürzte Zellinhalte
|
||||||
|
|
||||||
|
Beim ersten brauchbaren Datenbestand richtet KST4Contest die Spalten der Benutzerliste, der Nachrichtenansichten, der DXCluster- und QSO-Tabellen sowie der Worked-Datenbank einmalig nach Überschrift und vorhandenem Inhalt aus. Bereits gespeicherte Breiten haben Vorrang. **Name**, **AP** und **NOT QRV @** werden dabei begrenzt, damit einzelne lange Werte nicht den restlichen Tabellenbereich verdrängen. **Message** und vergleichbare Freitextspalten bleiben flexibel und richten sich nicht nach der längsten Nachricht.
|
||||||
|
|
||||||
|
Manuell geänderte Spaltenbreiten werden automatisch gespeichert und beim nächsten Start wiederhergestellt. Spätere Nachrichten oder Stationsaktualisierungen überschreiben diese Auswahl nicht.
|
||||||
|
|
||||||
|
Passt ein normaler Textwert nicht vollständig in seine Zelle, zeigt ein Tooltip den vollständigen Wert. Der Tooltip erscheint nur bei tatsächlich gekürztem Text. Funktionale Tooltips, etwa für QRA-, Worked- oder Bandzustände, bleiben erhalten; bei gekürztem Zelltext stehen Volltext und Erklärung gemeinsam im Tooltip.
|
||||||
|
|
||||||
|
### Nachrichtentext und Links
|
||||||
|
|
||||||
KST4Contest zeigt Nachrichtentexte bewusst einzeilig an. So bleiben auch bei hohem Chat-Aufkommen viele Einträge gleichzeitig sichtbar. Der Nachteil liegt auf der Hand: Bei einer schmalen **Message**-Spalte passt nicht jede Nachricht vollständig in die Zeile.
|
KST4Contest zeigt Nachrichtentexte bewusst einzeilig an. So bleiben auch bei hohem Chat-Aufkommen viele Einträge gleichzeitig sichtbar. Der Nachteil liegt auf der Hand: Bei einer schmalen **Message**-Spalte passt nicht jede Nachricht vollständig in die Zeile.
|
||||||
|
|
||||||
@@ -432,7 +440,7 @@ Zusätzlich öffnet KST4Contest das Fenster **Cluster & QSO of the other**. Es z
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Die Position des vertikalen Dividers sowie die Fenstergröße werden zusammen mit den übrigen UI-Einstellungen gespeichert. Nach einer Änderung **Save Settings** verwenden.
|
Die Position des vertikalen Dividers sowie die Fenstergröße werden automatisch gespeichert. Die DXCluster- und QSO-Tabellen besitzen hier eigene Spaltenbreiten; Änderungen im Monitorfenster verändern daher nicht das Tabellenlayout der Hauptfenster-Tabs.
|
||||||
|
|
||||||
Das Fenster lässt sich über das Menü aus- und wieder einblenden:
|
Das Fenster lässt sich über das Menü aus- und wieder einblenden:
|
||||||
|
|
||||||
@@ -478,13 +486,13 @@ Die serverbezogenen Funktionen sind nur bei vollständig aufgebauter ON4KST-Verb
|
|||||||
|
|
||||||
## Fenstergrößen und Divider
|
## Fenstergrößen und Divider
|
||||||
|
|
||||||
Beim Klick auf **Save Settings** speichert KST4Contest die Größen der Programmfenster und die Positionen der relevanten Divider in der Konfigurationsdatei. Diese Werte werden beim nächsten Programmstart wiederverwendet.
|
KST4Contest speichert Größen und Positionen der Programmfenster, die relevanten Divider sowie manuell geänderte Tabellenbreiten automatisch nach einer kurzen Verzögerung in der Konfigurationsdatei. Ein ausstehender Layoutstand wird beim Programmende noch geschrieben. **Save Settings** ist dafür nicht erforderlich, speichert aber weiterhin den vollständigen aktuellen Stand einschließlich Layout.
|
||||||
|
|
||||||
Das Hauptfenster wird beim Start zusätzlich gegen den sichtbaren Bereich des primären Bildschirms geprüft. Ist die gespeicherte Größe zu groß, verkleinert und verschiebt KST4Contest das Fenster so, dass es wieder erreichbar bleibt. Die genaue Herleitung ist unter [Bildschirmgerechte Größe des Hauptfensters](de-Funktionen#bildschirmgerechte-größe-des-hauptfensters-ab-v141) beschrieben.
|
Das Hauptfenster wird beim Start zusätzlich gegen den sichtbaren Bereich des primären Bildschirms geprüft. Ist die gespeicherte Größe zu groß, verkleinert und verschiebt KST4Contest das Fenster so, dass es wieder erreichbar bleibt. Die genaue Herleitung ist unter [Bildschirmgerechte Größe des Hauptfensters](de-Funktionen#bildschirmgerechte-größe-des-hauptfensters-ab-v141) beschrieben.
|
||||||
|
|
||||||
Für die übrigen Programmfenster gilt diese zusätzliche Größenbegrenzung derzeit nicht. Wird beispielsweise das separate Monitorfenster nach einem Wechsel auf einen kleineren Bildschirm zu groß dargestellt, muss seine Größe manuell korrigiert und anschließend erneut mit **Save Settings** gespeichert werden.
|
Für die übrigen Programmfenster gilt diese zusätzliche Größenbegrenzung derzeit nicht. Wird beispielsweise das separate Monitorfenster nach einem Wechsel auf einen kleineren Bildschirm zu groß dargestellt, genügt eine manuelle Korrektur; die neue Größe wird automatisch gespeichert.
|
||||||
|
|
||||||
Bei einer ungünstigen Aufteilung sollten zuerst die Divider an eine brauchbare Position verschoben und die Einstellungen erneut gespeichert werden. Das Löschen der Konfigurationsdatei setzt zwar die UI-Werte zurück, entfernt aber auch die übrigen gespeicherten Programmeinstellungen und sollte deshalb nur verwendet werden, wenn sich die Oberfläche auf anderem Weg nicht mehr herstellen lässt.
|
Bei einer ungünstigen Aufteilung sollten zuerst die Divider und Spaltenbreiten wieder an brauchbare Positionen verschoben werden. Das Löschen der Konfigurationsdatei setzt zwar die UI-Werte zurück, entfernt aber auch die übrigen gespeicherten Programmeinstellungen und sollte deshalb nur verwendet werden, wenn sich die Oberfläche auf anderem Weg nicht mehr herstellen lässt.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ v1.42 führt mehrere bisher getrennte Auswertungen zusammen. Bandinformationen,
|
|||||||
|
|
||||||
### Geändert
|
### Geändert
|
||||||
|
|
||||||
|
- **Tabellenlayout automatisch gesichert:** Tabellen werden beim ersten brauchbaren Inhalt sinnvoll dimensioniert. Manuell geänderte Spaltenbreiten, Fenstergrößen und relevante Divider werden verzögert in `preferences.xml` gespeichert; Haupt- und Monitorfenster behalten für DXCluster und QSO of the other getrennte Layouts. Gekürzte normale Zellwerte zeigen ihren Volltext im Tooltip, ohne funktionale Tooltips oder anklickbare Links zu verdrängen.
|
||||||
|
|
||||||
- **DX-Cluster-Zeilenformat vereinheitlicht:** Lokale Spots verwenden jetzt eine feste, DXSpider-kompatible 75-Zeichen-Nutzzeile mit dem DX-Rufzeichen ab Spalte 27, einem 30 Zeichen breiten Kommentarfeld und der UTC-Zeit ab Spalte 71. Das Format bleibt auch bei Mikrowellenfrequenzen bis 24 GHz stabil. Überlange DX-Rufzeichen werden verworfen und protokolliert, statt still abgeschnitten zu werden; AirScout- und Testkommentare sind entsprechend kompakter.
|
- **DX-Cluster-Zeilenformat vereinheitlicht:** Lokale Spots verwenden jetzt eine feste, DXSpider-kompatible 75-Zeichen-Nutzzeile mit dem DX-Rufzeichen ab Spalte 27, einem 30 Zeichen breiten Kommentarfeld und der UTC-Zeit ab Spalte 71. Das Format bleibt auch bei Mikrowellenfrequenzen bis 24 GHz stabil. Überlange DX-Rufzeichen werden verworfen und protokolliert, statt still abgeschnitten zu werden; AirScout- und Testkommentare sind entsprechend kompakter.
|
||||||
|
|
||||||
- **Sessionbezogene ON4KST-Verbindungssteuerung:** Socket, Reader, Writer, Messagebus und Warteschlangen gehören jetzt zu einer eindeutig identifizierten Verbindungssession. Veraltete Threads einer abgelösten Verbindung können dadurch keine Daten mehr verarbeiten oder die neue Verbindung schließen. `ONLINE` wird erst nach bestätigtem Login und vollständig empfangenen Benutzerlisten gemeldet. Verbindungsaufbau, Login und Synchronisation besitzen feste Zeitlimits; Heartbeats, ausbleibende Eingangsdaten, EOF sowie Lese- und Schreibfehler werden überwacht und lösen bei Bedarf einen kontrollierten Neuaufbau mit Backoff aus.
|
- **Sessionbezogene ON4KST-Verbindungssteuerung:** Socket, Reader, Writer, Messagebus und Warteschlangen gehören jetzt zu einer eindeutig identifizierten Verbindungssession. Veraltete Threads einer abgelösten Verbindung können dadurch keine Daten mehr verarbeiten oder die neue Verbindung schließen. `ONLINE` wird erst nach bestätigtem Login und vollständig empfangenen Benutzerlisten gemeldet. Verbindungsaufbau, Login und Synchronisation besitzen feste Zeitlimits; Heartbeats, ausbleibende Eingangsdaten, EOF sowie Lese- und Schreibfehler werden überwacht und lösen bei Bedarf einen kontrollierten Neuaufbau mit Backoff aus.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Nach dem ersten Start öffnet sich das **Einstellungsfenster** – dieses ist der zentrale Ausgangspunkt für alle Konfigurationen. Es empfiehlt sich, das Einstellungsfenster während des Betriebs geöffnet zu lassen (z. B. um den Beacon schnell ein- und auszuschalten).
|
Nach dem ersten Start öffnet sich das **Einstellungsfenster** – dieses ist der zentrale Ausgangspunkt für alle Konfigurationen. Es empfiehlt sich, das Einstellungsfenster während des Betriebs geöffnet zu lassen (z. B. um den Beacon schnell ein- und auszuschalten).
|
||||||
|
|
||||||
> **Wichtig**: Nach jeder Änderung unbedingt **„Save Settings"** klicken! Die Einstellungen werden unter Linux und macOS in `~/.praktiKST/preferences.xml` und unter Windows in `%USERPROFILE%\.praktiKST\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktiKST\preferences.xml`) gespeichert. Ab v1.21 werden auch Fenstergrößen und Divider-Positionen beim Speichern gesichert.
|
> **Wichtig**: Fachliche Einstellungen mit **„Save Settings"** sichern, wenn sie beim nächsten Start wieder gelten sollen. Layoutänderungen wie Fenstergrößen, Divider-Positionen und Tabellenbreiten speichert KST4Contest automatisch. Die gemeinsame Datei liegt unter Linux und macOS in `~/.praktiKST/preferences.xml` und unter Windows in `%USERPROFILE%\.praktiKST\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktiKST\preferences.xml`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -886,8 +886,10 @@ Der Dark Mode wird über **Windows → Use dark mode design** aktiviert. Mit **W
|
|||||||
|
|
||||||
## Einstellungen speichern
|
## Einstellungen speichern
|
||||||
|
|
||||||
Nach **jeder** Änderung **„Save Settings"** klicken! Ohne Speichern gehen alle Änderungen beim nächsten Start verloren.
|
**Save Settings** speichert die fachlichen Einstellungen und den vollständigen aktuellen Layoutstand. Änderungen an Fenstergrößen und -positionen, relevanten Dividern sowie verwalteten Tabellenbreiten werden zusätzlich automatisch mit kurzer Verzögerung gespeichert. Ein ausstehender Layoutstand wird beim Programmende noch geschrieben.
|
||||||
|
|
||||||
- Speicherort: unter Linux und macOS `~/.praktiKST/preferences.xml` und unter Windows `%USERPROFILE%\.praktiKST\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktiKST\preferences.xml`)
|
- Speicherort: unter Linux und macOS `~/.praktiKST/preferences.xml` und unter Windows `%USERPROFILE%\.praktiKST\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktiKST\preferences.xml`)
|
||||||
- Ab v1.21: Fenstergrößen und Divider-Positionen werden ebenfalls gespeichert.
|
- Der automatische Layout-Writer übernimmt keine noch nicht mit **Save Settings** bestätigten fachlichen Änderungen.
|
||||||
|
- Die Konfigurationsversion 6 ergänzt optionale Spaltenbreiten unter `guiOptions`. Ältere `preferences.xml`-Dateien bleiben lesbar; fehlen Breiten oder sind Einträge ungültig, ermittelt KST4Contest wieder brauchbare Anfangsbreiten.
|
||||||
|
- Ältere Programmversionen ignorieren die zusätzlichen XML-Einträge. Wenn eine ältere Version die Datei vollständig neu speichert, können lediglich die Spaltenbreiten verloren gehen.
|
||||||
- Bei Problemen: Konfigurationsdatei löschen → KST4Contest erstellt eine neue mit Standardwerten.
|
- Bei Problemen: Konfigurationsdatei löschen → KST4Contest erstellt eine neue mit Standardwerten.
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ v1.42 brings several previously separate calculations together. Band information
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- **Automatic table-layout persistence:** Tables receive useful widths when their first meaningful contents arrive. Manually changed column widths, window sizes and relevant dividers are written to `preferences.xml` after a short delay; the main and monitor windows keep separate DXCluster and QSO-of-the-other layouts. Truncated normal cell values expose their full text in a tooltip without replacing functional tooltips or clickable links.
|
||||||
|
|
||||||
- **Unified DX Cluster line format:** Local spots now use a fixed, DXSpider-compatible 75-character payload line with the DX callsign in column 27, a 30-character comment field and UTC time in column 71. The layout remains stable for microwave frequencies up to 24 GHz. Overlong DX callsigns are rejected and logged instead of being silently truncated; AirScout and test comments are correspondingly more compact.
|
- **Unified DX Cluster line format:** Local spots now use a fixed, DXSpider-compatible 75-character payload line with the DX callsign in column 27, a 30-character comment field and UTC time in column 71. The layout remains stable for microwave frequencies up to 24 GHz. Overlong DX callsigns are rejected and logged instead of being silently truncated; AirScout and test comments are correspondingly more compact.
|
||||||
|
|
||||||
- **Session-based ON4KST connection lifecycle:** Each socket, reader, writer, message bus and queue now belongs to an explicitly identified connection session. Delayed threads from an obsolete connection can therefore no longer process data or close its replacement. `ONLINE` is reported only after the login has been accepted and all requested user lists have been received. Connection setup, login and synchronisation use bounded timeouts, while heartbeats, missing inbound traffic, EOF and read or write failures trigger controlled reconnect attempts with backoff where appropriate.
|
- **Session-based ON4KST connection lifecycle:** Each socket, reader, writer, message bus and queue now belongs to an explicitly identified connection session. Delayed threads from an obsolete connection can therefore no longer process data or close its replacement. `ONLINE` is reported only after the login has been accepted and all requested user lists have been received. Connection setup, login and synchronisation use bounded timeouts, while heartbeats, missing inbound traffic, EOF and read or write failures trigger controlled reconnect attempts with backoff where appropriate.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
After the first start, the **settings window** opens – this is the central starting point for all configuration. It is recommended to keep the settings window open during operation (e.g. to quickly toggle the beacon on and off).
|
After the first start, the **settings window** opens – this is the central starting point for all configuration. It is recommended to keep the settings window open during operation (e.g. to quickly toggle the beacon on and off).
|
||||||
|
|
||||||
> **Important**: Always click **"Save Settings"** after any change! Settings are stored in `~/.praktiKST/preferences.xml` on Linux and macOS and in `%USERPROFILE%\.praktiKST\preferences.xml` (or `C:\Users\<Username>\.praktiKST\preferences.xml`) on Windows. From v1.21 onwards, window sizes and divider positions are also saved when you click Save.
|
> **Important**: Use **Save Settings** for functional settings which should remain in effect after the next start. KST4Contest saves layout changes such as window sizes, divider positions and table-column widths automatically. The shared file is `~/.praktiKST/preferences.xml` on Linux and macOS and `%USERPROFILE%\.praktiKST\preferences.xml` (or `C:\Users\<Username>\.praktiKST\preferences.xml`) on Windows.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -943,8 +943,10 @@ Enable Dark Mode through **Windows → Use dark mode design**. Use **Windows →
|
|||||||
|
|
||||||
## Saving Settings
|
## Saving Settings
|
||||||
|
|
||||||
Click **"Save Settings"** after **every** change! Without saving, all changes will be lost on the next start.
|
**Save Settings** stores functional settings and the complete current layout. Changes to window sizes and positions, relevant dividers and managed table-column widths are also saved automatically after a short delay. Any pending layout update is written when the programme exits.
|
||||||
|
|
||||||
- Storage location: `~/.praktiKST/preferences.xml` on Linux and macOS and `%USERPROFILE%\.praktiKST\preferences.xml` (or `C:\Users\<Username>\.praktiKST\preferences.xml`) on Windows
|
- Storage location: `~/.praktiKST/preferences.xml` on Linux and macOS and `%USERPROFILE%\.praktiKST\preferences.xml` (or `C:\Users\<Username>\.praktiKST\preferences.xml`) on Windows
|
||||||
- From v1.21: Window sizes and divider positions are also saved.
|
- The automatic layout writer does not copy functional changes which have not yet been confirmed with **Save Settings**.
|
||||||
|
- Configuration version 6 adds optional column-width entries below `guiOptions`. Older `preferences.xml` files remain readable. Missing or invalid widths simply cause KST4Contest to calculate useful initial widths again.
|
||||||
|
- Older programme versions ignore the additional XML entries. If an older version rewrites the complete file, only the column widths may be lost.
|
||||||
- If you encounter problems: delete the configuration file → KST4Contest will create a new one with default values.
|
- If you encounter problems: delete the configuration file → KST4Contest will create a new one with default values.
|
||||||
|
|||||||
@@ -154,7 +154,15 @@ The value affects, among other things:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Message Tables
|
## Table Widths and Truncated Cell Values
|
||||||
|
|
||||||
|
When useful data first becomes available, KST4Contest sizes the columns of the user list, message views, DXCluster and QSO tables, and Worked database once from their headings and existing contents. Stored widths take precedence. The initial widths of **Name**, **AP** and **NOT QRV @** are capped so that one long value cannot displace the rest of the table. **Message** and similar free-text columns remain flexible and do not follow the longest message.
|
||||||
|
|
||||||
|
Manually changed column widths are saved automatically and restored at the next start. Later messages or station updates do not overwrite that choice.
|
||||||
|
|
||||||
|
If a normal text value does not fit in its cell, a tooltip shows the complete value. It appears only when the displayed text is actually truncated. Functional tooltips for QRA, Worked, band and similar states remain available. If such a cell is also truncated, the tooltip contains both the full value and the functional explanation.
|
||||||
|
|
||||||
|
### Message Text and Links
|
||||||
|
|
||||||
KST4Contest deliberately displays message text on a single line. This keeps a larger number of entries visible when chat activity is high. The disadvantage is obvious: if the **Message** column is narrow, not every message fits completely into its cell.
|
KST4Contest deliberately displays message text on a single line. This keeps a larger number of entries visible when chat activity is high. The disadvantage is obvious: if the **Message** column is narrow, not every message fits completely into its cell.
|
||||||
|
|
||||||
@@ -436,7 +444,7 @@ KST4Contest additionally opens the **Cluster & QSO of the other** window. It sho
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
The vertical divider position and window size are stored together with the other UI settings. Use **Save Settings** after changing them.
|
The vertical divider position and window size are saved automatically. The DXCluster and QSO tables use their own column widths in this window, so changing the monitor layout does not alter the corresponding main-window tabs.
|
||||||
|
|
||||||
The window can be hidden and restored through:
|
The window can be hidden and restored through:
|
||||||
|
|
||||||
@@ -482,13 +490,13 @@ Functions which communicate with the server are available only after the ON4KST
|
|||||||
|
|
||||||
## Window Sizes and Dividers
|
## Window Sizes and Dividers
|
||||||
|
|
||||||
When **Save Settings** is clicked, KST4Contest stores the programme-window sizes and the positions of the relevant dividers in the configuration file. These values are reused at the next start.
|
KST4Contest automatically stores programme-window sizes and positions, relevant dividers and manually changed table-column widths in the configuration file after a short delay. Any pending layout update is written when the programme exits. **Save Settings** is not required for these changes, but still stores the complete current state, including the layout.
|
||||||
|
|
||||||
The main window is additionally checked against the visible area of the primary screen during startup. If the stored size is too large, KST4Contest reduces and moves the window so that it remains accessible. The complete process is described under [Screen-Aware Main Window Sizing](en-Features#screen-aware-main-window-sizing-from-v141).
|
The main window is additionally checked against the visible area of the primary screen during startup. If the stored size is too large, KST4Contest reduces and moves the window so that it remains accessible. The complete process is described under [Screen-Aware Main Window Sizing](en-Features#screen-aware-main-window-sizing-from-v141).
|
||||||
|
|
||||||
The other programme windows do not currently use this additional size restriction. If, for example, the separate monitor window appears too large after moving to a smaller screen, its size must be corrected manually and stored again using **Save Settings**.
|
The other programme windows do not currently use this additional size restriction. If, for example, the separate monitor window appears too large after moving to a smaller screen, correcting it manually is sufficient; the new size is saved automatically.
|
||||||
|
|
||||||
If the layout has become inconvenient, first move the dividers back to usable positions and save the settings again. Deleting the configuration file also resets the UI values, but it removes the other stored programme settings as well. It should therefore be used only when the interface cannot be restored in another way.
|
If the layout has become inconvenient, first move the dividers and column widths back to usable positions. Deleting the configuration file also resets the UI values, but it removes the other stored programme settings as well. It should therefore be used only when the interface cannot be restored in another way.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package kst4contest.model;
|
package kst4contest.model;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.ArrayList;
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.OptionalDouble;
|
||||||
|
|
||||||
import javax.xml.XMLConstants;
|
import javax.xml.XMLConstants;
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
@@ -50,7 +55,7 @@ public class ChatPreferences {
|
|||||||
* Reading must stay backwards compatible: missing/unknown tags should fall back to defaults.
|
* Reading must stay backwards compatible: missing/unknown tags should fall back to defaults.
|
||||||
*/
|
*/
|
||||||
// private static final int CONFIG_VERSION = 2;
|
// private static final int CONFIG_VERSION = 2;
|
||||||
public static final int CONFIG_VERSION = 5;
|
public static final int CONFIG_VERSION = 6;
|
||||||
|
|
||||||
// Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility.
|
// Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility.
|
||||||
private static final String TAG_CONFIG_VERSION = "configVersion";
|
private static final String TAG_CONFIG_VERSION = "configVersion";
|
||||||
@@ -341,6 +346,11 @@ public class ChatPreferences {
|
|||||||
private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 };
|
private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 };
|
||||||
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
|
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
|
||||||
private boolean GUIstationMapPathAnalysisVisible = true;
|
private boolean GUIstationMapPathAnalysisVisible = true;
|
||||||
|
private final Map<String, Double> tableColumnWidths = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
private static final String TAG_TABLE_COLUMN_WIDTH = "tableColumnWidth";
|
||||||
|
private static final double MIN_TABLE_COLUMN_WIDTH = 16.0;
|
||||||
|
private static final double MAX_TABLE_COLUMN_WIDTH = 10_000.0;
|
||||||
|
|
||||||
|
|
||||||
/*********************************************************************************
|
/*********************************************************************************
|
||||||
@@ -645,6 +655,31 @@ public class ChatPreferences {
|
|||||||
this.GUIstationMapPathAnalysisVisible = GUIstationMapPathAnalysisVisible;
|
this.GUIstationMapPathAnalysisVisible = GUIstationMapPathAnalysisVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a stored width for one stable table/leaf-column identity.
|
||||||
|
*
|
||||||
|
* @param tableId stable table layout identifier
|
||||||
|
* @param columnId stable leaf-column identifier
|
||||||
|
* @return stored pixel width, or empty when no usable value exists
|
||||||
|
*/
|
||||||
|
public synchronized OptionalDouble getTableColumnWidth(String tableId, String columnId) {
|
||||||
|
if (!isValidTableColumnIdentity(tableId, columnId)) {
|
||||||
|
return OptionalDouble.empty();
|
||||||
|
}
|
||||||
|
Double width = tableColumnWidths.get(tableColumnWidthKey(tableId, columnId));
|
||||||
|
return isValidTableColumnWidth(width) ? OptionalDouble.of(width) : OptionalDouble.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates one table leaf-column width in memory. Persistence is coordinated by
|
||||||
|
* the layout autosave layer.
|
||||||
|
*/
|
||||||
|
public synchronized void setTableColumnWidth(String tableId, String columnId, double width) {
|
||||||
|
if (isValidTableColumnIdentity(tableId, columnId) && isValidTableColumnWidth(width)) {
|
||||||
|
tableColumnWidths.put(tableColumnWidthKey(tableId, columnId), width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isGuiOptions_defaultFilterNothing() {
|
public boolean isGuiOptions_defaultFilterNothing() {
|
||||||
return guiOptions_defaultFilterNothing;
|
return guiOptions_defaultFilterNothing;
|
||||||
}
|
}
|
||||||
@@ -1389,7 +1424,7 @@ public class ChatPreferences {
|
|||||||
*
|
*
|
||||||
* @return true if the file writing was successful, else false
|
* @return true if the file writing was successful, else false
|
||||||
*/
|
*/
|
||||||
public boolean writePreferencesToXmlFile() {
|
public synchronized boolean writePreferencesToXmlFile() {
|
||||||
|
|
||||||
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
|
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
|
||||||
try {
|
try {
|
||||||
@@ -2085,27 +2120,19 @@ public class ChatPreferences {
|
|||||||
);
|
);
|
||||||
guiOptions.appendChild(GUIstationMapPathAnalysisVisible);
|
guiOptions.appendChild(GUIstationMapPathAnalysisVisible);
|
||||||
|
|
||||||
|
appendTableColumnWidths(doc, guiOptions);
|
||||||
|
|
||||||
/****************************************************************************************
|
/****************************************************************************************
|
||||||
****************************** now write this XML! *************************************
|
****************************** now write this XML! *************************************
|
||||||
****************************************************************************************/
|
****************************************************************************************/
|
||||||
|
|
||||||
writeXml(doc, System.out);
|
writeDocumentAtomically(doc);
|
||||||
|
|
||||||
// write dom document to a file
|
|
||||||
try (FileOutputStream output =
|
|
||||||
new FileOutputStream(storeAndRestorePreferencesFileName)) {
|
|
||||||
writeXml(doc, output);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (TransformerException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} catch (ParserConfigurationException | TransformerException e1) {
|
} catch (ParserConfigurationException | TransformerException | IOException e1) {
|
||||||
// TODO Auto-generated catch block
|
// TODO Auto-generated catch block
|
||||||
e1.printStackTrace();
|
e1.printStackTrace();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -2117,6 +2144,111 @@ public class ChatPreferences {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes only layout values into the existing preferences document. Functional
|
||||||
|
* settings are deliberately read from disk and left untouched.
|
||||||
|
*/
|
||||||
|
public synchronized boolean writeLayoutPreferencesToXmlFile() {
|
||||||
|
Path preferencesPath = Path.of(storeAndRestorePreferencesFileName).toAbsolutePath();
|
||||||
|
if (!Files.isRegularFile(preferencesPath)) {
|
||||||
|
System.out.println("[ChatPreferences, Warning]: Cannot autosave layout because preferences.xml does not exist.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DocumentBuilder documentBuilder = createSecureDocumentBuilderFactory().newDocumentBuilder();
|
||||||
|
Document document = documentBuilder.parse(preferencesPath.toFile());
|
||||||
|
Element root = document.getDocumentElement();
|
||||||
|
if (root == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
upsertDirectChildText(document, root, TAG_CONFIG_VERSION, String.valueOf(CONFIG_VERSION));
|
||||||
|
Element guiOptions = getDirectChildElement(root, "guiOptions");
|
||||||
|
if (guiOptions == null) {
|
||||||
|
guiOptions = document.createElement("guiOptions");
|
||||||
|
root.appendChild(guiOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLayoutElements(document, guiOptions);
|
||||||
|
removeDirectChildren(guiOptions, TAG_TABLE_COLUMN_WIDTH);
|
||||||
|
appendTableColumnWidths(document, guiOptions);
|
||||||
|
writeDocumentAtomically(document);
|
||||||
|
return true;
|
||||||
|
} catch (ParserConfigurationException | SAXException | IOException | TransformerException exception) {
|
||||||
|
exception.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLayoutElements(Document document, Element guiOptions) {
|
||||||
|
upsertDirectChildText(document, guiOptions, "GUIscn_ChatwindowMainSceneSizeHW",
|
||||||
|
getGUIscn_ChatwindowMainSceneSizeHW()[0] + ";" + getGUIscn_ChatwindowMainSceneSizeHW()[1]);
|
||||||
|
upsertDirectChildText(document, guiOptions, "GUIclusterAndQSOMonStage_SceneSizeHW",
|
||||||
|
getGUIclusterAndQSOMonStage_SceneSizeHW()[0] + ";" + getGUIclusterAndQSOMonStage_SceneSizeHW()[1]);
|
||||||
|
upsertDirectChildText(document, guiOptions, "GUIstage_updateStage_SceneSizeHW",
|
||||||
|
getGUIstage_updateStage_SceneSizeHW()[0] + ";" + getGUIstage_updateStage_SceneSizeHW()[1]);
|
||||||
|
upsertDirectChildText(document, guiOptions, "GUIsettingsStageSceneSizeHW",
|
||||||
|
getGUIsettingsStageSceneSizeHW()[0] + ";" + getGUIsettingsStageSceneSizeHW()[1]);
|
||||||
|
upsertDirectChildText(document, guiOptions, "GUIselectedCallSignSplitPane_dividerposition",
|
||||||
|
doubleArrayToCSVString(getGUIselectedCallSignSplitPane_dividerposition()));
|
||||||
|
upsertDirectChildText(document, guiOptions, "GUImainWindowLeftSplitPane_dividerposition",
|
||||||
|
doubleArrayToCSVString(getGUImainWindowLeftSplitPane_dividerposition()));
|
||||||
|
upsertDirectChildText(document, guiOptions, "GUImessageSectionSplitpane_dividerposition",
|
||||||
|
doubleArrayToCSVString(getGUImessageSectionSplitpane_dividerposition()));
|
||||||
|
upsertDirectChildText(document, guiOptions, "GUImainWindowRightSplitPane_dividerposition",
|
||||||
|
doubleArrayToCSVString(getGUImainWindowRightSplitPane_dividerposition()));
|
||||||
|
upsertDirectChildText(document, guiOptions, "GUIpnl_directedMSGWin_dividerpositionDefault",
|
||||||
|
doubleArrayToCSVString(getGUIpnl_directedMSGWin_dividerpositionDefault()));
|
||||||
|
upsertDirectChildText(document, guiOptions, "GUIstationMapStageSceneSizeHW",
|
||||||
|
getGUIstationMapStageSceneSizeHW()[0] + ";" + getGUIstationMapStageSceneSizeHW()[1]);
|
||||||
|
upsertDirectChildText(document, guiOptions, "GUIstationMapStagePositionXY",
|
||||||
|
getGUIstationMapStagePositionXY()[0] + ";" + getGUIstationMapStagePositionXY()[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendTableColumnWidths(Document document, Element guiOptions) {
|
||||||
|
for (Map.Entry<String, Double> entry : tableColumnWidths.entrySet()) {
|
||||||
|
if (!isValidTableColumnWidth(entry.getValue())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int separatorIndex = entry.getKey().indexOf('\u0000');
|
||||||
|
if (separatorIndex <= 0 || separatorIndex >= entry.getKey().length() - 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Element widthElement = document.createElement(TAG_TABLE_COLUMN_WIDTH);
|
||||||
|
widthElement.setAttribute("tableId", entry.getKey().substring(0, separatorIndex));
|
||||||
|
widthElement.setAttribute("columnId", entry.getKey().substring(separatorIndex + 1));
|
||||||
|
widthElement.setAttribute("pixels", String.valueOf(entry.getValue()));
|
||||||
|
guiOptions.appendChild(widthElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeDocumentAtomically(Document document) throws IOException, TransformerException {
|
||||||
|
Path target = Path.of(storeAndRestorePreferencesFileName).toAbsolutePath();
|
||||||
|
Path parent = target.getParent();
|
||||||
|
Path fileName = target.getFileName();
|
||||||
|
if (parent == null || fileName == null) {
|
||||||
|
throw new IOException("Preferences path has no parent directory: " + target);
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
Path temporary = Files.createTempFile(parent, fileName.toString(), ".tmp");
|
||||||
|
try {
|
||||||
|
try (OutputStream output = Files.newOutputStream(temporary)) {
|
||||||
|
writeXml(document, output);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch (AtomicMoveNotSupportedException exception) {
|
||||||
|
Files.move(temporary, target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// write doc to output stream
|
// write doc to output stream
|
||||||
private static void writeXml(Document doc, OutputStream output) throws TransformerException {
|
private static void writeXml(Document doc, OutputStream output) throws TransformerException {
|
||||||
|
|
||||||
@@ -2920,6 +3052,22 @@ public class ChatPreferences {
|
|||||||
if (s5 != null) {
|
if (s5 != null) {
|
||||||
this.setGUIpnl_directedMSGWin_dividerpositionDefault(csvStringToDoubleArray(s5));
|
this.setGUIpnl_directedMSGWin_dividerpositionDefault(csvStringToDoubleArray(s5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tableColumnWidths.clear();
|
||||||
|
NodeList widthElements = element.getElementsByTagName(TAG_TABLE_COLUMN_WIDTH);
|
||||||
|
for (int widthIndex = 0; widthIndex < widthElements.getLength(); widthIndex++) {
|
||||||
|
Node widthNode = widthElements.item(widthIndex);
|
||||||
|
if (!(widthNode instanceof Element widthElement)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String tableId = widthElement.getAttribute("tableId");
|
||||||
|
String columnId = widthElement.getAttribute("columnId");
|
||||||
|
double width = parseDoubleOrDefault(widthElement.getAttribute("pixels"), Double.NaN);
|
||||||
|
if (isValidTableColumnIdentity(tableId, columnId) && isValidTableColumnWidth(width)) {
|
||||||
|
tableColumnWidths.put(tableColumnWidthKey(tableId, columnId), width);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3100,6 +3248,63 @@ public class ChatPreferences {
|
|||||||
return (n instanceof Element) ? (Element) n : null;
|
return (n instanceof Element) ? (Element) n : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DocumentBuilderFactory createSecureDocumentBuilderFactory()
|
||||||
|
throws ParserConfigurationException {
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||||
|
return factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Element getDirectChildElement(Element parent, String tagName) {
|
||||||
|
for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
|
||||||
|
if (child instanceof Element element && tagName.equals(element.getTagName())) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void upsertDirectChildText(
|
||||||
|
Document document,
|
||||||
|
Element parent,
|
||||||
|
String tagName,
|
||||||
|
String value
|
||||||
|
) {
|
||||||
|
Element element = getDirectChildElement(parent, tagName);
|
||||||
|
if (element == null) {
|
||||||
|
element = document.createElement(tagName);
|
||||||
|
parent.appendChild(element);
|
||||||
|
}
|
||||||
|
element.setTextContent(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void removeDirectChildren(Element parent, String tagName) {
|
||||||
|
for (Node child = parent.getFirstChild(); child != null; ) {
|
||||||
|
Node next = child.getNextSibling();
|
||||||
|
if (child instanceof Element element && tagName.equals(element.getTagName())) {
|
||||||
|
parent.removeChild(child);
|
||||||
|
}
|
||||||
|
child = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String tableColumnWidthKey(String tableId, String columnId) {
|
||||||
|
if (!isValidTableColumnIdentity(tableId, columnId)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return tableId + '\u0000' + columnId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isValidTableColumnIdentity(String tableId, String columnId) {
|
||||||
|
return tableId != null && !tableId.isBlank() && tableId.indexOf('\u0000') < 0
|
||||||
|
&& columnId != null && !columnId.isBlank() && columnId.indexOf('\u0000') < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isValidTableColumnWidth(Double width) {
|
||||||
|
return width != null && Double.isFinite(width)
|
||||||
|
&& width >= MIN_TABLE_COLUMN_WIDTH && width <= MAX_TABLE_COLUMN_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the text content of the first matching child tag (directly under {@code parent})
|
* Returns the text content of the first matching child tag (directly under {@code parent})
|
||||||
* or {@code defaultValue} if the tag does not exist or is empty.
|
* or {@code defaultValue} if the tag does not exist or is empty.
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
private StationMapView stationMapView; //view class for the avl stn map
|
private StationMapView stationMapView; //view class for the avl stn map
|
||||||
private StationMapBridge stationMapBridge; //bridge for mapping actions between map and view
|
private StationMapBridge stationMapBridge; //bridge for mapping actions between map and view
|
||||||
|
private LayoutAutosave layoutAutosave;
|
||||||
|
|
||||||
private final Button btnConnectionStateIndicator = new Button("LINK");
|
private final Button btnConnectionStateIndicator = new Button("LINK");
|
||||||
private final Tooltip tipConnectionStateIndicator = new Tooltip();
|
private final Tooltip tipConnectionStateIndicator = new Tooltip();
|
||||||
@@ -186,7 +187,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
stationMapView = new StationMapView(chatcontroller.getChatPreferences());
|
stationMapView = new StationMapView(
|
||||||
|
chatcontroller.getChatPreferences(),
|
||||||
|
this::requestLayoutSave
|
||||||
|
);
|
||||||
stationMapBridge = new StationMapBridge(
|
stationMapBridge = new StationMapBridge(
|
||||||
chatcontroller,
|
chatcontroller,
|
||||||
tbl_chatMember,
|
tbl_chatMember,
|
||||||
@@ -537,7 +541,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
* Builds the tooltip shown on a band-status cell: a fixed legend plus this row's
|
* Builds the tooltip shown on a band-status cell: a fixed legend plus this row's
|
||||||
* resolved status for the given band.
|
* resolved status for the given band.
|
||||||
*/
|
*/
|
||||||
private Tooltip buildBandCellStatusTooltip(ChatMember chatMember, Band band, String status) {
|
private String buildBandCellStatusTooltipText(ChatMember chatMember, Band band, String status) {
|
||||||
StringBuilder tooltip = new StringBuilder("Band status:\n")
|
StringBuilder tooltip = new StringBuilder("Band status:\n")
|
||||||
.append("X = worked on this band\n")
|
.append("X = worked on this band\n")
|
||||||
.append("B+ = band available, not worked on this band yet (call already worked on another band)\n")
|
.append("B+ = band available, not worked on this band yet (call already worked on another band)\n")
|
||||||
@@ -551,30 +555,26 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
.append("Status: ").append(status == null || status.isBlank() ? "-" : status);
|
.append("Status: ").append(status == null || status.isBlank() ? "-" : status);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Tooltip(tooltip.toString());
|
return tooltip.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a shared cell factory for one band-status column, attaching the
|
* Creates a shared cell factory for one band-status column, attaching the
|
||||||
* {@link #buildBandCellStatusTooltip(ChatMember, Band, String)} tooltip.
|
* {@link #buildBandCellStatusTooltipText(ChatMember, Band, String)} tooltip.
|
||||||
*/
|
*/
|
||||||
private Callback<TableColumn<ChatMember, String>, TableCell<ChatMember, String>> createBandStatusCellFactory(Band band) {
|
private Callback<TableColumn<ChatMember, String>, TableCell<ChatMember, String>> createBandStatusCellFactory(Band band) {
|
||||||
return column -> new TableCell<ChatMember, String>() {
|
return column -> new TruncatedTextTableCell<ChatMember>(
|
||||||
|
java.util.function.Function.identity(),
|
||||||
|
(member, status) -> buildBandCellStatusTooltipText(member, band, status)
|
||||||
|
) {
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(String item, boolean empty) {
|
protected void updateItem(String item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
if (empty) {
|
if (empty) {
|
||||||
setText(null);
|
|
||||||
setTooltip(null);
|
|
||||||
setStyle("");
|
setStyle("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatMember member = getTableRow() == null ? null : getTableRow().getItem();
|
|
||||||
|
|
||||||
setText(item);
|
|
||||||
setTooltip(buildBandCellStatusTooltip(member, band, item));
|
|
||||||
setAlignment(Pos.CENTER);
|
setAlignment(Pos.CENTER);
|
||||||
setStyle("-fx-font-weight: bold;");
|
setStyle("-fx-font-weight: bold;");
|
||||||
}
|
}
|
||||||
@@ -1374,6 +1374,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
|
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
|
||||||
// System.out.println("<<<<<<<<<<<<<<<<<<< devider " + selectedCallSignSplitPane.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + selectedCallSignSplitPane.getDividers().size());
|
// System.out.println("<<<<<<<<<<<<<<<<<<< devider " + selectedCallSignSplitPane.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + selectedCallSignSplitPane.getDividers().size());
|
||||||
chatcontroller.getChatPreferences().getGUIselectedCallSignSplitPane_dividerposition()[selectedCallSignSplitPane.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
|
chatcontroller.getChatPreferences().getGUIselectedCallSignSplitPane_dividerposition()[selectedCallSignSplitPane.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1649,9 +1650,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tbl_chatMemberTable.setTooltip(new Tooltip(
|
|
||||||
"Stations available \n\nUse right click to a station to select predefined texts\nor hit <strg> + <1> ... <9> to write textsnippet to selected station\n\nHit <enter> to send"));
|
|
||||||
|
|
||||||
TableColumn<ChatMember, String> callSignCol =
|
TableColumn<ChatMember, String> callSignCol =
|
||||||
new TableColumn<ChatMember, String>("Callsign");
|
new TableColumn<ChatMember, String>("Callsign");
|
||||||
|
|
||||||
@@ -1669,7 +1667,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
return new SimpleStringProperty(displayedCallsign);
|
return new SimpleStringProperty(displayedCallsign);
|
||||||
});
|
});
|
||||||
|
|
||||||
callSignCol.setCellFactory(column -> new TableCell<ChatMember, String>() {
|
callSignCol.setCellFactory(column -> new TruncatedTextTableCell<ChatMember>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(String item, boolean empty) {
|
protected void updateItem(String item, boolean empty) {
|
||||||
@@ -1757,14 +1755,24 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
* <p>No font weight is changed here. The compact worked/grid status such as
|
* <p>No font weight is changed here. The compact worked/grid status such as
|
||||||
* {@code xo} is emphasized only in the worked-any column.</p>
|
* {@code xo} is emphasized only in the worked-any column.</p>
|
||||||
*/
|
*/
|
||||||
qraCol.setCellFactory(column -> new TableCell<ChatMember, String>() {
|
qraCol.setCellFactory(column -> new TruncatedTextTableCell<ChatMember>(
|
||||||
|
java.util.function.Function.identity(),
|
||||||
|
(member, value) -> {
|
||||||
|
String grossField = WorkedGrossFieldCache.extractGrossField(value);
|
||||||
|
boolean gridWorked = member != null
|
||||||
|
&& chatcontroller != null
|
||||||
|
&& chatcontroller.isGridSquareWorkedAny(member);
|
||||||
|
return "Grid status: "
|
||||||
|
+ (grossField == null ? "unknown" : grossField)
|
||||||
|
+ "\nGrid worked any: "
|
||||||
|
+ (gridWorked ? "yes" : "no");
|
||||||
|
}
|
||||||
|
) {
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(String item, boolean empty) {
|
protected void updateItem(String item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
if (empty || item == null) {
|
if (empty || item == null) {
|
||||||
setText(null);
|
|
||||||
setTooltip(null);
|
|
||||||
setStyle("");
|
setStyle("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1774,16 +1782,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
&& chatcontroller != null
|
&& chatcontroller != null
|
||||||
&& chatcontroller.isGridSquareWorkedAny(member);
|
&& chatcontroller.isGridSquareWorkedAny(member);
|
||||||
|
|
||||||
String grossField = WorkedGrossFieldCache.extractGrossField(item);
|
|
||||||
|
|
||||||
setText(item);
|
|
||||||
setTooltip(new Tooltip(
|
|
||||||
"Grid status: "
|
|
||||||
+ (grossField == null ? "unknown" : grossField)
|
|
||||||
+ "\nGrid worked any: "
|
|
||||||
+ (gridWorked ? "yes" : "no")
|
|
||||||
));
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Important:
|
* Important:
|
||||||
* Do not style the cell unless the Grid color button is active AND the
|
* Do not style the cell unless the Grid color button is active AND the
|
||||||
@@ -1929,7 +1927,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
*/
|
*/
|
||||||
airScoutCol.setCellFactory(new Callback<TableColumn<ChatMember, String>, TableCell<ChatMember, String>>() {
|
airScoutCol.setCellFactory(new Callback<TableColumn<ChatMember, String>, TableCell<ChatMember, String>>() {
|
||||||
public TableCell call(TableColumn param) {
|
public TableCell call(TableColumn param) {
|
||||||
return new TableCell<ChatMember, String>() {
|
return new TruncatedTextTableCell<ChatMember>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateItem(String item, boolean empty) {
|
public void updateItem(String item, boolean empty) {
|
||||||
@@ -1953,7 +1951,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
this.getStyleClass().add("table-cell-50PercentAP");
|
this.getStyleClass().add("table-cell-50PercentAP");
|
||||||
}
|
}
|
||||||
|
|
||||||
setText(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2038,22 +2035,18 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
/**
|
/**
|
||||||
* Shows the compact worked/grid status and explains it by tooltip.
|
* Shows the compact worked/grid status and explains it by tooltip.
|
||||||
*/
|
*/
|
||||||
wkdAny_subcol.setCellFactory(column -> new TableCell<ChatMember, String>() {
|
wkdAny_subcol.setCellFactory(column -> new TruncatedTextTableCell<ChatMember>(
|
||||||
|
java.util.function.Function.identity(),
|
||||||
|
(member, value) -> buildWorkedAnyGridStatusTooltip(member)
|
||||||
|
) {
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(String item, boolean empty) {
|
protected void updateItem(String item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
if (empty) {
|
if (empty) {
|
||||||
setText(null);
|
|
||||||
setTooltip(null);
|
|
||||||
setStyle("");
|
setStyle("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatMember member = getTableRow() == null ? null : getTableRow().getItem();
|
|
||||||
|
|
||||||
setText(item);
|
|
||||||
setTooltip(new Tooltip(buildWorkedAnyGridStatusTooltip(member)));
|
|
||||||
setAlignment(Pos.CENTER);
|
setAlignment(Pos.CENTER);
|
||||||
setStyle("-fx-font-weight: bold;");
|
setStyle("-fx-font-weight: bold;");
|
||||||
}
|
}
|
||||||
@@ -2389,7 +2382,38 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
// }, new Date(), 5000);
|
// }, new Date(), 5000);
|
||||||
|
|
||||||
tbl_chatMemberTable.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
|
tbl_chatMemberTable.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
|
||||||
tbl_chatMemberTable.autosize();
|
applyTruncatedTextCells(
|
||||||
|
nameCol, qrBCol, qtfCol, tropoCol, priorityScoreCol,
|
||||||
|
lastActCol, notQRVCol, chatCategoryCol
|
||||||
|
);
|
||||||
|
TableLayoutManager.install(
|
||||||
|
tbl_chatMemberTable,
|
||||||
|
"chat-members",
|
||||||
|
chatcontroller.getChatPreferences(),
|
||||||
|
layoutAutosave,
|
||||||
|
TableLayoutManager.column("callsign", callSignCol),
|
||||||
|
TableLayoutManager.column("name", nameCol).maximumInitialWidth(220),
|
||||||
|
TableLayoutManager.column("qra", qraCol),
|
||||||
|
TableLayoutManager.column("qrb", qrBCol),
|
||||||
|
TableLayoutManager.column("qtf", qtfCol),
|
||||||
|
TableLayoutManager.column("qrg", qrgCol),
|
||||||
|
TableLayoutManager.column("tropo", tropoCol),
|
||||||
|
TableLayoutManager.column("score", priorityScoreCol),
|
||||||
|
TableLayoutManager.column("activity", lastActCol),
|
||||||
|
TableLayoutManager.column("airscout", airScoutCol).maximumInitialWidth(190),
|
||||||
|
TableLayoutManager.column("worked-any", wkdAny_subcol),
|
||||||
|
TableLayoutManager.column("band-50", sixMCol_subcol),
|
||||||
|
TableLayoutManager.column("band-70", fourMCol_subcol),
|
||||||
|
TableLayoutManager.column("band-144", vhfCol_subcol),
|
||||||
|
TableLayoutManager.column("band-432", uhfCol_subcol),
|
||||||
|
TableLayoutManager.column("band-1296", shf23_subcol),
|
||||||
|
TableLayoutManager.column("band-2320", shf13_subcol),
|
||||||
|
TableLayoutManager.column("band-3400", shf9_subcol),
|
||||||
|
TableLayoutManager.column("band-5760", shf6_subcol),
|
||||||
|
TableLayoutManager.column("band-10g", shf3_subcol),
|
||||||
|
TableLayoutManager.column("not-qrv", notQRVCol).maximumInitialWidth(180),
|
||||||
|
TableLayoutManager.column("category", chatCategoryCol)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3120,6 +3144,24 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
ObservableList<ChatMessage> toOtherMSGList = chatcontroller.getLst_toOtherMessageList();
|
ObservableList<ChatMessage> toOtherMSGList = chatcontroller.getLst_toOtherMessageList();
|
||||||
tbl_furtherInfoAbtCallsignMSGTable.setItems(chatcontroller.getLst_selectedCallSignInfofilteredMessageList());
|
tbl_furtherInfoAbtCallsignMSGTable.setItems(chatcontroller.getLst_selectedCallSignInfofilteredMessageList());
|
||||||
|
applyTruncatedTextCells(
|
||||||
|
timeCol, callSignTRCVCol, callSignRCVRCol, qrgTXerCol,
|
||||||
|
qrgRXerCol, workedRXCol, workedTXCol
|
||||||
|
);
|
||||||
|
TableLayoutManager.install(
|
||||||
|
tbl_furtherInfoAbtCallsignMSGTable,
|
||||||
|
"selected-station-messages",
|
||||||
|
chatcontroller.getChatPreferences(),
|
||||||
|
layoutAutosave,
|
||||||
|
TableLayoutManager.column("time", timeCol),
|
||||||
|
TableLayoutManager.column("call-tx", callSignTRCVCol),
|
||||||
|
TableLayoutManager.column("call-rx", callSignRCVRCol),
|
||||||
|
TableLayoutManager.column("last-qrg-tx", qrgTXerCol),
|
||||||
|
TableLayoutManager.column("last-qrg-rx", qrgRXerCol),
|
||||||
|
TableLayoutManager.column("message", msgCol).flexible(360),
|
||||||
|
TableLayoutManager.column("worked-rx", workedRXCol),
|
||||||
|
TableLayoutManager.column("worked-tx", workedTXCol)
|
||||||
|
);
|
||||||
|
|
||||||
return tbl_furtherInfoAbtCallsignMSGTable;
|
return tbl_furtherInfoAbtCallsignMSGTable;
|
||||||
}
|
}
|
||||||
@@ -3281,11 +3323,11 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
Tab dxClusterMessagesTab = new Tab("DXCluster messages");
|
Tab dxClusterMessagesTab = new Tab("DXCluster messages");
|
||||||
dxClusterMessagesTab.setTooltip(new Tooltip("DXCluster spots."));
|
dxClusterMessagesTab.setTooltip(new Tooltip("DXCluster spots."));
|
||||||
dxClusterMessagesTab.setContent(initDXClusterTable());
|
dxClusterMessagesTab.setContent(initDXClusterTable("dx-cluster-main"));
|
||||||
|
|
||||||
Tab qsoOfTheOtherTab = new Tab("QSO of the other");
|
Tab qsoOfTheOtherTab = new Tab("QSO of the other");
|
||||||
qsoOfTheOtherTab.setTooltip(new Tooltip("Messages between other stations. This view is not tied to the selected ChatMember."));
|
qsoOfTheOtherTab.setTooltip(new Tooltip("Messages between other stations. This view is not tied to the selected ChatMember."));
|
||||||
qsoOfTheOtherTab.setContent(initChatToOtherMSGTable());
|
qsoOfTheOtherTab.setContent(initChatToOtherMSGTable("qso-other-main"));
|
||||||
|
|
||||||
bottomMessageTabs.getTabs().addAll(publicMessagesTab, dxClusterMessagesTab, qsoOfTheOtherTab);
|
bottomMessageTabs.getTabs().addAll(publicMessagesTab, dxClusterMessagesTab, qsoOfTheOtherTab);
|
||||||
bottomMessageTabs.getSelectionModel().select(publicMessagesTab);
|
bottomMessageTabs.getSelectionModel().select(publicMessagesTab);
|
||||||
@@ -3437,6 +3479,19 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
ObservableList<ChatMessage> generalMSGList = chatcontroller.getLst_toAllMessageList();
|
ObservableList<ChatMessage> generalMSGList = chatcontroller.getLst_toAllMessageList();
|
||||||
tbl_generalMSGTable.setItems(generalMSGList);
|
tbl_generalMSGTable.setItems(generalMSGList);
|
||||||
|
applyTruncatedTextCells(timeCol, callSignCol, nameCol, categoryCol);
|
||||||
|
TableLayoutManager.install(
|
||||||
|
tbl_generalMSGTable,
|
||||||
|
"public-messages",
|
||||||
|
chatcontroller.getChatPreferences(),
|
||||||
|
layoutAutosave,
|
||||||
|
TableLayoutManager.column("time", timeCol),
|
||||||
|
TableLayoutManager.column("callsign", callSignCol),
|
||||||
|
TableLayoutManager.column("name", nameCol).maximumInitialWidth(220),
|
||||||
|
TableLayoutManager.column("message", msgCol).flexible(360),
|
||||||
|
TableLayoutManager.column("last-qrg", qrgCol),
|
||||||
|
TableLayoutManager.column("category", categoryCol)
|
||||||
|
);
|
||||||
|
|
||||||
tbl_generalMSGTable.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
|
tbl_generalMSGTable.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -3660,7 +3715,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
*/
|
*/
|
||||||
airScoutCol.setCellFactory(new Callback<TableColumn<ChatMessage, String>, TableCell<ChatMessage, String>>() {
|
airScoutCol.setCellFactory(new Callback<TableColumn<ChatMessage, String>, TableCell<ChatMessage, String>>() {
|
||||||
public TableCell call(TableColumn param) {
|
public TableCell call(TableColumn param) {
|
||||||
return new TableCell<ChatMessage, String>() {
|
return new TruncatedTextTableCell<ChatMessage>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateItem(String item, boolean empty) {
|
public void updateItem(String item, boolean empty) {
|
||||||
@@ -3682,7 +3737,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setText(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -3741,6 +3795,22 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
ObservableList<ChatMessage> privateMSGList = chatcontroller.getLst_toMeMessageList();
|
ObservableList<ChatMessage> privateMSGList = chatcontroller.getLst_toMeMessageList();
|
||||||
tbl_privateMSGTable.setItems(privateMSGList);
|
tbl_privateMSGTable.setItems(privateMSGList);
|
||||||
|
applyTruncatedTextCells(timeCol, callSignCol, nameCol, qraCol, qrbCol, categoryCol);
|
||||||
|
TableLayoutManager.install(
|
||||||
|
tbl_privateMSGTable,
|
||||||
|
"private-messages",
|
||||||
|
chatcontroller.getChatPreferences(),
|
||||||
|
layoutAutosave,
|
||||||
|
TableLayoutManager.column("time", timeCol),
|
||||||
|
TableLayoutManager.column("callsign", callSignCol),
|
||||||
|
TableLayoutManager.column("name", nameCol).maximumInitialWidth(220),
|
||||||
|
TableLayoutManager.column("qra", qraCol),
|
||||||
|
TableLayoutManager.column("qrb", qrbCol),
|
||||||
|
TableLayoutManager.column("message", msgCol).flexible(360),
|
||||||
|
TableLayoutManager.column("last-qrg", qrgCol),
|
||||||
|
TableLayoutManager.column("airscout", airScoutCol).maximumInitialWidth(190),
|
||||||
|
TableLayoutManager.column("category", categoryCol)
|
||||||
|
);
|
||||||
|
|
||||||
tbl_privateMSGTable.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
|
tbl_privateMSGTable.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -3812,7 +3882,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
return tbl_privateMSGTable;
|
return tbl_privateMSGTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TableView<ClusterMessage> initDXClusterTable() {
|
private TableView<ClusterMessage> initDXClusterTable(String layoutId) {
|
||||||
|
|
||||||
TableView<ClusterMessage> tbl_DXCTable = new TableView<ClusterMessage>();
|
TableView<ClusterMessage> tbl_DXCTable = new TableView<ClusterMessage>();
|
||||||
// tbl_DXCTable.setTooltip(new Tooltip("Cluster Messages are shown here"));
|
// tbl_DXCTable.setTooltip(new Tooltip("Cluster Messages are shown here"));
|
||||||
@@ -3973,11 +4043,29 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
ObservableList<ClusterMessage> clusterMSGList = chatcontroller.getLst_clusterMemberList();
|
ObservableList<ClusterMessage> clusterMSGList = chatcontroller.getLst_clusterMemberList();
|
||||||
tbl_DXCTable.setItems(clusterMSGList);
|
tbl_DXCTable.setItems(clusterMSGList);
|
||||||
|
applyTruncatedTextCells(
|
||||||
|
timeCol, callSignCol, locTXCol, callSignRXCol,
|
||||||
|
locRXCol, workedCol
|
||||||
|
);
|
||||||
|
TableLayoutManager.install(
|
||||||
|
tbl_DXCTable,
|
||||||
|
layoutId,
|
||||||
|
chatcontroller.getChatPreferences(),
|
||||||
|
layoutAutosave,
|
||||||
|
TableLayoutManager.column("time", timeCol),
|
||||||
|
TableLayoutManager.column("call-tx", callSignCol),
|
||||||
|
TableLayoutManager.column("locator-tx", locTXCol),
|
||||||
|
TableLayoutManager.column("call-rx", callSignRXCol),
|
||||||
|
TableLayoutManager.column("locator-rx", locRXCol),
|
||||||
|
TableLayoutManager.column("qrg", qrgCol),
|
||||||
|
TableLayoutManager.column("message", msgCol).flexible(360),
|
||||||
|
TableLayoutManager.column("worked", workedCol)
|
||||||
|
);
|
||||||
|
|
||||||
return tbl_DXCTable;
|
return tbl_DXCTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TableView<ChatMessage> initChatToOtherMSGTable() {
|
private TableView<ChatMessage> initChatToOtherMSGTable(String layoutId) {
|
||||||
|
|
||||||
TableView<ChatMessage> tbl_toOtherMSGTable = new TableView<ChatMessage>();
|
TableView<ChatMessage> tbl_toOtherMSGTable = new TableView<ChatMessage>();
|
||||||
// tbl_toOtherMSGTable.setTooltip(new Tooltip("Messages between other member are shown here"));
|
// tbl_toOtherMSGTable.setTooltip(new Tooltip("Messages between other member are shown here"));
|
||||||
@@ -4175,6 +4263,25 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
ObservableList<ChatMessage> toOtherMSGList = chatcontroller.getLst_toOtherMessageList();
|
ObservableList<ChatMessage> toOtherMSGList = chatcontroller.getLst_toOtherMessageList();
|
||||||
tbl_toOtherMSGTable.setItems(toOtherMSGList);
|
tbl_toOtherMSGTable.setItems(toOtherMSGList);
|
||||||
|
applyTruncatedTextCells(
|
||||||
|
timeCol, callSignTRCVCol, qrgTXerCol, workedTXCol,
|
||||||
|
callSignRCVRCol, qrgRXerCol, workedRXCol, categoryCol
|
||||||
|
);
|
||||||
|
TableLayoutManager.install(
|
||||||
|
tbl_toOtherMSGTable,
|
||||||
|
layoutId,
|
||||||
|
chatcontroller.getChatPreferences(),
|
||||||
|
layoutAutosave,
|
||||||
|
TableLayoutManager.column("time", timeCol),
|
||||||
|
TableLayoutManager.column("call-tx", callSignTRCVCol),
|
||||||
|
TableLayoutManager.column("last-qrg-tx", qrgTXerCol),
|
||||||
|
TableLayoutManager.column("worked-tx", workedTXCol),
|
||||||
|
TableLayoutManager.column("call-rx", callSignRCVRCol),
|
||||||
|
TableLayoutManager.column("last-qrg-rx", qrgRXerCol),
|
||||||
|
TableLayoutManager.column("worked-rx", workedRXCol),
|
||||||
|
TableLayoutManager.column("message", msgCol).flexible(360),
|
||||||
|
TableLayoutManager.column("category", categoryCol)
|
||||||
|
);
|
||||||
|
|
||||||
return tbl_toOtherMSGTable;
|
return tbl_toOtherMSGTable;
|
||||||
}
|
}
|
||||||
@@ -5261,6 +5368,28 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
tbl_chatMemberWkdDBTable.getColumns().addAll(callSignCol, workedCol);
|
tbl_chatMemberWkdDBTable.getColumns().addAll(callSignCol, workedCol);
|
||||||
|
|
||||||
tbl_chatMemberWkdDBTable.setItems(chatcontroller.getLst_DBBasedWkdCallSignList());
|
tbl_chatMemberWkdDBTable.setItems(chatcontroller.getLst_DBBasedWkdCallSignList());
|
||||||
|
applyTruncatedTextCells(
|
||||||
|
callSignCol, wkdAny_subcol, sixMCol_subcol, fourMCol_subcol,
|
||||||
|
vhfCol_subcol, uhfCol_subcol, shf23_subcol, shf13_subcol,
|
||||||
|
shf9_subcol, shf6_subcol, shf3_subcol
|
||||||
|
);
|
||||||
|
TableLayoutManager.install(
|
||||||
|
tbl_chatMemberWkdDBTable,
|
||||||
|
"worked-database",
|
||||||
|
chatcontroller.getChatPreferences(),
|
||||||
|
layoutAutosave,
|
||||||
|
TableLayoutManager.column("callsign", callSignCol),
|
||||||
|
TableLayoutManager.column("worked-any", wkdAny_subcol),
|
||||||
|
TableLayoutManager.column("band-50", sixMCol_subcol),
|
||||||
|
TableLayoutManager.column("band-70", fourMCol_subcol),
|
||||||
|
TableLayoutManager.column("band-144", vhfCol_subcol),
|
||||||
|
TableLayoutManager.column("band-432", uhfCol_subcol),
|
||||||
|
TableLayoutManager.column("band-1296", shf23_subcol),
|
||||||
|
TableLayoutManager.column("band-2320", shf13_subcol),
|
||||||
|
TableLayoutManager.column("band-3400", shf9_subcol),
|
||||||
|
TableLayoutManager.column("band-5760", shf6_subcol),
|
||||||
|
TableLayoutManager.column("band-10g", shf3_subcol)
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: https://www.youtube.com/watch?v=M_kp20qrtLw = tutorial dafuer
|
// TODO: https://www.youtube.com/watch?v=M_kp20qrtLw = tutorial dafuer
|
||||||
|
|
||||||
@@ -6100,6 +6229,9 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
System.out.println("[Main.java, Info:] Stage is closing, killing all resources");
|
System.out.println("[Main.java, Info:] Stage is closing, killing all resources");
|
||||||
|
if (layoutAutosave != null) {
|
||||||
|
layoutAutosave.flushPending();
|
||||||
|
}
|
||||||
timer_buildWindowTitle.purge();
|
timer_buildWindowTitle.purge();
|
||||||
timer_buildWindowTitle.cancel();
|
timer_buildWindowTitle.cancel();
|
||||||
|
|
||||||
@@ -6119,6 +6251,12 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void requestLayoutSave() {
|
||||||
|
if (layoutAutosave != null) {
|
||||||
|
layoutAutosave.requestSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Queue<Media> musicList = new LinkedList<Media>();
|
private Queue<Media> musicList = new LinkedList<Media>();
|
||||||
private MediaPlayer mediaPlayer ;
|
private MediaPlayer mediaPlayer ;
|
||||||
|
|
||||||
@@ -6544,6 +6682,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
ChatMember ownChatMemberObject = new ChatMember();
|
ChatMember ownChatMemberObject = new ChatMember();
|
||||||
|
|
||||||
chatcontroller = new ChatController(ownChatMemberObject, this); // instantiate the Chatcontroller with the user object
|
chatcontroller = new ChatController(ownChatMemberObject, this); // instantiate the Chatcontroller with the user object
|
||||||
|
layoutAutosave = new LayoutAutosave(chatcontroller.getChatPreferences());
|
||||||
messageVariableResolver = new MessageVariableResolver(chatcontroller.getChatPreferences());
|
messageVariableResolver = new MessageVariableResolver(chatcontroller.getChatPreferences());
|
||||||
chatcontroller.setStatusListener(this); //callback interface for updating Thread events in visual
|
chatcontroller.setStatusListener(this); //callback interface for updating Thread events in visual
|
||||||
|
|
||||||
@@ -6687,6 +6826,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
@Override
|
@Override
|
||||||
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newWidthValue) {
|
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newWidthValue) {
|
||||||
chatcontroller.getChatPreferences().getGUIscn_ChatwindowMainSceneSizeHW()[1] = newWidthValue.doubleValue();
|
chatcontroller.getChatPreferences().getGUIscn_ChatwindowMainSceneSizeHW()[1] = newWidthValue.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -6694,6 +6834,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
@Override
|
@Override
|
||||||
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newHeightValue) {
|
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newHeightValue) {
|
||||||
chatcontroller.getChatPreferences().getGUIscn_ChatwindowMainSceneSizeHW()[0] = newHeightValue.doubleValue();
|
chatcontroller.getChatPreferences().getGUIscn_ChatwindowMainSceneSizeHW()[0] = newHeightValue.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7399,6 +7540,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
|
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
|
||||||
System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<< devider>>>>>> " + messageSectionSplitpane.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + messageSectionSplitpane.getDividers().size());
|
System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<< devider>>>>>> " + messageSectionSplitpane.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + messageSectionSplitpane.getDividers().size());
|
||||||
chatcontroller.getChatPreferences().getGUImessageSectionSplitpane_dividerposition()[messageSectionSplitpane.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
|
chatcontroller.getChatPreferences().getGUImessageSectionSplitpane_dividerposition()[messageSectionSplitpane.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -8582,6 +8724,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
|
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
|
||||||
System.out.println("<<<<<<<<<<<<<<<<<<< mainWindowLeftSplitPanedevider " + mainWindowLeftSplitPane.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + mainWindowLeftSplitPane.getDividers().size());
|
System.out.println("<<<<<<<<<<<<<<<<<<< mainWindowLeftSplitPanedevider " + mainWindowLeftSplitPane.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + mainWindowLeftSplitPane.getDividers().size());
|
||||||
chatcontroller.getChatPreferences().getGUImainWindowLeftSplitPane_dividerposition()[mainWindowLeftSplitPane.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
|
chatcontroller.getChatPreferences().getGUImainWindowLeftSplitPane_dividerposition()[mainWindowLeftSplitPane.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -8615,6 +8758,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
if (dividerIndex >= 0 && dividerIndex < storedPositions.length) {
|
if (dividerIndex >= 0 && dividerIndex < storedPositions.length) {
|
||||||
storedPositions[dividerIndex] = newDividerPosition.doubleValue();
|
storedPositions[dividerIndex] = newDividerPosition.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
} else {
|
} else {
|
||||||
// Avoid crashes if preferences are older than the current UI layout.
|
// Avoid crashes if preferences are older than the current UI layout.
|
||||||
System.out.println("WARN: cannot store mainWindowRightSplitPane divider position: index="
|
System.out.println("WARN: cannot store mainWindowRightSplitPane divider position: index="
|
||||||
@@ -8663,7 +8807,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
SplitPane pnl_directedMSGWin = new SplitPane();
|
SplitPane pnl_directedMSGWin = new SplitPane();
|
||||||
pnl_directedMSGWin.setOrientation(Orientation.VERTICAL);
|
pnl_directedMSGWin.setOrientation(Orientation.VERTICAL);
|
||||||
pnl_directedMSGWin.setDividerPositions(chatcontroller.getChatPreferences().getGUIpnl_directedMSGWin_dividerpositionDefault());
|
pnl_directedMSGWin.setDividerPositions(chatcontroller.getChatPreferences().getGUIpnl_directedMSGWin_dividerpositionDefault());
|
||||||
pnl_directedMSGWin.getItems().addAll(initDXClusterTable(), initChatToOtherMSGTable());
|
pnl_directedMSGWin.getItems().addAll(
|
||||||
|
initDXClusterTable("dx-cluster-monitor"),
|
||||||
|
initChatToOtherMSGTable("qso-other-monitor")
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8676,6 +8823,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
|
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
|
||||||
System.out.println("<<<<<<<<<<<<<<<<<<<|||||||||||||||||||| devider " + pnl_directedMSGWin.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + pnl_directedMSGWin.getDividers().size());
|
System.out.println("<<<<<<<<<<<<<<<<<<<|||||||||||||||||||| devider " + pnl_directedMSGWin.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + pnl_directedMSGWin.getDividers().size());
|
||||||
chatcontroller.getChatPreferences().getGUIpnl_directedMSGWin_dividerpositionDefault()[pnl_directedMSGWin.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
|
chatcontroller.getChatPreferences().getGUIpnl_directedMSGWin_dividerpositionDefault()[pnl_directedMSGWin.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -8689,6 +8837,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
@Override
|
@Override
|
||||||
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newHeightValue) {
|
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newHeightValue) {
|
||||||
chatcontroller.getChatPreferences().getGUIclusterAndQSOMonStage_SceneSizeHW()[1] = newHeightValue.doubleValue();
|
chatcontroller.getChatPreferences().getGUIclusterAndQSOMonStage_SceneSizeHW()[1] = newHeightValue.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -8696,6 +8845,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
@Override
|
@Override
|
||||||
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newWidthValue) {
|
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newWidthValue) {
|
||||||
chatcontroller.getChatPreferences().getGUIclusterAndQSOMonStage_SceneSizeHW()[0] = newWidthValue.doubleValue();
|
chatcontroller.getChatPreferences().getGUIclusterAndQSOMonStage_SceneSizeHW()[0] = newWidthValue.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -8809,6 +8959,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
System.out.println("SRVR Version: " + chatcontroller.getUpdateInformation().getLatestVersionNumberOnServer() + " // installed version " + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER);
|
System.out.println("SRVR Version: " + chatcontroller.getUpdateInformation().getLatestVersionNumberOnServer() + " // installed version " + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER);
|
||||||
|
|
||||||
stage_updateStage.setScene(new Scene(vbxUpdateWindow, chatcontroller.getChatPreferences().getGUIstage_updateStage_SceneSizeHW()[0], chatcontroller.getChatPreferences().getGUIstage_updateStage_SceneSizeHW()[1]));
|
stage_updateStage.setScene(new Scene(vbxUpdateWindow, chatcontroller.getChatPreferences().getGUIstage_updateStage_SceneSizeHW()[0], chatcontroller.getChatPreferences().getGUIstage_updateStage_SceneSizeHW()[1]));
|
||||||
|
stage_updateStage.getScene().widthProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
chatcontroller.getChatPreferences().getGUIstage_updateStage_SceneSizeHW()[0] = newValue.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
|
});
|
||||||
|
stage_updateStage.getScene().heightProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
chatcontroller.getChatPreferences().getGUIstage_updateStage_SceneSizeHW()[1] = newValue.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// if (chatcontroller.getUpdateInformation().getLatestVersionNumberOnServer() > ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER) {
|
// if (chatcontroller.getUpdateInformation().getLatestVersionNumberOnServer() > ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER) {
|
||||||
@@ -11762,7 +11920,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
System.out.println("saved");
|
System.out.println("saved");
|
||||||
|
|
||||||
chatcontroller.getChatPreferences().writePreferencesToXmlFile();
|
if (chatcontroller.getChatPreferences().writePreferencesToXmlFile()
|
||||||
|
&& layoutAutosave != null) {
|
||||||
|
layoutAutosave.cancelPending();
|
||||||
|
}
|
||||||
Alert a = new Alert(AlertType.INFORMATION);
|
Alert a = new Alert(AlertType.INFORMATION);
|
||||||
|
|
||||||
a.setTitle("Info");
|
a.setTitle("Info");
|
||||||
@@ -11800,6 +11961,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
// VBox vBox = new VBox(tabPaneOptions);
|
// VBox vBox = new VBox(tabPaneOptions);
|
||||||
settingsScene = new Scene(optionsPanel, chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[0], chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[1]);
|
settingsScene = new Scene(optionsPanel, chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[0], chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[1]);
|
||||||
settingsScene.getStylesheets().add(ApplicationConstants.STYLECSSFILE_DEFAULT_DAYLIGHT);
|
settingsScene.getStylesheets().add(ApplicationConstants.STYLECSSFILE_DEFAULT_DAYLIGHT);
|
||||||
|
settingsScene.widthProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[0] = newValue.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
|
});
|
||||||
|
settingsScene.heightProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[1] = newValue.doubleValue();
|
||||||
|
requestLayoutSave();
|
||||||
|
});
|
||||||
|
|
||||||
settingsStage.setScene(settingsScene);
|
settingsStage.setScene(settingsScene);
|
||||||
|
|
||||||
@@ -12515,13 +12684,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static <T> void applyQrgUiFormatting(TableColumn<T, String> col) {
|
private static <T> void applyQrgUiFormatting(TableColumn<T, String> col) {
|
||||||
col.setCellFactory(tc -> new TableCell<T, String>() {
|
col.setCellFactory(tc -> new TruncatedTextTableCell<>(Kst4ContestApplication::formatQrgForUi));
|
||||||
@Override
|
}
|
||||||
protected void updateItem(String item, boolean empty) {
|
|
||||||
super.updateItem(item, empty);
|
@SafeVarargs
|
||||||
setText(empty ? "" : formatQrgForUi(item));
|
private static <T> void applyTruncatedTextCells(TableColumn<T, String>... columns) {
|
||||||
}
|
for (TableColumn<T, String> column : columns) {
|
||||||
});
|
column.setCellFactory(ignored -> new TruncatedTextTableCell<>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package kst4contest.view;
|
||||||
|
|
||||||
|
import javafx.animation.PauseTransition;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
import kst4contest.model.ChatPreferences;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coalesces JavaFX layout changes into selective preferences writes.
|
||||||
|
*/
|
||||||
|
public final class LayoutAutosave {
|
||||||
|
|
||||||
|
private static final Duration SAVE_DELAY = Duration.millis(750);
|
||||||
|
|
||||||
|
private final ChatPreferences preferences;
|
||||||
|
private final PauseTransition saveDelay = new PauseTransition(SAVE_DELAY);
|
||||||
|
private boolean pending;
|
||||||
|
|
||||||
|
public LayoutAutosave(ChatPreferences preferences) {
|
||||||
|
this.preferences = Objects.requireNonNull(preferences, "preferences");
|
||||||
|
saveDelay.setOnFinished(event -> flushPending());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestSave() {
|
||||||
|
if (!Platform.isFxApplicationThread()) {
|
||||||
|
Platform.runLater(this::requestSave);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pending = true;
|
||||||
|
saveDelay.playFromStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void flushPending() {
|
||||||
|
if (!pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDelay.stop();
|
||||||
|
pending = false;
|
||||||
|
preferences.writeLayoutPreferencesToXmlFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelPending() {
|
||||||
|
saveDelay.stop();
|
||||||
|
pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
package kst4contest.view;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.TableColumn;
|
||||||
|
import javafx.scene.control.TableView;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.text.Font;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import kst4contest.model.ChatPreferences;
|
||||||
|
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.OptionalDouble;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies persisted leaf-column widths and performs one content-based initial
|
||||||
|
* sizing pass when no width has been stored yet.
|
||||||
|
*/
|
||||||
|
public final class TableLayoutManager {
|
||||||
|
|
||||||
|
private static final double CELL_HORIZONTAL_PADDING = 28.0;
|
||||||
|
private static final double DEFAULT_MINIMUM_WIDTH = 42.0;
|
||||||
|
|
||||||
|
private TableLayoutManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ColumnSpec column(String id, TableColumn<?, String> column) {
|
||||||
|
return new ColumnSpec(id, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <S> void install(
|
||||||
|
TableView<S> table,
|
||||||
|
String tableId,
|
||||||
|
ChatPreferences preferences,
|
||||||
|
LayoutAutosave autosave,
|
||||||
|
ColumnSpec... columnSpecs
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(table, "table");
|
||||||
|
Objects.requireNonNull(tableId, "tableId");
|
||||||
|
Objects.requireNonNull(preferences, "preferences");
|
||||||
|
Objects.requireNonNull(autosave, "autosave");
|
||||||
|
|
||||||
|
Map<TableColumn<?, String>, ColumnState> states = new IdentityHashMap<>();
|
||||||
|
for (ColumnSpec spec : columnSpecs) {
|
||||||
|
if (spec == null || !spec.column.getColumns().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spec.column.prefWidthProperty().isBound()) {
|
||||||
|
spec.column.prefWidthProperty().unbind();
|
||||||
|
}
|
||||||
|
spec.column.setId(tableId + "." + spec.id);
|
||||||
|
|
||||||
|
OptionalDouble storedWidth = preferences.getTableColumnWidth(tableId, spec.id);
|
||||||
|
ColumnState state = new ColumnState(spec, storedWidth.isEmpty());
|
||||||
|
states.put(spec.column, state);
|
||||||
|
|
||||||
|
if (storedWidth.isPresent()) {
|
||||||
|
setWidth(state, storedWidth.getAsDouble());
|
||||||
|
state.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.column.widthProperty().addListener((observable, oldWidth, newWidth) -> {
|
||||||
|
if (state.adjusting || !state.initialized || newWidth == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storeWidth(preferences, autosave, tableId, state.spec.id, newWidth.doubleValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
installEarlyManualResizeDetection(table, tableId, preferences, autosave, states);
|
||||||
|
scheduleInitialSizingWhenUsable(table, tableId, preferences, autosave, states);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <S> void scheduleInitialSizingWhenUsable(
|
||||||
|
TableView<S> table,
|
||||||
|
String tableId,
|
||||||
|
ChatPreferences preferences,
|
||||||
|
LayoutAutosave autosave,
|
||||||
|
Map<TableColumn<?, String>, ColumnState> states
|
||||||
|
) {
|
||||||
|
if (states.values().stream().noneMatch(state -> !state.initialized)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table.getItems() != null && !table.getItems().isEmpty()) {
|
||||||
|
Platform.runLater(() -> sizePendingColumns(table, tableId, preferences, autosave, states));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
final ListChangeListener<S>[] holder = new ListChangeListener[1];
|
||||||
|
holder[0] = change -> {
|
||||||
|
if (table.getItems() == null || table.getItems().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
table.getItems().removeListener(holder[0]);
|
||||||
|
Platform.runLater(() -> sizePendingColumns(table, tableId, preferences, autosave, states));
|
||||||
|
};
|
||||||
|
table.getItems().addListener(holder[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <S> void sizePendingColumns(
|
||||||
|
TableView<S> table,
|
||||||
|
String tableId,
|
||||||
|
ChatPreferences preferences,
|
||||||
|
LayoutAutosave autosave,
|
||||||
|
Map<TableColumn<?, String>, ColumnState> states
|
||||||
|
) {
|
||||||
|
for (ColumnState state : states.values()) {
|
||||||
|
if (state.initialized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double width = state.spec.flexible
|
||||||
|
? flexibleInitialWidth(table, state.spec)
|
||||||
|
: contentInitialWidth(table, state.spec);
|
||||||
|
setWidth(state, width);
|
||||||
|
state.initialized = true;
|
||||||
|
storeWidth(preferences, autosave, tableId, state.spec.id, width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <S> double contentInitialWidth(TableView<S> table, ColumnSpec spec) {
|
||||||
|
Text measurement = new Text();
|
||||||
|
measurement.setFont(Font.getDefault());
|
||||||
|
double requiredWidth = measure(spec.column.getText(), measurement);
|
||||||
|
for (int rowIndex = 0; rowIndex < table.getItems().size(); rowIndex++) {
|
||||||
|
ObservableValue<?> value = spec.column.getCellObservableValue(rowIndex);
|
||||||
|
if (value == null || value.getValue() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
requiredWidth = Math.max(
|
||||||
|
requiredWidth,
|
||||||
|
measure(String.valueOf(value.getValue()), measurement)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return clamp(requiredWidth + CELL_HORIZONTAL_PADDING, spec.minimumWidth, spec.maximumInitialWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <S> double flexibleInitialWidth(TableView<S> table, ColumnSpec spec) {
|
||||||
|
double tableShare = table.getWidth() > 1.0 ? table.getWidth() * 0.42 : spec.flexibleFallbackWidth;
|
||||||
|
Text measurement = new Text();
|
||||||
|
measurement.setFont(Font.getDefault());
|
||||||
|
double headerWidth = measure(spec.column.getText(), measurement) + CELL_HORIZONTAL_PADDING;
|
||||||
|
return clamp(Math.max(headerWidth, tableShare), spec.minimumWidth, spec.maximumInitialWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <S> void installEarlyManualResizeDetection(
|
||||||
|
TableView<S> table,
|
||||||
|
String tableId,
|
||||||
|
ChatPreferences preferences,
|
||||||
|
LayoutAutosave autosave,
|
||||||
|
Map<TableColumn<?, String>, ColumnState> states
|
||||||
|
) {
|
||||||
|
Map<TableColumn<?, String>, Double> widthsAtHeaderPress = new IdentityHashMap<>();
|
||||||
|
table.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||||
|
if (!isColumnHeaderEvent(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
widthsAtHeaderPress.clear();
|
||||||
|
states.forEach((column, state) -> widthsAtHeaderPress.put(column, column.getWidth()));
|
||||||
|
});
|
||||||
|
table.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> {
|
||||||
|
if (widthsAtHeaderPress.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
states.forEach((column, state) -> {
|
||||||
|
Double oldWidth = widthsAtHeaderPress.get(column);
|
||||||
|
if (oldWidth == null || Math.abs(oldWidth - column.getWidth()) <= 0.5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.initialized = true;
|
||||||
|
storeWidth(preferences, autosave, tableId, state.spec.id, column.getWidth());
|
||||||
|
});
|
||||||
|
widthsAtHeaderPress.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isColumnHeaderEvent(MouseEvent event) {
|
||||||
|
Object target = event.getTarget();
|
||||||
|
Node node = target instanceof Node ? (Node) target : null;
|
||||||
|
while (node != null && node.getParent() != null) {
|
||||||
|
if (node.getStyleClass().contains("column-header")
|
||||||
|
|| node.getStyleClass().contains("nested-column-header")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
node = node.getParent();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setWidth(ColumnState state, double width) {
|
||||||
|
state.adjusting = true;
|
||||||
|
try {
|
||||||
|
state.spec.column.setPrefWidth(width);
|
||||||
|
} finally {
|
||||||
|
state.adjusting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void storeWidth(
|
||||||
|
ChatPreferences preferences,
|
||||||
|
LayoutAutosave autosave,
|
||||||
|
String tableId,
|
||||||
|
String columnId,
|
||||||
|
double width
|
||||||
|
) {
|
||||||
|
preferences.setTableColumnWidth(tableId, columnId, width);
|
||||||
|
autosave.requestSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double measure(String value, Text measurement) {
|
||||||
|
measurement.setText(value == null ? "" : value);
|
||||||
|
return measurement.getLayoutBounds().getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double clamp(double value, double minimum, double maximum) {
|
||||||
|
return Math.max(minimum, Math.min(value, maximum));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ColumnSpec {
|
||||||
|
private final String id;
|
||||||
|
private final TableColumn<?, String> column;
|
||||||
|
private double minimumWidth = DEFAULT_MINIMUM_WIDTH;
|
||||||
|
private double maximumInitialWidth = Double.MAX_VALUE;
|
||||||
|
private double flexibleFallbackWidth = 320.0;
|
||||||
|
private boolean flexible;
|
||||||
|
|
||||||
|
private ColumnSpec(String id, TableColumn<?, String> column) {
|
||||||
|
if (id == null || id.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Column id must not be blank");
|
||||||
|
}
|
||||||
|
this.id = id;
|
||||||
|
this.column = Objects.requireNonNull(column, "column");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColumnSpec maximumInitialWidth(double maximumInitialWidth) {
|
||||||
|
this.maximumInitialWidth = maximumInitialWidth;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColumnSpec flexible(double fallbackWidth) {
|
||||||
|
flexible = true;
|
||||||
|
flexibleFallbackWidth = fallbackWidth;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ColumnState {
|
||||||
|
private final ColumnSpec spec;
|
||||||
|
private boolean adjusting;
|
||||||
|
private boolean initialized;
|
||||||
|
|
||||||
|
private ColumnState(ColumnSpec spec, boolean awaitingInitialSizing) {
|
||||||
|
this.spec = spec;
|
||||||
|
initialized = !awaitingInitialSizing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package kst4contest.view;
|
||||||
|
|
||||||
|
import javafx.scene.control.TableCell;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays text normally and exposes the full value only when it is clipped.
|
||||||
|
* An optional functional explanation remains available and is combined with
|
||||||
|
* the full value when both are needed.
|
||||||
|
*/
|
||||||
|
public class TruncatedTextTableCell<S> extends TableCell<S, String> {
|
||||||
|
|
||||||
|
private final Function<String, String> formatter;
|
||||||
|
private final BiFunction<S, String, String> functionalTooltipProvider;
|
||||||
|
private final Tooltip tooltip = new Tooltip();
|
||||||
|
private final Text textMeasurement = new Text();
|
||||||
|
private String fullText = "";
|
||||||
|
|
||||||
|
public TruncatedTextTableCell() {
|
||||||
|
this(Function.identity(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TruncatedTextTableCell(Function<String, String> formatter) {
|
||||||
|
this(formatter, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TruncatedTextTableCell(
|
||||||
|
Function<String, String> formatter,
|
||||||
|
BiFunction<S, String, String> functionalTooltipProvider
|
||||||
|
) {
|
||||||
|
this.formatter = formatter == null ? Function.identity() : formatter;
|
||||||
|
this.functionalTooltipProvider = functionalTooltipProvider;
|
||||||
|
tooltip.setWrapText(true);
|
||||||
|
tooltip.setMaxWidth(800);
|
||||||
|
tooltip.setShowDelay(Duration.millis(250));
|
||||||
|
tooltip.setShowDuration(Duration.seconds(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(String item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (empty || item == null) {
|
||||||
|
fullText = "";
|
||||||
|
setText(null);
|
||||||
|
setGraphic(null);
|
||||||
|
setTooltip(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatted = formatter.apply(item);
|
||||||
|
fullText = formatted == null ? "" : formatted;
|
||||||
|
setText(fullText);
|
||||||
|
setGraphic(null);
|
||||||
|
updateTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void layoutChildren() {
|
||||||
|
super.layoutChildren();
|
||||||
|
updateTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTooltip() {
|
||||||
|
if (isEmpty()) {
|
||||||
|
setTooltip(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String functionalText = resolveFunctionalTooltip();
|
||||||
|
boolean clipped = isTextClipped();
|
||||||
|
String tooltipText = TruncatedTextTooltipSupport.buildTooltipText(
|
||||||
|
fullText,
|
||||||
|
clipped,
|
||||||
|
functionalText
|
||||||
|
);
|
||||||
|
if (tooltipText == null) {
|
||||||
|
setTooltip(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip.setText(tooltipText);
|
||||||
|
setTooltip(tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveFunctionalTooltip() {
|
||||||
|
if (functionalTooltipProvider == null || getTableRow() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return functionalTooltipProvider.apply(getTableRow().getItem(), fullText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTextClipped() {
|
||||||
|
if (fullText.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
textMeasurement.setText(fullText);
|
||||||
|
textMeasurement.setFont(getFont());
|
||||||
|
double requiredWidth = textMeasurement.getLayoutBounds().getWidth();
|
||||||
|
double availableWidth = Math.max(0.0,
|
||||||
|
getWidth() - snappedLeftInset() - snappedRightInset() - 2.0);
|
||||||
|
return TruncatedTextTooltipSupport.isTextClipped(requiredWidth, availableWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package kst4contest.view;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure tooltip decisions kept separate from JavaFX controls for unit testing.
|
||||||
|
*/
|
||||||
|
final class TruncatedTextTooltipSupport {
|
||||||
|
|
||||||
|
private TruncatedTextTooltipSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isTextClipped(double requiredWidth, double availableWidth) {
|
||||||
|
return requiredWidth > availableWidth + 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String buildTooltipText(String fullText, boolean clipped, String functionalText) {
|
||||||
|
boolean hasFunctionalText = functionalText != null && !functionalText.isBlank();
|
||||||
|
if (!clipped && !hasFunctionalText) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!clipped) {
|
||||||
|
return functionalText;
|
||||||
|
}
|
||||||
|
if (!hasFunctionalText) {
|
||||||
|
return fullText;
|
||||||
|
}
|
||||||
|
return fullText + "\n\n" + functionalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ public final class StationMapView {
|
|||||||
private final Label detailPathModeValue = new Label("-");
|
private final Label detailPathModeValue = new Label("-");
|
||||||
|
|
||||||
private final ChatPreferences chatPreferences;
|
private final ChatPreferences chatPreferences;
|
||||||
|
private final Runnable layoutSaveRequester;
|
||||||
|
|
||||||
private final Stage stage = new Stage();
|
private final Stage stage = new Stage();
|
||||||
private final WebView webView = new WebView();
|
private final WebView webView = new WebView();
|
||||||
@@ -180,7 +181,12 @@ public final class StationMapView {
|
|||||||
|
|
||||||
|
|
||||||
public StationMapView(ChatPreferences chatPreferences) {
|
public StationMapView(ChatPreferences chatPreferences) {
|
||||||
|
this(chatPreferences, () -> { });
|
||||||
|
}
|
||||||
|
|
||||||
|
public StationMapView(ChatPreferences chatPreferences, Runnable layoutSaveRequester) {
|
||||||
this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences");
|
this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences");
|
||||||
|
this.layoutSaveRequester = Objects.requireNonNull(layoutSaveRequester, "layoutSaveRequester");
|
||||||
GuiUtils.applyApplicationIcon(stage);
|
GuiUtils.applyApplicationIcon(stage);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -461,17 +467,25 @@ public final class StationMapView {
|
|||||||
stage.setY(pos[1]);
|
stage.setY(pos[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
stage.widthProperty().addListener((obs, oldValue, newValue) ->
|
stage.widthProperty().addListener((obs, oldValue, newValue) -> {
|
||||||
chatPreferences.getGUIstationMapStageSceneSizeHW()[0] = newValue.doubleValue());
|
chatPreferences.getGUIstationMapStageSceneSizeHW()[0] = newValue.doubleValue();
|
||||||
|
layoutSaveRequester.run();
|
||||||
|
});
|
||||||
|
|
||||||
stage.heightProperty().addListener((obs, oldValue, newValue) ->
|
stage.heightProperty().addListener((obs, oldValue, newValue) -> {
|
||||||
chatPreferences.getGUIstationMapStageSceneSizeHW()[1] = newValue.doubleValue());
|
chatPreferences.getGUIstationMapStageSceneSizeHW()[1] = newValue.doubleValue();
|
||||||
|
layoutSaveRequester.run();
|
||||||
|
});
|
||||||
|
|
||||||
stage.xProperty().addListener((obs, oldValue, newValue) ->
|
stage.xProperty().addListener((obs, oldValue, newValue) -> {
|
||||||
chatPreferences.getGUIstationMapStagePositionXY()[0] = newValue.doubleValue());
|
chatPreferences.getGUIstationMapStagePositionXY()[0] = newValue.doubleValue();
|
||||||
|
layoutSaveRequester.run();
|
||||||
|
});
|
||||||
|
|
||||||
stage.yProperty().addListener((obs, oldValue, newValue) ->
|
stage.yProperty().addListener((obs, oldValue, newValue) -> {
|
||||||
chatPreferences.getGUIstationMapStagePositionXY()[1] = newValue.doubleValue());
|
chatPreferences.getGUIstationMapStagePositionXY()[1] = newValue.doubleValue();
|
||||||
|
layoutSaveRequester.run();
|
||||||
|
});
|
||||||
|
|
||||||
stage.setOnShown(event -> Platform.runLater(() -> {
|
stage.setOnShown(event -> Platform.runLater(() -> {
|
||||||
webView.requestFocus();
|
webView.requestFocus();
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package kst4contest.test;
|
||||||
|
|
||||||
|
import kst4contest.model.ChatPreferences;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class ChatPreferencesLayoutPersistenceTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path temporaryDirectory;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void columnWidthsSurviveFullXmlRoundTripAndLayoutsStayIndependent() {
|
||||||
|
Path preferencesFile = temporaryDirectory.resolve("preferences.xml");
|
||||||
|
ChatPreferences written = preferencesAt(preferencesFile);
|
||||||
|
written.setTableColumnWidth("dx-cluster-main", "message", 410.5);
|
||||||
|
written.setTableColumnWidth("dx-cluster-monitor", "message", 275.25);
|
||||||
|
|
||||||
|
assertTrue(written.writePreferencesToXmlFile());
|
||||||
|
|
||||||
|
ChatPreferences restored = preferencesAt(preferencesFile);
|
||||||
|
assertTrue(restored.readPreferencesFromXmlFile());
|
||||||
|
assertEquals(410.5,
|
||||||
|
restored.getTableColumnWidth("dx-cluster-main", "message").orElseThrow());
|
||||||
|
assertEquals(275.25,
|
||||||
|
restored.getTableColumnWidth("dx-cluster-monitor", "message").orElseThrow());
|
||||||
|
assertFalse(restored.getTableColumnWidth("qso-other-main", "message").isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void legacyXmlWithoutColumnWidthsKeepsWidthsAbsent() throws IOException {
|
||||||
|
Path preferencesFile = temporaryDirectory.resolve("legacy.xml");
|
||||||
|
Files.writeString(preferencesFile, """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<praktiKST>
|
||||||
|
<configVersion>5</configVersion>
|
||||||
|
<guiOptions>
|
||||||
|
<GUIscn_ChatwindowMainSceneSizeHW>768;1234</GUIscn_ChatwindowMainSceneSizeHW>
|
||||||
|
</guiOptions>
|
||||||
|
</praktiKST>
|
||||||
|
""");
|
||||||
|
|
||||||
|
ChatPreferences restored = preferencesAt(preferencesFile);
|
||||||
|
assertTrue(restored.readPreferencesFromXmlFile());
|
||||||
|
|
||||||
|
assertFalse(restored.getTableColumnWidth("public-messages", "time").isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidColumnWidthEntriesAreIgnored() throws IOException {
|
||||||
|
Path preferencesFile = temporaryDirectory.resolve("invalid.xml");
|
||||||
|
Files.writeString(preferencesFile, """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<praktiKST>
|
||||||
|
<configVersion>6</configVersion>
|
||||||
|
<guiOptions>
|
||||||
|
<tableColumnWidth tableId="public-messages" columnId="callsign" pixels="NaN"/>
|
||||||
|
<tableColumnWidth tableId="public-messages" columnId="name" pixels="-20"/>
|
||||||
|
<tableColumnWidth tableId="public-messages" columnId="category" pixels="999999"/>
|
||||||
|
<tableColumnWidth tableId="public-messages" columnId="time" pixels="88.5"/>
|
||||||
|
</guiOptions>
|
||||||
|
</praktiKST>
|
||||||
|
""");
|
||||||
|
|
||||||
|
ChatPreferences restored = preferencesAt(preferencesFile);
|
||||||
|
assertTrue(restored.readPreferencesFromXmlFile());
|
||||||
|
|
||||||
|
assertFalse(restored.getTableColumnWidth("public-messages", "callsign").isPresent());
|
||||||
|
assertFalse(restored.getTableColumnWidth("public-messages", "name").isPresent());
|
||||||
|
assertFalse(restored.getTableColumnWidth("public-messages", "category").isPresent());
|
||||||
|
assertEquals(88.5,
|
||||||
|
restored.getTableColumnWidth("public-messages", "time").orElseThrow());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectiveLayoutWritePreservesDiskSettingsAndUnknownXml() throws IOException {
|
||||||
|
Path preferencesFile = temporaryDirectory.resolve("selective.xml");
|
||||||
|
Files.writeString(preferencesFile, """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<praktiKST>
|
||||||
|
<configVersion>5</configVersion>
|
||||||
|
<station>
|
||||||
|
<LoginCallSign>SAVED-CALL</LoginCallSign>
|
||||||
|
</station>
|
||||||
|
<futureExtension mode="keep-me"><value>42</value></futureExtension>
|
||||||
|
<guiOptions>
|
||||||
|
<GUIscn_ChatwindowMainSceneSizeHW>700;1100</GUIscn_ChatwindowMainSceneSizeHW>
|
||||||
|
<futureLayoutValue>untouched</futureLayoutValue>
|
||||||
|
</guiOptions>
|
||||||
|
</praktiKST>
|
||||||
|
""");
|
||||||
|
|
||||||
|
ChatPreferences preferences = preferencesAt(preferencesFile);
|
||||||
|
assertTrue(preferences.readPreferencesFromXmlFile());
|
||||||
|
preferences.setStn_loginCallSign("UNSAVED-CALL");
|
||||||
|
preferences.getGUIscn_ChatwindowMainSceneSizeHW()[0] = 812;
|
||||||
|
preferences.getGUIscn_ChatwindowMainSceneSizeHW()[1] = 1340;
|
||||||
|
preferences.setTableColumnWidth("qso-other-monitor", "call-tx", 123.75);
|
||||||
|
|
||||||
|
assertTrue(preferences.writeLayoutPreferencesToXmlFile());
|
||||||
|
|
||||||
|
String writtenXml = Files.readString(preferencesFile);
|
||||||
|
assertTrue(writtenXml.contains("<LoginCallSign>SAVED-CALL</LoginCallSign>"));
|
||||||
|
assertFalse(writtenXml.contains("UNSAVED-CALL"));
|
||||||
|
assertTrue(writtenXml.contains("<futureExtension mode=\"keep-me\">"));
|
||||||
|
assertTrue(writtenXml.contains("<futureLayoutValue>untouched</futureLayoutValue>"));
|
||||||
|
assertTrue(writtenXml.contains("<configVersion>6</configVersion>"));
|
||||||
|
assertTrue(writtenXml.contains("<GUIscn_ChatwindowMainSceneSizeHW>812.0;1340.0"));
|
||||||
|
|
||||||
|
ChatPreferences restored = preferencesAt(preferencesFile);
|
||||||
|
assertTrue(restored.readPreferencesFromXmlFile());
|
||||||
|
assertEquals("SAVED-CALL", restored.getStn_loginCallSign());
|
||||||
|
assertEquals(123.75,
|
||||||
|
restored.getTableColumnWidth("qso-other-monitor", "call-tx").orElseThrow());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatPreferences preferencesAt(Path preferencesFile) {
|
||||||
|
ChatPreferences preferences = new ChatPreferences();
|
||||||
|
preferences.setStoreAndRestorePreferencesFileName(preferencesFile.toString());
|
||||||
|
return preferences;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package kst4contest.view;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class TruncatedTextTableCellTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void plainFullTextTooltipIsShownOnlyForClippedText() {
|
||||||
|
assertNull(TruncatedTextTooltipSupport.buildTooltipText("complete", false, null));
|
||||||
|
assertEquals("complete",
|
||||||
|
TruncatedTextTooltipSupport.buildTooltipText("complete", true, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void functionalTooltipRemainsAndCombinesWithClippedValue() {
|
||||||
|
assertEquals("Worked status",
|
||||||
|
TruncatedTextTooltipSupport.buildTooltipText("X", false, "Worked status"));
|
||||||
|
assertEquals("Long value\n\nWorked status",
|
||||||
|
TruncatedTextTooltipSupport.buildTooltipText("Long value", true, "Worked status"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clippingComparisonUsesAvailableRenderedWidth() {
|
||||||
|
assertFalse(TruncatedTextTooltipSupport.isTextClipped(100, 100));
|
||||||
|
assertTrue(TruncatedTextTooltipSupport.isTextClipped(102, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user