mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-12 12:15:33 +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:
@@ -1,11 +1,16 @@
|
||||
package kst4contest.model;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
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.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalDouble;
|
||||
|
||||
import javax.xml.XMLConstants;
|
||||
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.
|
||||
*/
|
||||
// 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.
|
||||
private static final String TAG_CONFIG_VERSION = "configVersion";
|
||||
@@ -341,6 +346,11 @@ public class ChatPreferences {
|
||||
private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 };
|
||||
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
return guiOptions_defaultFilterNothing;
|
||||
}
|
||||
@@ -1389,7 +1424,7 @@ public class ChatPreferences {
|
||||
*
|
||||
* @return true if the file writing was successful, else false
|
||||
*/
|
||||
public boolean writePreferencesToXmlFile() {
|
||||
public synchronized boolean writePreferencesToXmlFile() {
|
||||
|
||||
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
|
||||
try {
|
||||
@@ -2085,27 +2120,19 @@ public class ChatPreferences {
|
||||
);
|
||||
guiOptions.appendChild(GUIstationMapPathAnalysisVisible);
|
||||
|
||||
appendTableColumnWidths(doc, guiOptions);
|
||||
|
||||
/****************************************************************************************
|
||||
****************************** now write this XML! *************************************
|
||||
****************************************************************************************/
|
||||
|
||||
writeXml(doc, System.out);
|
||||
|
||||
// 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();
|
||||
}
|
||||
writeDocumentAtomically(doc);
|
||||
|
||||
|
||||
} catch (ParserConfigurationException | TransformerException e1) {
|
||||
} catch (ParserConfigurationException | TransformerException | IOException e1) {
|
||||
// TODO Auto-generated catch block
|
||||
e1.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
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
|
||||
private static void writeXml(Document doc, OutputStream output) throws TransformerException {
|
||||
|
||||
@@ -2920,6 +3052,22 @@ public class ChatPreferences {
|
||||
if (s5 != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
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})
|
||||
* 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 StationMapBridge stationMapBridge; //bridge for mapping actions between map and view
|
||||
private LayoutAutosave layoutAutosave;
|
||||
|
||||
private final Button btnConnectionStateIndicator = new Button("LINK");
|
||||
private final Tooltip tipConnectionStateIndicator = new Tooltip();
|
||||
@@ -186,7 +187,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
return;
|
||||
}
|
||||
|
||||
stationMapView = new StationMapView(chatcontroller.getChatPreferences());
|
||||
stationMapView = new StationMapView(
|
||||
chatcontroller.getChatPreferences(),
|
||||
this::requestLayoutSave
|
||||
);
|
||||
stationMapBridge = new StationMapBridge(
|
||||
chatcontroller,
|
||||
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
|
||||
* 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")
|
||||
.append("X = worked on this 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);
|
||||
}
|
||||
|
||||
return new Tooltip(tooltip.toString());
|
||||
return tooltip.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return column -> new TableCell<ChatMember, String>() {
|
||||
return column -> new TruncatedTextTableCell<ChatMember>(
|
||||
java.util.function.Function.identity(),
|
||||
(member, status) -> buildBandCellStatusTooltipText(member, band, status)
|
||||
) {
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
setTooltip(null);
|
||||
setStyle("");
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMember member = getTableRow() == null ? null : getTableRow().getItem();
|
||||
|
||||
setText(item);
|
||||
setTooltip(buildBandCellStatusTooltip(member, band, item));
|
||||
setAlignment(Pos.CENTER);
|
||||
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) {
|
||||
// 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();
|
||||
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 =
|
||||
new TableColumn<ChatMember, String>("Callsign");
|
||||
|
||||
@@ -1669,7 +1667,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
return new SimpleStringProperty(displayedCallsign);
|
||||
});
|
||||
|
||||
callSignCol.setCellFactory(column -> new TableCell<ChatMember, String>() {
|
||||
callSignCol.setCellFactory(column -> new TruncatedTextTableCell<ChatMember>() {
|
||||
|
||||
@Override
|
||||
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
|
||||
* {@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
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty || item == null) {
|
||||
setText(null);
|
||||
setTooltip(null);
|
||||
setStyle("");
|
||||
return;
|
||||
}
|
||||
@@ -1774,16 +1782,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
&& chatcontroller != null
|
||||
&& 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:
|
||||
* 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>>() {
|
||||
public TableCell call(TableColumn param) {
|
||||
return new TableCell<ChatMember, String>() {
|
||||
return new TruncatedTextTableCell<ChatMember>() {
|
||||
|
||||
@Override
|
||||
public void updateItem(String item, boolean empty) {
|
||||
@@ -1953,7 +1951,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
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.
|
||||
*/
|
||||
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
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
setTooltip(null);
|
||||
setStyle("");
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMember member = getTableRow() == null ? null : getTableRow().getItem();
|
||||
|
||||
setText(item);
|
||||
setTooltip(new Tooltip(buildWorkedAnyGridStatusTooltip(member)));
|
||||
setAlignment(Pos.CENTER);
|
||||
setStyle("-fx-font-weight: bold;");
|
||||
}
|
||||
@@ -2389,7 +2382,38 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
// }, new Date(), 5000);
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
@@ -3281,11 +3323,11 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
Tab dxClusterMessagesTab = new Tab("DXCluster messages");
|
||||
dxClusterMessagesTab.setTooltip(new Tooltip("DXCluster spots."));
|
||||
dxClusterMessagesTab.setContent(initDXClusterTable());
|
||||
dxClusterMessagesTab.setContent(initDXClusterTable("dx-cluster-main"));
|
||||
|
||||
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.setContent(initChatToOtherMSGTable());
|
||||
qsoOfTheOtherTab.setContent(initChatToOtherMSGTable("qso-other-main"));
|
||||
|
||||
bottomMessageTabs.getTabs().addAll(publicMessagesTab, dxClusterMessagesTab, qsoOfTheOtherTab);
|
||||
bottomMessageTabs.getSelectionModel().select(publicMessagesTab);
|
||||
@@ -3437,6 +3479,19 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
ObservableList<ChatMessage> generalMSGList = chatcontroller.getLst_toAllMessageList();
|
||||
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>() {
|
||||
@Override
|
||||
@@ -3660,7 +3715,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
*/
|
||||
airScoutCol.setCellFactory(new Callback<TableColumn<ChatMessage, String>, TableCell<ChatMessage, String>>() {
|
||||
public TableCell call(TableColumn param) {
|
||||
return new TableCell<ChatMessage, String>() {
|
||||
return new TruncatedTextTableCell<ChatMessage>() {
|
||||
|
||||
@Override
|
||||
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();
|
||||
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>() {
|
||||
@Override
|
||||
@@ -3812,7 +3882,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
return tbl_privateMSGTable;
|
||||
}
|
||||
|
||||
private TableView<ClusterMessage> initDXClusterTable() {
|
||||
private TableView<ClusterMessage> initDXClusterTable(String layoutId) {
|
||||
|
||||
TableView<ClusterMessage> tbl_DXCTable = new TableView<ClusterMessage>();
|
||||
// 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();
|
||||
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;
|
||||
}
|
||||
|
||||
private TableView<ChatMessage> initChatToOtherMSGTable() {
|
||||
private TableView<ChatMessage> initChatToOtherMSGTable(String layoutId) {
|
||||
|
||||
TableView<ChatMessage> tbl_toOtherMSGTable = new TableView<ChatMessage>();
|
||||
// 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();
|
||||
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;
|
||||
}
|
||||
@@ -5261,6 +5368,28 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
tbl_chatMemberWkdDBTable.getColumns().addAll(callSignCol, workedCol);
|
||||
|
||||
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
|
||||
|
||||
@@ -6100,6 +6229,9 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
@Override
|
||||
public void stop() {
|
||||
System.out.println("[Main.java, Info:] Stage is closing, killing all resources");
|
||||
if (layoutAutosave != null) {
|
||||
layoutAutosave.flushPending();
|
||||
}
|
||||
timer_buildWindowTitle.purge();
|
||||
timer_buildWindowTitle.cancel();
|
||||
|
||||
@@ -6119,6 +6251,12 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private void requestLayoutSave() {
|
||||
if (layoutAutosave != null) {
|
||||
layoutAutosave.requestSave();
|
||||
}
|
||||
}
|
||||
|
||||
private Queue<Media> musicList = new LinkedList<Media>();
|
||||
private MediaPlayer mediaPlayer ;
|
||||
|
||||
@@ -6544,6 +6682,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
ChatMember ownChatMemberObject = new ChatMember();
|
||||
|
||||
chatcontroller = new ChatController(ownChatMemberObject, this); // instantiate the Chatcontroller with the user object
|
||||
layoutAutosave = new LayoutAutosave(chatcontroller.getChatPreferences());
|
||||
messageVariableResolver = new MessageVariableResolver(chatcontroller.getChatPreferences());
|
||||
chatcontroller.setStatusListener(this); //callback interface for updating Thread events in visual
|
||||
|
||||
@@ -6687,6 +6826,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newWidthValue) {
|
||||
chatcontroller.getChatPreferences().getGUIscn_ChatwindowMainSceneSizeHW()[1] = newWidthValue.doubleValue();
|
||||
requestLayoutSave();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6694,6 +6834,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newHeightValue) {
|
||||
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) {
|
||||
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();
|
||||
requestLayoutSave();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8582,6 +8724,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
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());
|
||||
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) {
|
||||
storedPositions[dividerIndex] = newDividerPosition.doubleValue();
|
||||
requestLayoutSave();
|
||||
} else {
|
||||
// Avoid crashes if preferences are older than the current UI layout.
|
||||
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();
|
||||
pnl_directedMSGWin.setOrientation(Orientation.VERTICAL);
|
||||
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) {
|
||||
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();
|
||||
requestLayoutSave();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8689,6 +8837,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newHeightValue) {
|
||||
chatcontroller.getChatPreferences().getGUIclusterAndQSOMonStage_SceneSizeHW()[1] = newHeightValue.doubleValue();
|
||||
requestLayoutSave();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8696,6 +8845,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newWidthValue) {
|
||||
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);
|
||||
|
||||
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) {
|
||||
@@ -11762,7 +11920,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
System.out.println("saved");
|
||||
|
||||
chatcontroller.getChatPreferences().writePreferencesToXmlFile();
|
||||
if (chatcontroller.getChatPreferences().writePreferencesToXmlFile()
|
||||
&& layoutAutosave != null) {
|
||||
layoutAutosave.cancelPending();
|
||||
}
|
||||
Alert a = new Alert(AlertType.INFORMATION);
|
||||
|
||||
a.setTitle("Info");
|
||||
@@ -11800,6 +11961,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
// VBox vBox = new VBox(tabPaneOptions);
|
||||
settingsScene = new Scene(optionsPanel, chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[0], chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[1]);
|
||||
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);
|
||||
|
||||
@@ -12515,13 +12684,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
}
|
||||
|
||||
private static <T> void applyQrgUiFormatting(TableColumn<T, String> col) {
|
||||
col.setCellFactory(tc -> new TableCell<T, String>() {
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty ? "" : formatQrgForUi(item));
|
||||
}
|
||||
});
|
||||
col.setCellFactory(tc -> new TruncatedTextTableCell<>(Kst4ContestApplication::formatQrgForUi));
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
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 ChatPreferences chatPreferences;
|
||||
private final Runnable layoutSaveRequester;
|
||||
|
||||
private final Stage stage = new Stage();
|
||||
private final WebView webView = new WebView();
|
||||
@@ -180,7 +181,12 @@ public final class StationMapView {
|
||||
|
||||
|
||||
public StationMapView(ChatPreferences chatPreferences) {
|
||||
this(chatPreferences, () -> { });
|
||||
}
|
||||
|
||||
public StationMapView(ChatPreferences chatPreferences, Runnable layoutSaveRequester) {
|
||||
this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences");
|
||||
this.layoutSaveRequester = Objects.requireNonNull(layoutSaveRequester, "layoutSaveRequester");
|
||||
GuiUtils.applyApplicationIcon(stage);
|
||||
|
||||
try {
|
||||
@@ -461,17 +467,25 @@ public final class StationMapView {
|
||||
stage.setY(pos[1]);
|
||||
}
|
||||
|
||||
stage.widthProperty().addListener((obs, oldValue, newValue) ->
|
||||
chatPreferences.getGUIstationMapStageSceneSizeHW()[0] = newValue.doubleValue());
|
||||
stage.widthProperty().addListener((obs, oldValue, newValue) -> {
|
||||
chatPreferences.getGUIstationMapStageSceneSizeHW()[0] = newValue.doubleValue();
|
||||
layoutSaveRequester.run();
|
||||
});
|
||||
|
||||
stage.heightProperty().addListener((obs, oldValue, newValue) ->
|
||||
chatPreferences.getGUIstationMapStageSceneSizeHW()[1] = newValue.doubleValue());
|
||||
stage.heightProperty().addListener((obs, oldValue, newValue) -> {
|
||||
chatPreferences.getGUIstationMapStageSceneSizeHW()[1] = newValue.doubleValue();
|
||||
layoutSaveRequester.run();
|
||||
});
|
||||
|
||||
stage.xProperty().addListener((obs, oldValue, newValue) ->
|
||||
chatPreferences.getGUIstationMapStagePositionXY()[0] = newValue.doubleValue());
|
||||
stage.xProperty().addListener((obs, oldValue, newValue) -> {
|
||||
chatPreferences.getGUIstationMapStagePositionXY()[0] = newValue.doubleValue();
|
||||
layoutSaveRequester.run();
|
||||
});
|
||||
|
||||
stage.yProperty().addListener((obs, oldValue, newValue) ->
|
||||
chatPreferences.getGUIstationMapStagePositionXY()[1] = newValue.doubleValue());
|
||||
stage.yProperty().addListener((obs, oldValue, newValue) -> {
|
||||
chatPreferences.getGUIstationMapStagePositionXY()[1] = newValue.doubleValue();
|
||||
layoutSaveRequester.run();
|
||||
});
|
||||
|
||||
stage.setOnShown(event -> Platform.runLater(() -> {
|
||||
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