From 192c8a2cb97650a395d6135138f2ea5c3755c7e2 Mon Sep 17 00:00:00 2001 From: Mathis Ginkel Date: Tue, 21 Apr 2026 22:18:06 +0200 Subject: [PATCH 1/8] Feat: Change look of chat box --- .../components/chatui/chatbox.fxml | 7 ++- .../components/chatui/chattab.fxml | 2 +- .../ui-structure/components/chatui/chatui.css | 62 ++++++++++++++++--- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/main/resources/ui-structure/components/chatui/chatbox.fxml b/src/main/resources/ui-structure/components/chatui/chatbox.fxml index e88c640..2a6c526 100644 --- a/src/main/resources/ui-structure/components/chatui/chatbox.fxml +++ b/src/main/resources/ui-structure/components/chatui/chatbox.fxml @@ -3,13 +3,14 @@ + - + styleClass="tab-pane"> diff --git a/src/main/resources/ui-structure/components/chatui/chattab.fxml b/src/main/resources/ui-structure/components/chatui/chattab.fxml index ce3bf95..7db08e1 100644 --- a/src/main/resources/ui-structure/components/chatui/chattab.fxml +++ b/src/main/resources/ui-structure/components/chatui/chattab.fxml @@ -8,7 +8,7 @@ Date: Tue, 21 Apr 2026 22:18:06 +0200 Subject: [PATCH 2/8] Feat: Change look of chat box --- .../components/chatui/chatbox.fxml | 7 ++- .../components/chatui/chattab.fxml | 2 +- .../ui-structure/components/chatui/chatui.css | 62 ++++++++++++++++--- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/main/resources/ui-structure/components/chatui/chatbox.fxml b/src/main/resources/ui-structure/components/chatui/chatbox.fxml index e88c640..2a6c526 100644 --- a/src/main/resources/ui-structure/components/chatui/chatbox.fxml +++ b/src/main/resources/ui-structure/components/chatui/chatbox.fxml @@ -3,13 +3,14 @@ + - + styleClass="tab-pane"> diff --git a/src/main/resources/ui-structure/components/chatui/chattab.fxml b/src/main/resources/ui-structure/components/chatui/chattab.fxml index ce3bf95..7db08e1 100644 --- a/src/main/resources/ui-structure/components/chatui/chattab.fxml +++ b/src/main/resources/ui-structure/components/chatui/chattab.fxml @@ -8,7 +8,7 @@ Date: Sun, 26 Apr 2026 20:25:31 +0200 Subject: [PATCH 3/8] Fix: Reopen whisper chat when previously closed --- .../client/ui/chatui/ChatBoxController.java | 45 +++++++++++++++---- .../client/ui/chatui/ChatViewController.java | 28 +++++++++--- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java index 26aa545..66b087b 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java @@ -1,6 +1,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController; +import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController.ChatKey; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; @@ -167,9 +168,19 @@ public class ChatBoxController { ChatType whisper = ChatType.WHISPER; chatController .getChatModelMap() - .put(new ChatController.ChatKey(whisper, target), chatModel); - addChatTab(target, chatModel, whisper); + .put(new ChatKey(whisper, target), chatModel); + ChatViewController chatViewController = addChatTab(target, chatModel, whisper); + Tab chatTab = chatViewController.getChatTab(); + chatTab.setOnCloseRequest(_ -> { + chatViewController.tabIsOpen = false; + chatTabPane.getTabs().remove(chatTab); + }); } else { + ChatViewController chatViewController = chatController.activeChatControllers.get(new ChatKey(ChatType.WHISPER, target)); + if (!chatViewController.tabIsOpen) { + reopenChatTab(chatViewController.getChatTab()); + chatViewController.tabIsOpen = true; + } chatTabPane.getSelectionModel().select(usernameTabMap.get(target)); } } @@ -183,18 +194,25 @@ public class ChatBoxController { * @param chatModel The {@link ChatModel} containing the data and logic for this specific chat. * @throws RuntimeException If the FXML resource for the chat tab cannot be loaded. */ - public void addChatTab(String title, ChatModel chatModel, ChatType chatType) { + public ChatViewController addChatTab(String title, ChatModel chatModel, ChatType chatType) { String ressource = "/ui-structure/components/chatui/chattab.fxml"; URL resource = getClass().getResource(ressource); FXMLLoader fxmlLoader = new FXMLLoader(resource); - runOnPlatformSynchronized( + return runOnPlatformSynchronized( () -> { try { ChatViewController chatViewController = new ChatViewController( - this.chatController, chatModel, this.username); + this.chatController, chatModel, this.username, this); + + ChatKey chatKey; + if (chatType.equals(ChatType.WHISPER)) { + chatKey = new ChatKey(chatType, title); + } else { + chatKey = new ChatKey(chatType); + } chatController.activeChatControllers.put( - new ChatController.ChatKey(chatType), chatViewController); + chatKey, chatViewController); fxmlLoader.setController(chatViewController); Node load = fxmlLoader.load(); VBox.setVgrow(load, Priority.ALWAYS); @@ -204,9 +222,13 @@ public class ChatBoxController { chatModel.addListener((msg) -> chatViewController.showMessage(msg)); Tab newChat = new Tab(title, vbox); usernameTabMap.put(title, newChat); - this.chatTabPane.getTabs().add(newChat); + chatTabPane.getTabs().add(newChat); this.chatTabPane.getSelectionModel().select(newChat); - return newChat; + chatViewController.setChatTab(newChat); + if (!chatType.equals(ChatType.WHISPER)) { + newChat.setClosable(false); + } + return chatViewController; } catch (IOException e) { throw new RuntimeException(e); } @@ -267,4 +289,11 @@ public class ChatBoxController { } } } + + public void reopenChatTab(Tab chatTab) { + Platform.runLater(() -> { + chatTabPane.getTabs().add(chatTab); + chatTabPane.getSelectionModel().select(chatTab); + }); + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java index 2bad460..2a1ec5f 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java @@ -2,22 +2,22 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel; +import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; import java.net.URL; +import java.util.Map; import java.util.ResourceBundle; import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.Initializable; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.TextField; +import javafx.scene.control.*; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; /** Responsible for the presentation of the ChatModel to the Client */ public class ChatViewController implements Initializable { + private final ChatBoxController chatBoxController; @FXML private Button sendButton; @FXML private TextField inputField; @@ -30,6 +30,18 @@ public class ChatViewController implements Initializable { @FXML private VBox chat; + public Tab getChatTab() { + return chatTab; + } + + public void setChatTab(Tab chatTab) { + this.chatTab = chatTab; + } + + @FXML private Tab chatTab; + + public Boolean tabIsOpen; + private final ChatModel chatModel; private final String username; @@ -38,10 +50,12 @@ public class ChatViewController implements Initializable { private static final int CHAT_PADDING = 20; - public ChatViewController(ChatController chatController, ChatModel chatModel, String username) { + public ChatViewController(ChatController chatController, ChatModel chatModel, String username, ChatBoxController chatBoxController) { this.controller = chatController; this.username = username; this.chatModel = chatModel; + this.chatBoxController = chatBoxController; + tabIsOpen = true; } /** @@ -88,6 +102,10 @@ public class ChatViewController implements Initializable { * @param msg The {@link Message} object containing the content and metadata to display. */ public void showMessage(Message msg) { + if (!tabIsOpen) { + tabIsOpen = true; + chatBoxController.reopenChatTab(chatTab); + } Platform.runLater( () -> { String msgText = From 5358309e1af42e81e3ffc50e5627dfb89c04209a Mon Sep 17 00:00:00 2001 From: Mathis Ginkel Date: Sun, 26 Apr 2026 23:36:50 +0200 Subject: [PATCH 4/8] Test: Add test for ChatController --- .../client/chat/ChatClientInterface.java | 16 ++ .../casono/client/chat/ChatController.java | 35 ++++- .../casono/client/network/ChatClient.java | 3 +- .../casono/client/chat/ChatClientTest.java | 41 +++++ .../client/chat/ChatControllerTest.java | 145 +++++++++++++++++- 5 files changed, 229 insertions(+), 11 deletions(-) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatClientInterface.java create mode 100644 src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatClientTest.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatClientInterface.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatClientInterface.java new file mode 100644 index 0000000..0ec15de --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatClientInterface.java @@ -0,0 +1,16 @@ +package ch.unibas.dmi.dbis.cs108.casono.client.chat; + +import java.util.List; + +/** + * Interface used to create test instances of the ChatClient + */ +public interface ChatClientInterface { + + List getMessages(); + + void sendMessage(Message message); + + List getUsers(); + +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java index 09d7838..3a8c969 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java @@ -33,9 +33,9 @@ public class ChatController { private volatile String username; - private final ClientService clientService; + private ClientService clientService; - private final ChatClient chatClient; + private final ChatClientInterface chatClient; public ChatBoxController getChatBoxController() { return chatBoxController; @@ -112,6 +112,37 @@ public class ChatController { REFRESH_TIME); } + /** + * Another constructor to give the ChatClient directly instead of the Client Service used only for test purposes + * @param username + */ + public ChatController(String username, ChatClientInterface chatClient) { + this.username = username; + this.chatClient = chatClient; + chatModelMap = new LinkedHashMap<>(); + localUserList = new ArrayList<>(); + this.chatBoxController = new ChatBoxController(username, this); + this.logger = LogManager.getLogger(ChatController.class); + this.serverEventListener = this::handleServerEvent; + this.activeChatControllers = new HashMap<>(); + + this.timer = new Timer(true); + timer.schedule( + new TimerTask() { + @Override + public void run() { + try { + receiveMessage(); + checkWhisperUsers(); + } catch (RuntimeException e) { + logger.warn("Chat refresh failed: {}", e.getMessage()); + } + } + }, + 0, + REFRESH_TIME); + } + private void registerAsActiveController(ClientService clientService) { synchronized (ACTIVE_CONTROLLERS) { ChatController oldController = ACTIVE_CONTROLLERS.get(clientService); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java index 298e557..d46ce22 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java @@ -1,5 +1,6 @@ package ch.unibas.dmi.dbis.cs108.casono.client.network; +import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatClientInterface; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; import java.util.ArrayList; @@ -12,7 +13,7 @@ import org.apache.logging.log4j.Logger; * from the server. It uses the ClientService to send commands and receive responses from the * server. */ -public class ChatClient { +public class ChatClient implements ChatClientInterface { private final ClientService clientService; private final Logger logger; diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatClientTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatClientTest.java new file mode 100644 index 0000000..f55e904 --- /dev/null +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatClientTest.java @@ -0,0 +1,41 @@ +package ch.unibas.dmi.dbis.cs108.casono.client.chat; + +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +public class ChatClientTest implements ChatClientInterface { + + private ChatClientTest otherChatClient; + + public final Queue messageQueue; + + public ChatClientTest() { + this.messageQueue = new LinkedList<>(); + } + + public ChatClientTest(ChatClientTest otherChatClient) { + this.otherChatClient = otherChatClient; + this.messageQueue = new LinkedList<>(); + } + + @Override + public void sendMessage(Message message) { + otherChatClient.messageQueue.add(message); + } + + @Override + public List getMessages() { + List messages = new LinkedList<>(); + while (!messageQueue.isEmpty()) { + Message message = messageQueue.poll(); + messages.add(message); + } + return messages; + } + + @Override + public List getUsers() { + return List.of(); + } +} diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatControllerTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatControllerTest.java index 18990b1..10ad882 100644 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatControllerTest.java +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatControllerTest.java @@ -2,20 +2,149 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; public class ChatControllerTest { + ChatController chatController1; + ChatController chatController2; + + String name1 = "testperson1"; + String name2 = "testperson1"; + @BeforeEach - public void setup() {} + public void setup() { + ChatClientTest receivingChatClient = new ChatClientTest(); + ChatClientTest sendingChatClient = new ChatClientTest(receivingChatClient); + this.chatController1 = new ChatController(name1, sendingChatClient); + this.chatController2 = new ChatController(name2, receivingChatClient); + } @AfterEach public void teardown() {} - /* - * @Test - * public void testChatController() { - * Client client = new Client("user1"); - * client.startApplication(); - * } - */ + @Test + public void sendGlobalMessage() { + ChatModel global = new ChatModel(ChatType.GLOBAL, name2, -1, null); + chatController2.getChatModelMap().put(new ChatController.ChatKey(ChatType.GLOBAL), global); + + Message msg1 = new Message(ChatType.GLOBAL, -1, name1, null, "Hallo Welt"); + Message msg2 = new Message(ChatType.GLOBAL, -1, name1, null, "$$Hallo$$Welt$$"); + Message msg3 = new Message(ChatType.GLOBAL, -1, name1, null, "_*'=(&%$/§&%"); + Message msg4 = new Message(ChatType.LOBBY, -1, name1, null, "Guten Tag"); + Message msg5 = new Message(ChatType.LOBBY, -1, name1, null, "$/&)$)(&=)$="); + Message msg6 = new Message(ChatType.GLOBAL, -1, name1, null, "NAME=ANDERER_NUTZER"); + Message msg7 = new Message(ChatType.GLOBAL, -1, name1, null, "________"); + + ArrayList messages = new ArrayList<>(); + messages.add(msg1); + messages.add(msg2); + messages.add(msg3); + messages.add(msg4); + messages.add(msg5); + messages.add(msg6); + messages.add(msg7); + + chatController1.onSendToNetwork(msg1); + chatController1.onSendToNetwork(msg2); + chatController1.onSendToNetwork(msg3); + chatController1.onSendToNetwork(msg4); + chatController1.onSendToNetwork(msg5); + chatController1.onSendToNetwork(msg6); + chatController1.onSendToNetwork(msg7); + + chatController2.receiveMessage(); + + assertEquals(5, global.messages.size()); + + assertEquals(messages.get(0), global.messages.get(0)); + assertEquals(messages.get(1), global.messages.get(1)); + assertEquals(messages.get(2), global.messages.get(2)); + + assertNotEquals(messages.get(3), global.messages.get(3)); + assertNotEquals(messages.get(4), global.messages.get(4)); + + assertEquals(messages.get(5), global.messages.get(3)); + assertEquals(messages.get(6), global.messages.get(4)); + } + + @Test + public void sendLobbyMessage() { + ChatModel lobby = new ChatModel(ChatType.LOBBY, name2, 1, null); + chatController2.getChatModelMap().put(new ChatController.ChatKey(ChatType.LOBBY), lobby); + + Message msg1 = new Message(ChatType.LOBBY, 1, name1, null, "Hallo Welt"); + Message msg2 = new Message(ChatType.LOBBY, 1, name1, null, "$$Hallo$$Welt$$"); + Message msg3 = new Message(ChatType.LOBBY, 1, name1, null, "_*'=(&%$/§&%"); + Message msg4 = new Message(ChatType.LOBBY, 4, name1, null, "Guten Tag"); + Message msg5 = new Message(ChatType.LOBBY, 5, name1, null, "$/&)$)(&=)$="); + Message msg6 = new Message(ChatType.LOBBY, 1, name1, null, "NAME=ANDERER_NUTZER"); + Message msg7 = new Message(ChatType.LOBBY, 1, name1, null, "________"); + + ArrayList messages = new ArrayList<>(); + messages.add(msg1); + messages.add(msg2); + messages.add(msg3); + messages.add(msg4); + messages.add(msg5); + messages.add(msg6); + messages.add(msg7); + + for (Message msg : messages) { + chatController1.onSendToNetwork(msg); + } + + chatController2.receiveMessage(); + + assertEquals(5, lobby.messages.size()); + + assertEquals(messages.get(0), lobby.messages.get(0)); + assertEquals(messages.get(1), lobby.messages.get(1)); + assertEquals(messages.get(2), lobby.messages.get(2)); + + assertNotEquals(messages.get(3), lobby.messages.get(3)); + assertNotEquals(messages.get(4), lobby.messages.get(4)); + + assertEquals(messages.get(5), lobby.messages.get(3)); + assertEquals(messages.get(6), lobby.messages.get(4)); + } + + @Test + public void sendWhisperMessage() { + ChatModel whisper = new ChatModel(ChatType.WHISPER, name2, -1, name1); + chatController2.getChatModelMap().put(new ChatController.ChatKey(ChatType.WHISPER, name1), whisper); + + Message msg1 = new Message(ChatType.WHISPER, -1, name1, name2, "Hallo Welt"); + Message msg2 = new Message(ChatType.WHISPER, -1, name1, name2, "$$Hallo$$Welt$$"); + Message msg3 = new Message(ChatType.WHISPER, -1, name1, name2, "_*'=(&%$/§&%"); + Message msg4 = new Message(ChatType.WHISPER, -1, name1, name2, "NAME=ANDERER_NUTZER"); + Message msg5 = new Message(ChatType.WHISPER, -1, name1, name2, "________"); + + ArrayList messages = new ArrayList<>(); + messages.add(msg1); + messages.add(msg2); + messages.add(msg3); + messages.add(msg4); + messages.add(msg5); + + for (Message msg : messages) { + chatController1.onSendToNetwork(msg); + } + + chatController2.receiveMessage(); + + assertEquals(5, whisper.messages.size()); + + assertEquals(messages.get(0), whisper.messages.get(0)); + assertEquals(messages.get(1), whisper.messages.get(1)); + assertEquals(messages.get(2), whisper.messages.get(2)); + + assertEquals(messages.get(3), whisper.messages.get(3)); + assertEquals(messages.get(4), whisper.messages.get(4)); + } } From b7f5a268e32d92f34206b07b2ab5315d648a14cd Mon Sep 17 00:00:00 2001 From: Mathis Ginkel Date: Sun, 26 Apr 2026 23:51:45 +0200 Subject: [PATCH 5/8] Feat: Remove chatui directory in gameuicomponents --- .../gameuicomponents/chatui/chatbox.fxml | 59 ------- .../gameuicomponents/chatui/chatui.css | 155 ------------------ 2 files changed, 214 deletions(-) delete mode 100644 src/main/resources/ui-structure/gameuicomponents/chatui/chatbox.fxml delete mode 100644 src/main/resources/ui-structure/gameuicomponents/chatui/chatui.css diff --git a/src/main/resources/ui-structure/gameuicomponents/chatui/chatbox.fxml b/src/main/resources/ui-structure/gameuicomponents/chatui/chatbox.fxml deleted file mode 100644 index 98db056..0000000 --- a/src/main/resources/ui-structure/gameuicomponents/chatui/chatbox.fxml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -