Fix: Sort button IDs before iterating map for deterministic rendering

Copy mapping.keySet() to a List and call Collections.sort(...) before iterating.
Ensures stable button placement and reproducible image updates (render + update)
This commit is contained in:
Jona Walpert
2026-04-02 22:00:18 +02:00
parent 3153f7027d
commit 4daf6ff4a2
@@ -5,8 +5,13 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
* ButtonID to LobbyID. * ButtonID to LobbyID.
*/ */
import java.util.Map; import java.util.Map;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient; import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import javafx.scene.Node; import javafx.scene.Node;
@@ -107,34 +112,62 @@ public class LobbyButtonGridManager {
// No buttons to render // No buttons to render
return; return;
} }
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) { List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
int buttonId = entry.getKey(); Collections.sort(buttonIds);
int lobbyId = entry.getValue(); int index = 0;
for (Integer buttonId : buttonIds) {
int lobbyId = mapping.get(buttonId);
Button btn = new Button(); Button btn = new Button();
btn.setId("lobbyBtn-" + buttonId); btn.setId("lobbyBtn-" + buttonId);
// Set image based on lobby status (initial) // placeholder image so UI remains responsive
LobbyStatus initialStatus = getLobbyStatus(lobbyId); Image placeholder = safeLoadImage(BUTTON_FALLBACK_IMAGE);
Image image = safeLoadImage(getImagePathForButton(buttonId, initialStatus)); ImageView imageView = new ImageView(placeholder);
ImageView imageView = new ImageView(image); imageView.setPreserveRatio(true);
imageView.setPreserveRatio(true); imageView.fitWidthProperty().bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
// Dynamic width: bind to the cell size
imageView
.fitWidthProperty()
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
imageView.setSmooth(true); imageView.setSmooth(true);
btn.setGraphic(imageView); btn.setGraphic(imageView);
btn.setMaxWidth(Double.MAX_VALUE); btn.setMaxWidth(Double.MAX_VALUE);
btn.setMaxHeight(Double.MAX_VALUE); btn.setMaxHeight(Double.MAX_VALUE);
btn.setMinWidth(BUTTON_MIN_SIZE); btn.setMinWidth(BUTTON_MIN_SIZE);
btn.setMinHeight(BUTTON_MIN_SIZE); btn.setMinHeight(BUTTON_MIN_SIZE);
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS); GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS); GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
btn.setOnAction( final int bId = buttonId;
e -> { btn.setOnAction(e -> {
Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId); Integer targetLobbyId = translationManager.getLobbyIdForButton(bId);
if (targetLobbyId != null) { if (targetLobbyId != null) {
joinLobby(targetLobbyId); joinLobby(targetLobbyId);
} }
});
// async fetch status and update image
});
// async fetch status and update image
CompletableFuture.supplyAsync(() -> lobbyClient.fetchLobbyStatusString(lobbyId), executor)
.thenAccept(statusStr -> {
LobbyStatus status = parseLobbyStatus(statusStr);
String path = getImagePathForButton(buttonId, status == null ? LobbyStatus.CREATED : status);
Image img = safeLoadImage(path);
javafx.application.Platform.runLater(() -> {
ImageView iv = new ImageView(img);
iv.setPreserveRatio(true);
iv.fitWidthProperty()
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
iv.setSmooth(true);
btn.setGraphic(iv);
});
CompletableFuture.supplyAsync(() -> lobbyClient.fetchLobbyStatusString(lobbyId), executor)
.thenAccept(statusStr -> {
LobbyStatus status = parseLobbyStatus(statusStr);
String path = getImagePathForButton(buttonId, status == null ? LobbyStatus.CREATED : status);
Image img = safeLoadImage(path);
javafx.application.Platform.runLater(() -> {
ImageView iv = new ImageView(img);
iv.setPreserveRatio(true);
iv.fitWidthProperty()
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
iv.setSmooth(true);
btn.setGraphic(iv);
});
}); });
int row = index / COLS; int row = index / COLS;
int col = index % COLS; int col = index % COLS;
@@ -236,14 +269,19 @@ public class LobbyButtonGridManager {
javafx.application.Platform.runLater( javafx.application.Platform.runLater(
() -> { () -> {
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId(); Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) { if (mapping.isEmpty()) {
int buttonId = entry.getKey(); return;
int lobbyId = entry.getValue(); }
LobbyStatus status = getLobbyStatus(lobbyId); List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
Collections.sort(buttonIds);
for (Integer buttonId : buttonIds) {
int lobbyId = mapping.get(buttonId);
CompletableFuture.supplyAsync(() -> getLobbyStatus(lobbyId), executor)
.thenAccept(status -> {
String path = getImagePathForButton(buttonId, status); String path = getImagePathForButton(buttonId, status);
javafx.application.Platform.runLater(() -> {
for (Node node : gridPane.getChildren()) { for (Node node : gridPane.getChildren()) {
if (node instanceof Button if (node instanceof Button && ("lobbyBtn-" + buttonId).equals(node.getId())) {
&& ("lobbyBtn-" + buttonId).equals(node.getId())) {
Button btn = (Button) node; Button btn = (Button) node;
ImageView iv = new ImageView(safeLoadImage(path)); ImageView iv = new ImageView(safeLoadImage(path));
iv.setPreserveRatio(true); iv.setPreserveRatio(true);
@@ -257,6 +295,8 @@ public class LobbyButtonGridManager {
break; break;
} }
} }
});
});
} }
}); });
} }