package client; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Platform; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.layout.*; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import javafx.stage.Stage; import javafx.util.Duration; import network.protocol.packets.ServerResponsePacket; import java.net.InetSocketAddress; import java.net.SocketException; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; public class ClientGuiController implements ClientListener { private Client client; private boolean connected = true; @FXML private BorderPane vueContainer; @FXML private VBox roomList; @FXML private VBox chatList; @FXML private HBox chatInput; @FXML private ScrollPane chatPane; @FXML private Label roomName; public void setClient(Client client) { this.client = client; } @FXML public void initialize() throws SocketException { client = new Client(new InetSocketAddress("192.168.163.131", 6665), this, UsernameSingleton.getInstance().getUsername()); Platform.runLater(() -> { Stage stage = (Stage) vueContainer.getScene().getWindow(); stage.setResizable(true); // Fixed a bug that made the close button disappear under my Wayland setup (don't know if it's the same for x11) stage.setOnCloseRequest(event -> { connected = false; client.close(); }); chatList.heightProperty().addListener((obs, oldVal, newVal) -> chatPane.setVvalue(1.0)); }); requestRoomsRegularly(); } /** * Request the list of rooms from the server every second. */ private void requestRoomsRegularly() { Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(10), event -> client.RequestRoomList())); timeline.setCycleCount(Timeline.INDEFINITE); timeline.play(); } @Override public void handleDisconnect() { System.out.println("Disconnected"); } /** * Create an alert dialog when the connection to the server fails. */ @Override public void handleConnexionError() { if(connected) { Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("Connection"); alert.setHeaderText("Connection failed"); Optional res = alert.showAndWait(); if (res.isPresent() && res.get() == ButtonType.OK) { ((Stage) vueContainer.getScene().getWindow()).close(); } }); } } /** * Create an alert dialog when the connection to the server is successful. */ @Override public void handleConnect() { Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("Connection"); alert.setHeaderText("Connection to the server successful"); alert.showAndWait(); }); } /** * Format and insert the received chat message into the chat list. * @param time the time the message was sent * @param chatter the name of the person who sent the message * @param content the content of the message */ @Override public void handleChatMessage(Instant time, String chatter, String content) { String untagged = untag(content); String messageColor = Objects.equals(content, untagged) ? "lightblue" : "lightgreen"; Platform.runLater(() -> { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm") .withZone(ZoneId.systemDefault()); String timeString = formatter.format(time); Text chatterText = new Text(chatter + ": "); chatterText.setStyle("-fx-fill: black; -fx-font-weight: bold;"); TextFlow messageText = formatMessage(untagged); TextFlow wholeMessage = new TextFlow(chatterText, messageText); Text timeText = new Text(" " + timeString); timeText.setStyle("-fx-fill: gray; -fx-font-size: 10px;"); HBox messageContainer = new HBox(5, wholeMessage, timeText); messageContainer.setStyle("-fx-background-color: " + messageColor + "; -fx-padding: 8; -fx-background-radius: 5;"); messageContainer.setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(messageText, Priority.ALWAYS); chatList.getChildren().add(messageContainer); }); } /** * Remove the tag from the message (handle ANSI characters encoded by the server) * @param message the message to untag * @return the untagged message */ public String untag(String message) { Pattern pattern = Pattern.compile("\u001B\\[44;30m(.*?)\u001B\\[49;39m"); Matcher matcher = pattern.matcher(message); StringBuilder result = new StringBuilder(); while (matcher.find()) { String taggedName = matcher.group(1); matcher.appendReplacement(result, taggedName); } matcher.appendTail(result); return result.toString(); } /** * Display the list of rooms in the room list. * @param roomNames the list of room names */ @Override public void handleRoomList(List roomNames) { Platform.runLater(() -> { roomList.getChildren().clear(); for (String roomName : roomNames) { Button button = new Button(roomName); button.setOnAction(event -> { client.SendJoinRoom(roomName); createChatEnv(roomName); }); roomList.getChildren().add(button); } }); } /** * Create the chat (message wall + input) environment. * @param roomName the name of the room */ private void createChatEnv(String roomName) { Platform.runLater(() -> { this.roomName.setText("Room: " + roomName); chatList.getChildren().clear(); chatInput.getChildren().clear(); Button leaveButton = new Button("Leave room"); leaveButton.setOnAction(event -> { client.SendLeaveRoom(); this.roomName.setText(""); chatList.getChildren().clear(); chatInput.getChildren().clear(); }); chatList.getChildren().add(leaveButton); TextField messageInput = new TextField(); messageInput.setPromptText("Type a message..."); messageInput.getStyleClass().add("message-input"); HBox.setHgrow(messageInput, Priority.ALWAYS); messageInput.onActionProperty().set(event -> { client.SendChatMessage(messageInput.getText()); messageInput.clear(); }); chatInput.getChildren().add(messageInput); Button sendButton = new Button("Send"); sendButton.getStyleClass().add("send-button"); sendButton.setOnAction(event -> { client.SendChatMessage(messageInput.getText()); messageInput.clear(); }); chatInput.getChildren().add(sendButton); }); } /** * Colorize the message according to the color codes. * @param content the content of the message * @return the formatted message */ private TextFlow formatMessage(String content) { TextFlow textFlow = new TextFlow(); // Evil regex : match every sequence starting with a color code ending with the next color code Pattern pattern = Pattern.compile("&([rbgyn])([^&]*)"); Matcher matcher = pattern.matcher(content); int lastIndex = 0; while (matcher.find()) { if (matcher.start() > lastIndex) { textFlow.getChildren().add(new Text(content.substring(lastIndex, matcher.start()))); } String colorCode = matcher.group(1); String textPart = matcher.group(2); String color = switch (colorCode) { case "r" -> "red"; case "b" -> "blue"; case "g" -> "green"; case "y" -> "gray"; default -> "black"; }; Text coloredText = new Text(textPart); coloredText.setStyle("-fx-fill: " + color + ";"); textFlow.getChildren().add(coloredText); lastIndex = matcher.end(); } // Add the remaining text (works if there's no color code at all) if (lastIndex < content.length()) { textFlow.getChildren().add(new Text(content.substring(lastIndex))); } return textFlow; } @Override public void handleServerResponse(ServerResponsePacket.Response response) { } @Override public void handleActualRoom(String roomName) { } public void createRoom() { TextInputDialog dialog = new TextInputDialog(); dialog.setTitle("Create a room"); dialog.setHeaderText("Enter the name of the room"); dialog.setContentText("Room name:"); Optional result = dialog.showAndWait(); result.ifPresent(name -> { client.SendCreateRoom(name); createChatEnv(name); }); } }