mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-08-23 18:47:34 +02:00
Add persistent map analysis toggle and improve compact layout for smaller screens. Solves (#69)
This commit is contained in:
@@ -496,8 +496,10 @@ So kann der Contest-Operator auf einem Blick sehen, welche Stationen wann und ü
|
||||
## Intervall-Beacon
|
||||
|
||||
KST4Contest kann wiederkehrende CQ-Nachrichten in den öffentlichen Chat senden. Beide Chat-Kategorien verwenden ein gemeinsames Intervall, besitzen aber jeweils einen eigenen Aktivierungsschalter und Nachrichtentext. Globale Variablen wie `MYQRG`, `SECONDQRG` oder `MYLOCATOR` werden unmittelbar vor jeder Aussendung aktualisiert.
|
||||
|
||||
|
||||
Der Beacon ist für längeres CQ-Rufen auf einer festen Frequenz gedacht. Beim Absuchen oder häufigen Wechseln der QRG sollte er ausgeschaltet werden, damit keine inzwischen falsche Frequenz verbreitet wird. Details: [Konfiguration – Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
|
||||
|
||||
---
|
||||
|
||||
## Simplelogfile
|
||||
@@ -510,7 +512,7 @@ Dateibasierte Log-Auswertung per Regex. Details: [Log-Synchronisation](Log-Synch
|
||||
|
||||
Ein separates Fenster zeigt den QSO-Fluss zwischen anderen Stationen. Besonders interessant in ruhigeren Nacht-Stunden während des Contests, wenn weniger Verkehr herrscht.
|
||||
|
||||
Dieses Fenster kann miniaturisiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.
|
||||
Dieses Fenster kann minimiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -50,7 +50,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 = 4;
|
||||
public static final int CONFIG_VERSION = 5;
|
||||
|
||||
// Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility.
|
||||
private static final String TAG_CONFIG_VERSION = "configVersion";
|
||||
@@ -339,6 +339,7 @@ public class ChatPreferences {
|
||||
|
||||
private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 };
|
||||
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
|
||||
private boolean GUIstationMapPathAnalysisVisible = true;
|
||||
|
||||
|
||||
/*********************************************************************************
|
||||
@@ -635,6 +636,14 @@ public class ChatPreferences {
|
||||
this.GUIstationMapStagePositionXY = GUIstationMapStagePositionXY;
|
||||
}
|
||||
|
||||
public boolean isGUIstationMapPathAnalysisVisible() {
|
||||
return GUIstationMapPathAnalysisVisible;
|
||||
}
|
||||
|
||||
public void setGUIstationMapPathAnalysisVisible(boolean GUIstationMapPathAnalysisVisible) {
|
||||
this.GUIstationMapPathAnalysisVisible = GUIstationMapPathAnalysisVisible;
|
||||
}
|
||||
|
||||
public boolean isGuiOptions_defaultFilterNothing() {
|
||||
return guiOptions_defaultFilterNothing;
|
||||
}
|
||||
@@ -2048,6 +2057,12 @@ public class ChatPreferences {
|
||||
);
|
||||
guiOptions.appendChild(GUIstationMapStagePositionXY);
|
||||
|
||||
Element GUIstationMapPathAnalysisVisible = doc.createElement("GUIstationMapPathAnalysisVisible");
|
||||
GUIstationMapPathAnalysisVisible.setTextContent(
|
||||
String.valueOf(this.isGUIstationMapPathAnalysisVisible())
|
||||
);
|
||||
guiOptions.appendChild(GUIstationMapPathAnalysisVisible);
|
||||
|
||||
/****************************************************************************************
|
||||
****************************** now write this XML! *************************************
|
||||
****************************************************************************************/
|
||||
@@ -2783,6 +2798,17 @@ public class ChatPreferences {
|
||||
this.getGUIstationMapStagePositionXY()
|
||||
);
|
||||
|
||||
/*
|
||||
* Files written before config version 5 do not contain this value.
|
||||
* Keep the default true in that case so existing users discover the
|
||||
* path-analysis feature before choosing to hide it themselves.
|
||||
*/
|
||||
this.setGUIstationMapPathAnalysisVisible(getBoolean(
|
||||
element,
|
||||
this.isGUIstationMapPathAnalysisVisible(),
|
||||
"GUIstationMapPathAnalysisVisible"
|
||||
));
|
||||
|
||||
// Splitpane divider positions
|
||||
String s1 = getText(element, null, "GUIselectedCallSignSplitPane_dividerposition");
|
||||
if (s1 != null) {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ChatPreferencesStationMapVisibilityTest {
|
||||
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void pathAnalysisIsVisibleByDefault() {
|
||||
ChatPreferences preferences = new ChatPreferences();
|
||||
|
||||
assertTrue(preferences.isGUIstationMapPathAnalysisVisible());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hiddenPathAnalysisStateSurvivesXmlRoundTrip() throws IOException {
|
||||
Path preferencesFile = temporaryDirectory.resolve("preferences.xml");
|
||||
|
||||
ChatPreferences writtenPreferences = new ChatPreferences();
|
||||
writtenPreferences.setStoreAndRestorePreferencesFileName(
|
||||
preferencesFile.toString());
|
||||
writtenPreferences.setGUIstationMapPathAnalysisVisible(false);
|
||||
writtenPreferences.writePreferencesToXmlFile();
|
||||
|
||||
String writtenXml = Files.readString(preferencesFile);
|
||||
assertTrue(writtenXml.contains(
|
||||
"<GUIstationMapPathAnalysisVisible>false"
|
||||
+ "</GUIstationMapPathAnalysisVisible>"));
|
||||
|
||||
ChatPreferences restoredPreferences = new ChatPreferences();
|
||||
restoredPreferences.setStoreAndRestorePreferencesFileName(
|
||||
preferencesFile.toString());
|
||||
restoredPreferences.readPreferencesFromXmlFile();
|
||||
|
||||
assertFalse(restoredPreferences.isGUIstationMapPathAnalysisVisible());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyXmlWithoutVisibilitySettingKeepsAnalysisDiscoverable()
|
||||
throws IOException {
|
||||
|
||||
Path legacyPreferencesFile =
|
||||
temporaryDirectory.resolve("legacy-preferences.xml");
|
||||
|
||||
Files.writeString(legacyPreferencesFile, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<praktiKST>
|
||||
<configVersion>4</configVersion>
|
||||
<guiOptions>
|
||||
<GUIstationMapStageSceneSizeHW>1000.0;800.0</GUIstationMapStageSceneSizeHW>
|
||||
</guiOptions>
|
||||
</praktiKST>
|
||||
""");
|
||||
|
||||
ChatPreferences restoredPreferences = new ChatPreferences();
|
||||
restoredPreferences.setStoreAndRestorePreferencesFileName(
|
||||
legacyPreferencesFile.toString());
|
||||
restoredPreferences.readPreferencesFromXmlFile();
|
||||
|
||||
assertTrue(restoredPreferences.isGUIstationMapPathAnalysisVisible());
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
/**
|
||||
* Standalone station map window.
|
||||
@@ -49,6 +51,9 @@ public final class StationMapView {
|
||||
*/
|
||||
private static final boolean MAP_DEBUG_LOGGING = false;
|
||||
|
||||
private static final double MINIMUM_HEIGHT_WITH_PATH_ANALYSIS = 650.0;
|
||||
private static final double MINIMUM_HEIGHT_WITHOUT_PATH_ANALYSIS = 420.0;
|
||||
|
||||
private final PathProfileChart detailPathProfileChart = new PathProfileChart();
|
||||
private final Label detailPathModeValue = new Label("-");
|
||||
|
||||
@@ -76,6 +81,12 @@ public final class StationMapView {
|
||||
private VBox detailPane;
|
||||
|
||||
private final Label statusLabel = new Label("Station map not initialized yet.");
|
||||
|
||||
private final Label pathAnalysisHiddenHintLabel = new Label("Path analysis is hidden.");
|
||||
private final Button pathAnalysisVisibilityButton = new Button();
|
||||
private final Tooltip pathAnalysisVisibilityTooltip = new Tooltip();
|
||||
|
||||
|
||||
private final Label detailCallsignValue = new Label("-");
|
||||
private final Label detailLocatorValue = new Label("-");
|
||||
private final Label detailQrbValue = new Label("-");
|
||||
@@ -138,8 +149,12 @@ public final class StationMapView {
|
||||
private PathAnalysisResult lastPathAnalysisResult = PathAnalysisResult.waitingForSelection("");
|
||||
|
||||
private VBox mapAndProfilePane;
|
||||
private VBox profileSection;
|
||||
private VBox pathAnalysisSection;
|
||||
private ScrollPane detailScrollPane;
|
||||
|
||||
|
||||
|
||||
private final Label detailPathLosValue = new Label("-");
|
||||
private final Label detailPathWorstClearanceValue = new Label("-");
|
||||
|
||||
@@ -287,6 +302,14 @@ public final class StationMapView {
|
||||
}
|
||||
});
|
||||
|
||||
pathAnalysisVisibilityButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||
pathAnalysisVisibilityButton.setTooltip(pathAnalysisVisibilityTooltip);
|
||||
pathAnalysisVisibilityButton.setOnAction(event ->
|
||||
setPathAnalysisVisible(!profileSection.isVisible(), true));
|
||||
|
||||
pathAnalysisHiddenHintLabel.setMinWidth(Region.USE_PREF_SIZE);
|
||||
pathAnalysisHiddenHintLabel.setStyle("-fx-font-style: italic; -fx-opacity: 0.85;");
|
||||
|
||||
webView.setFocusTraversable(true);
|
||||
webView.setPickOnBounds(true);
|
||||
|
||||
@@ -323,7 +346,8 @@ public final class StationMapView {
|
||||
webView.widthProperty().addListener((obs, oldValue, newValue) -> requestMapInvalidateSize());
|
||||
webView.heightProperty().addListener((obs, oldValue, newValue) -> requestMapInvalidateSize());
|
||||
|
||||
VBox profileSection = createProfileSection();
|
||||
profileSection = createProfileSection();
|
||||
|
||||
|
||||
mapAndProfilePane = new VBox(6, webView, profileSection);
|
||||
mapAndProfilePane.setPadding(new Insets(0));
|
||||
@@ -349,10 +373,9 @@ public final class StationMapView {
|
||||
mapAndProfilePane.widthProperty().subtract(20)
|
||||
);
|
||||
|
||||
detailPane = new VBox(10,
|
||||
createSelectedStationSection(),
|
||||
createPathAnalysisSection()
|
||||
);
|
||||
pathAnalysisSection = createPathAnalysisSection();
|
||||
detailPane = new VBox(10, createSelectedStationSection(), pathAnalysisSection);
|
||||
|
||||
detailPane.setPadding(new Insets(10));
|
||||
|
||||
detailPane.setMinWidth(0);
|
||||
@@ -364,8 +387,14 @@ public final class StationMapView {
|
||||
detailScrollPane.setFitToWidth(true);
|
||||
detailScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
detailScrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
detailScrollPane.setMinWidth(300);
|
||||
/*
|
||||
* Allow the details pane to be reduced far enough to leave more room for the
|
||||
* map. At its minimum width, a callsign with up to ten characters remains
|
||||
* readable.
|
||||
*/
|
||||
detailScrollPane.setMinWidth(210);
|
||||
detailScrollPane.setPrefWidth(350);
|
||||
|
||||
detailScrollPane.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
detailScrollPane.setFitToWidth(true);
|
||||
@@ -378,8 +407,7 @@ public final class StationMapView {
|
||||
SplitPane.setResizableWithParent(detailScrollPane, true);
|
||||
|
||||
rootPane = new BorderPane();
|
||||
rootPane.setTop(statusLabel);
|
||||
BorderPane.setMargin(statusLabel, new Insets(8));
|
||||
rootPane.setTop(createMapHeader());
|
||||
rootPane.setCenter(mainSplitPane);
|
||||
|
||||
double[] size = chatPreferences.getGUIstationMapStageSceneSizeHW();
|
||||
@@ -390,7 +418,12 @@ public final class StationMapView {
|
||||
scene = new Scene(rootPane, initialWidth, initialHeight);
|
||||
|
||||
stage.setMinWidth(900);
|
||||
stage.setMinHeight(650);
|
||||
stage.setMinHeight(resolveMinimumStationMapHeight(
|
||||
chatPreferences.isGUIstationMapPathAnalysisVisible()));
|
||||
|
||||
stage.setScene(scene);
|
||||
setPathAnalysisVisible(chatPreferences.isGUIstationMapPathAnalysisVisible(), false);
|
||||
applyThemeFromPreferences();
|
||||
|
||||
stage.setScene(scene);
|
||||
applyThemeFromPreferences();
|
||||
@@ -446,6 +479,91 @@ public final class StationMapView {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an always-visible header for the map status and analysis controls.
|
||||
*
|
||||
* Keeping the control outside the sections that it hides is important: users
|
||||
* must always have an obvious way to restore a previously hidden analysis.
|
||||
*/
|
||||
private HBox createMapHeader() {
|
||||
// The status may be shortened before the show/hide control is ever clipped.
|
||||
statusLabel.setMinWidth(0);
|
||||
statusLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
statusLabel.setTextOverrun(OverrunStyle.ELLIPSIS);
|
||||
|
||||
HBox header = new HBox(
|
||||
10,
|
||||
statusLabel,
|
||||
pathAnalysisHiddenHintLabel,
|
||||
pathAnalysisVisibilityButton
|
||||
);
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.setPadding(new Insets(8));
|
||||
HBox.setHgrow(statusLabel, Priority.ALWAYS);
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides both parts of the path analysis as one logical feature.
|
||||
*
|
||||
* Both visible and managed must be changed. A node that is merely invisible
|
||||
* would still reserve layout space and the map would not grow into that area.
|
||||
* The current analysis result remains attached to the controls and is
|
||||
* immediately available again when the user restores the sections.
|
||||
*
|
||||
* @param visible true to show the profile and detailed analysis
|
||||
* @param persist true when the change was explicitly requested by the user
|
||||
*/
|
||||
private void setPathAnalysisVisible(boolean visible, boolean persist) {
|
||||
profileSection.setVisible(visible);
|
||||
profileSection.setManaged(visible);
|
||||
|
||||
pathAnalysisSection.setVisible(visible);
|
||||
pathAnalysisSection.setManaged(visible);
|
||||
|
||||
pathAnalysisHiddenHintLabel.setVisible(!visible);
|
||||
pathAnalysisHiddenHintLabel.setManaged(!visible);
|
||||
|
||||
pathAnalysisVisibilityButton.setText(
|
||||
visible ? "Hide path analysis" : "Show path analysis");
|
||||
pathAnalysisVisibilityButton.setAccessibleText(
|
||||
visible ? "Hide path analysis" : "Show path analysis");
|
||||
pathAnalysisVisibilityButton.setStyle(
|
||||
visible ? "" : "-fx-font-weight: bold;");
|
||||
|
||||
pathAnalysisVisibilityTooltip.setText(visible
|
||||
? "Hide the path profile and detailed path analysis. You can show them again at any time."
|
||||
: "Show the path profile and detailed path analysis.");
|
||||
|
||||
pathAnalysisVisibilityButton.setAccessibleHelp(
|
||||
pathAnalysisVisibilityTooltip.getText());
|
||||
|
||||
if (!visible) {
|
||||
/*
|
||||
* Do not leave a profile hover marker on the map after its chart was
|
||||
* hidden.
|
||||
*/
|
||||
showProfileHoverPointOnMap(null);
|
||||
}
|
||||
|
||||
stage.setMinHeight(resolveMinimumStationMapHeight(visible));
|
||||
|
||||
if (persist) {
|
||||
chatPreferences.setGUIstationMapPathAnalysisVisible(visible);
|
||||
}
|
||||
|
||||
if (rootPane != null) {
|
||||
rootPane.requestLayout();
|
||||
Platform.runLater(this::requestMapInvalidateSize);
|
||||
}
|
||||
}
|
||||
|
||||
private double resolveMinimumStationMapHeight(boolean pathAnalysisVisible) {
|
||||
return pathAnalysisVisible
|
||||
? MINIMUM_HEIGHT_WITH_PATH_ANALYSIS
|
||||
: MINIMUM_HEIGHT_WITHOUT_PATH_ANALYSIS;
|
||||
}
|
||||
|
||||
private VBox createSelectedStationSection() {
|
||||
GridPane detailGrid = new GridPane();
|
||||
detailGrid.setHgap(8);
|
||||
@@ -494,13 +612,18 @@ public final class StationMapView {
|
||||
gridPane.getColumnConstraints().clear();
|
||||
|
||||
ColumnConstraints labelColumn = new ColumnConstraints();
|
||||
labelColumn.setMinWidth(105);
|
||||
labelColumn.setMinWidth(70);
|
||||
labelColumn.setPrefWidth(115);
|
||||
labelColumn.setMaxWidth(130);
|
||||
labelColumn.setHgrow(Priority.NEVER);
|
||||
|
||||
ColumnConstraints valueColumn = new ColumnConstraints();
|
||||
valueColumn.setMinWidth(180);
|
||||
|
||||
/*
|
||||
* Reserve enough space for a callsign with up to ten characters, while still
|
||||
* allowing the details pane to become considerably narrower.
|
||||
*/
|
||||
valueColumn.setMinWidth(85);
|
||||
valueColumn.setHgrow(Priority.ALWAYS);
|
||||
|
||||
gridPane.getColumnConstraints().addAll(labelColumn, valueColumn);
|
||||
@@ -1530,8 +1653,11 @@ public final class StationMapView {
|
||||
return 768.0;
|
||||
}
|
||||
|
||||
double minimumHeight = resolveMinimumStationMapHeight(
|
||||
chatPreferences.isGUIstationMapPathAnalysisVisible());
|
||||
|
||||
// Avoid restoring very large old test sizes after the layout changed.
|
||||
if (storedSize[1] < 650.0 || storedSize[1] > 1100.0) {
|
||||
if (storedSize[1] < minimumHeight || storedSize[1] > 1100.0) {
|
||||
return 768.0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user