Merge branch 'feat/client-chat' into chore/ui-checkstyle-fixes

# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatModel.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Message.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/CoreClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java
#	src/main/resources/ui-structure/components/Chatbox.fxml
This commit is contained in:
Julian Kropff
2026-04-11 12:56:01 +02:00
42 changed files with 1326 additions and 517 deletions
@@ -44,4 +44,8 @@ public class ClientApp {
// Forward the original address argument to the launcher as well.
Launcher.main(new String[] { arg });
}
public static void main(String[] args) {
start(args[0]);
}
}
@@ -2,64 +2,128 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.jspecify.annotations.Nullable;
/**
* responsible for the transferring of messages from the server to the ChatModel
* or from the ChatViewController to the server
* responsible for the transferring of messages from the server to the ChatModel or from the
* ChatViewController to the server
*/
public class ChatController {
private final String username;
private final ClientService clientService;
private final ChatClient chatClient;
private ChatModel chatModel;
public ChatBoxController getChatBoxController() {
return chatBoxController;
}
private int game_id;
private final ChatBoxController chatBoxController;
private int lobbyId = -1;
private final Timer timer;
public record ChatKey(ChatType type, @Nullable String targetUser) {
public ChatKey(ChatType type) {
this(type, null);
}
}
public Map<ChatKey, ChatModel> getChatModelMap() {
return chatModelMap;
}
private Map<ChatKey, ChatModel> chatModelMap;
private static final long REFRESH_TIME = 1000;
/**
* Constructor, adds TimerTask to be sent to the server regularly
*
* @param username
* @param clientService
*/
public ChatController(String username, ClientService clientService) {
this.username = username;
this.clientService = clientService;
this.chatClient = new ChatClient(this.clientService);
}
public void createChat(int game_id) {
chatModel = new ChatModel(ChatModel.ChatType.GLOBAL, username);
this.game_id = game_id;
}
public ChatModel getChatModel() {
return chatModel;
chatClient = new ChatClient(clientService);
chatModelMap = new LinkedHashMap<>();
this.chatBoxController = new ChatBoxController(username, this);
this.timer = new Timer();
timer.schedule(
new TimerTask() {
@Override
public void run() {
receiveMessage();
}
},
0,
REFRESH_TIME);
}
/**
* method to send a message, the ChatViewController received to the server
* Method to be activated, if a lobby has be chosen. It will update the UI and add a new
* ChatModel to hold the Data for the Lobby Chat
*
* @param lobbyId
*/
public void sendMessage(String msg, String username) {
Message message = new Message(Message.MessageType.GLOBAL, 0, username, null, msg);
onSendToNetwork(message);
public void setLobbyChat(int lobbyId) {
this.lobbyId = lobbyId;
ChatModel lobbyChatModel = new ChatModel(ChatType.LOBBY, username, lobbyId, null);
chatModelMap.put(new ChatKey(ChatType.LOBBY), lobbyChatModel);
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
}
/**
* method to get all messages from the server
*/
public Boolean receiveMessage() {
/** method to get all messages from the server */
public void receiveMessage() {
List<Message> newMessages = chatClient.getMessages();
if (!newMessages.isEmpty()) {
for (Message msg : newMessages) {
chatModel.addMessage(msg);
switch (msg.getMessageType()) {
case ChatType.GLOBAL:
chatModelMap.get(new ChatKey(ChatType.GLOBAL)).addMessage(msg);
break;
case ChatType.LOBBY:
if (msg.lobbyId == lobbyId) {
chatModelMap
.computeIfAbsent(
new ChatKey(ChatType.LOBBY),
(_key) ->
new ChatModel(
ChatType.LOBBY,
username,
msg.lobbyId,
null))
.addMessage(msg);
}
break;
case ChatType.WHISPER:
if (msg.target.equals(username)) {
if (chatModelMap.containsKey(
new ChatKey(ChatType.WHISPER, msg.sender))) {
chatModelMap
.get(new ChatKey(ChatType.WHISPER, msg.sender))
.addMessage(msg);
} else {
chatBoxController.addWhisperChat(msg.sender);
}
}
break;
}
}
return true;
} else { return false; }
}
}
/**
* method to send a message to the server
*
* @param message
*/
public void onSendToNetwork(Message message) {
chatClient.sendMessage(message);
}
}
@@ -1,67 +1,85 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.util.ArrayList;
import java.util.function.Consumer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
/**
* ChatModel, stores the data for a specific chat
*
* Holds the current state of a chat
* <p>Holds the current state of a chat
*/
public class ChatModel {
private ArrayList<Consumer<Message>> listeners = new ArrayList<>();
public ArrayList<Message> messages;
public ChatType chattype;
private final ChatType chattype;
public String username;
/** The person currently using this client */
public final String username;
public int count;
/** The person to send the message to If the chat is a whisper chat */
private final String target;
public enum ChatType {
GLOBAL,
LOBBY,
WHISPER
}
private final IntegerProperty count;
public int lobbyId;
/**
* Creates a new ChatModel, given a username of the client
* @param chattype
* @param username
* Constructs a new ChatModel for a specific chat type.
*
* @param chattype The type of chat (e.g., GLOBAL, LOBBY, or WHISPER).
* @param username The username of the current user.
* @param lobbyId The ID of the lobby, or -1 if not applicable.
* @param target The username of the whisper recipient, or null for other chat types.
*/
public ChatModel(ChatType chattype, String username) {
public ChatModel(ChatType chattype, String username, int lobbyId, String target) {
this.messages = new ArrayList<Message>();
this.chattype = chattype;
this.username = username;
this.count = 0;
this.count = new SimpleIntegerProperty(0);
this.lobbyId = lobbyId;
this.target = target;
}
/**
* method, used by the ChatViewController, to access all new messages, that are stored in the ChatModel
* Returns the type of chat this model represents.
*
* @return The {@link ChatType}.
*/
public synchronized String viewNextMessage() {
count--;
Message msg = messages.getLast();
return String.format("[%s] %s: %s", msg.timestamp, msg.user, msg.getMessage());
public ChatType getChattype() {
return chattype;
}
/**
* Adds a new message
* method used by the ChatController
* @param msg
* Adds a new message to the history and notifies all registered listeners. This method is
* synchronized to ensure thread safety when updating the message list.
*
* @param msg The {@link Message} to be added.
*/
public synchronized void addMessage(Message msg) {
messages.add(msg);
count++;
listeners.stream().forEach((l) -> l.accept(messages.getLast()));
}
/**
* method to send all current messages to the ChatViewController, if needed
* Registers a listener to be notified whenever a new message is added to this model.
*
* @param listener A {@link Consumer} that processes the new {@link Message}.
*/
public void addCompleteChat() {
for (int i = 0; i < this.messages.size(); i++) {
Message msg = this.messages.get(i);
}
public void addListener(Consumer<Message> listener) {
this.listeners.add(listener);
}
/**
* Returns the target user for this chat, primarily used for whispers.
*
* @return The target username or null.
*/
public String getTarget() {
return target;
}
}
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
/** Describing the Type of the Chat */
public enum ChatType {
GLOBAL,
LOBBY,
WHISPER
}
@@ -1,57 +1,63 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Message Object for internal handling of Chat-Messages
* TODO: Should be used on both sides of the network
*/
import java.util.List;
import java.util.Optional;
import org.jspecify.annotations.NonNull;
/** Message Object for internal handling of Chat-Messages */
public class Message {
private final MessageType type;
private final ChatType type;
private final String message;
public String user;
public String sender;
public String timestamp;
public int game_id = 0;
public int lobbyId = 0;
public String target = null;
public enum MessageType {
GLOBAL, LOBBY, WHISPER
};
/**
* Constructor for creating Messages with all information given
* @param type - Either global, local or whisper
* @param game_id - lobby id, or null, if the type is global
* @param user - username
* @param target - username of the target user, for whisper chat
* @param message
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
* received from the server.
*
* @param type The chat category (e.g., GLOBAL, LOBBY, or WHISPER).
* @param lobbyId The ID of the lobby, or -1 if not applicable.
* @param sender The username of the message creator.
* @param target The username of the recipient (required for whispers, otherwise null).
* @param timestamp The formatted time string (e.g., "HH:mm").
* @param message The actual text content of the message.
*/
public Message(MessageType type, int game_id, String user, String target, String timestamp, String message) {
public Message(
ChatType type,
int lobbyId,
String sender,
String target,
String timestamp,
String message) {
this.type = type;
this.game_id = game_id;
this.user = user;
this.lobbyId = lobbyId;
this.sender = sender;
this.target = target;
this.timestamp = timestamp;
this.message = message;
}
/**
* Constructor for creating the Messages of the current user, using this client -> time of writing is being recorded
* @param type - Either global, local or whisper
* @param game_id - lobby id, or null, if the type is global
* @param user - username
* @param target - username of the target user, for whisper chat
* @param message
* Constructs a new Message for the current user. Automatically generates a timestamp based on
* the local system time ("HH:mm").
*
* @param type The chat category (e.g., GLOBAL, LOBBY, or WHISPER).
* @param lobbyId The ID of the lobby, or -1 if not applicable.
* @param sender The username of the current user.
* @param target The username of the recipient (for whispers).
* @param message The actual text content to be sent.
*/
public Message(MessageType type, int game_id, String user, String target, String message) {
public Message(ChatType type, int lobbyId, String sender, String target, String message) {
this.type = type;
this.game_id = game_id;
this.user = user;
this.lobbyId = lobbyId;
this.sender = sender;
this.target = target;
this.message = message;
LocalDateTime now = LocalDateTime.now();
@@ -59,78 +65,139 @@ public class Message {
this.timestamp = now.format(formatter);
}
/**
* Returns the text content of the message.
*
* @return The message string.
*/
public String getMessage() {
return message;
}
public MessageType getMessageType() {
/**
* Returns the type of chat this message belongs to.
*
* @return The {@link ChatType}.
*/
public ChatType getMessageType() {
return type;
}
/*
* Method to test the system
* @return - String representation of the Message instance, as for example "player1: Hello World"
public String toString() {
return String.format("%s: %s", this.user, this.message);
}
*/
/**
* Method to create the request representation of the message object, to be sent to the server
* @return - request as specified in the network protocol, as String
* Formats the message object into a string representation compatible with the network protocol
* arguments.
*
* @return A formatted string containing all message attributes for server transmission.
*/
public String toArgsString() {
return String.format("TYPE=%s GAME=%d USER=%s TARGET=%s TIME=%s TEXT=%s",
this.type.toString(), this.game_id, this.user, this.target, this.timestamp, this.message);
String gameIdString = "";
if (lobbyId >= 0) {
gameIdString = " GAME=" + lobbyId;
} else {
gameIdString = " GAME='-1'";
}
return String.format(
"TYPE=%s%s USER='%s' TARGET='%s' TIME='%s' TEXT='%s'",
this.type.toString(),
gameIdString,
this.sender,
this.target,
this.timestamp,
this.message);
}
/**
* Parses a list of network request parameters to reconstruct a Message object. Handles
* different chat types (GLOBAL, LOBBY, WHISPER) and their specific requirements.
*
* @param parameters A list of {@link RequestParameter} received from the network.
* @return A new {@link Message} instance populated with the parsed data.
*/
public static Message toMessageReqPars(List<RequestParameter> parameters) {
String typeString = getParString(parameters, "TYPE");
ChatType type = ChatType.valueOf(typeString);
return switch (type) {
case GLOBAL ->
new Message(
ChatType.GLOBAL,
-1,
getParString(parameters, "USER"),
null,
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
case LOBBY ->
new Message(
ChatType.LOBBY,
Integer.parseInt(getParString(parameters, "GAME")),
getParString(parameters, "USER"),
null,
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
case WHISPER ->
new Message(
ChatType.WHISPER,
Integer.parseInt(getParString(parameters, "GAME", "-1")),
getParString(parameters, "USER"),
getParString(parameters, "TARGET"),
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
};
}
/**
* Pattern, to analyze the response String with the given parameters
* Helper method to extract a specific parameter value by its key.
*
* @param parameters The list of parameters to search.
* @param keyString The key to look for.
* @return The value associated with the key.
* @throws RuntimeException if the key is not found.
*/
public static Pattern msgRex = Pattern.compile(
"TYPE=(?<type>\\w+) GAME=(?<game>\\w+) USER=(?<user>\\w+) TARGET=(?<target>\\w+) TIME=(?<time>[0-9:.]+) TEXT=(?<text>.*)$");
private static @NonNull String getParString(
List<RequestParameter> parameters, String keyString) {
return getParString(parameters, keyString, null);
}
/**
* Method to create a Message Object, from the information given by the String
* @param response - String that got sent as a response from the server
* @return - New Message Object
* Helper method to extract a specific parameter value by its key, with a fallback default
* value.
*
* @param parameters The list of parameters to search.
* @param keyString The key to look for.
* @param defaultVal The value to return if the key is missing.
* @return The found value or the default value.
*/
public static Message toMessage(String response) {
Matcher m = msgRex.matcher(response);
if (! m.matches()) {
throw new RuntimeException("Can not parse message: '" + response+"'");
private static @NonNull String getParString(
List<RequestParameter> parameters, String keyString, String defaultVal) {
Optional<String> parOption =
parameters.stream()
.filter((p) -> keyString.equals(p.key()))
.findFirst()
.map(RequestParameter::value);
if (parOption.isEmpty()) {
if (defaultVal == null) {
throw new RuntimeException("No " + keyString + " found");
} else {
return defaultVal;
}
}
String typeString=m.group("type");
return parOption.get();
}
switch (typeString) {
case "GLOBAL":
return new Message(MessageType.GLOBAL,
0,
m.group("user"),
null,
m.group("time"),
m.group("text"));
case "LOBBY":
return new Message(MessageType.LOBBY,
Integer.parseInt(m.group("game")),
m.group("user"),
null,
m.group("time"),
m.group("text"));
case "WHISPER":
return new Message(MessageType.WHISPER,
Integer.parseInt(m.group("game")),
m.group("user"),
m.group("target"),
m.group("time"),
m.group("text"));
default:
throw new RuntimeException("Unknown message type " + typeString);
/**
* Converts the message object into a network response body using the provided builder.
*
* @param builder The {@link ResponseBodyBuilder} used to construct the response.
* @return The built {@link ResponseBody} containing the message data.
*/
public ResponseBody toResponse(ResponseBodyBuilder builder) {
builder.param("TYPE", type.name());
builder.param("GAME", lobbyId);
builder.param("USER", this.sender);
if (target != null) {
builder.param("TARGET", target);
}
builder.param("TIME", this.timestamp);
builder.param("TEXT", this.message);
return builder.build();
}
}
@@ -1,60 +1,71 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
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;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* The ChatClient class is responsible for sending messages to the server and
* retrieving messages from the server. It uses the ClientService to send
* commands and receive responses from the server.
* The ChatClient class is responsible for sending messages to the server and retrieving messages
* from the server. It uses the ClientService to send commands and receive responses from the
* server.
*/
public class ChatClient {
private ClientService clientService;
private final ClientService clientService;
private final Logger logger;
/**
* Constructs a ChatClient with the given ClientService for communication.
*
* @param clientService The ClientService instance used to send commands and
* receive responses from the server.
* @param clientService The ClientService instance used to send commands and receive responses
* from the server.
*/
public ChatClient(ClientService clientService) {
this.clientService = clientService;
this.logger = LogManager.getLogger(ChatClient.class);
}
/**
* Send a Message to the server by converting it to a string format and
* sending a "SEND_MESSAGE" command with the message content as arguments.
* Send a Message to the server by converting it to a string format and sending a "SEND_MESSAGE"
* command with the message content as arguments.
*
* @param message The Message object to be sent to the server.
*/
public void sendMessage(Message message) {
String request = "SEND_MESSAGE " + message.toArgsString();
logger.info("Writing to server: " + request);
clientService.processCommand(request);
}
/**
* Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT"
* command to determine how many messages are available and then sending
* "GET_NEXT_MESSAGE" commands in a loop to retrieve each message. The
* retrieved messages are parsed into Message objects and returned as a list.
* Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT" command to determine
* how many messages are available and then sending "GET_NEXT_MESSAGE" commands in a loop to
* retrieve each message. The retrieved messages are parsed into Message objects and returned as
* a list.
*
* @return A list of Message objects representing the messages retrieved from
* the server.
* @return A list of Message objects representing the messages retrieved from the server.
*/
public List<Message> getMessages() {
String countStr = clientService.processCommand("GET_MESSAGE_COUNT");
int count = Integer.parseInt(countStr);
System.out.println("Got " + count + " messages");
List<Message> messages = new ArrayList<>();
logger.info("Asking server for new messages");
List<RequestParameter> countStr =
ClientService.convertToRequestParameters(
clientService.processCommand("GET_MESSAGE_COUNT"));
RequestParameter countRes = countStr.getFirst();
if (!countRes.key().equals("COUNT")) {
logger.error("Not the right response from server");
}
int count = Integer.parseInt(countRes.value());
logger.info("Got " + count + " messages");
ArrayList<Message> messages = new ArrayList<>();
for (int i = 0; i < count; i++) {
String message = clientService.processCommand("GET_NEXT_MESSAGE");
if (message != null) {
Message message1 = Message.toMessage(message);
messages.add(message1);
}
List<RequestParameter> msgRes =
ClientService.convertToRequestParameters(
clientService.processCommand("GET_NEXT_MESSAGE"));
Message msg = Message.toMessageReqPars(msgRes);
messages.add(msg);
}
return messages;
}
@@ -1,24 +1,26 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* The ClientService class is responsible for managing the connection to the
* server,
* sending commands, and receiving responses. It uses a TcpTransport to
* communicate
* with the server and an ExecutorService to handle asynchronous requests.
* The ClientService class is responsible for managing the connection to the server, sending
* commands, and receiving responses. It uses a TcpTransport to communicate with the server and an
* ExecutorService to handle asynchronous requests.
*/
public class ClientService {
@@ -26,27 +28,29 @@ public class ClientService {
private final Socket socket;
private final ExecutorService executor;
private final boolean offlineMode;
public static ArrayList<String> response;
private final AtomicInteger idGenerator;
private final Logger logger;
/**
* Constructs a ClientService with the given server IP and port. It establishes
* a socket connection to the server and initializes the TcpTransport and
* ExecutorService for communication.
* Constructs a ClientService with the given server IP and port. It establishes a socket
* connection to the server and initializes the TcpTransport and ExecutorService for
* communication.
*
* @param ip The IP address of the server to connect to.
* @param ip The IP address of the server to connect to.
* @param port The port number of the server to connect to.
*/
public ClientService(String ip, int port) {
this.idGenerator = new AtomicInteger(0);
this.offlineMode = false;
this.logger = LogManager.getLogger(ClientService.class);
try {
socket = new Socket(ip, port);
clienttcptransport = new TcpTransport(socket);
logger.info("Connected to server at " + ip);
} catch (IOException i) {
throw new RuntimeException(i);
}
@@ -54,70 +58,104 @@ public class ClientService {
executor = Executors.newSingleThreadExecutor();
}
static Pattern responseRex =
Pattern.compile(
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[\\d\\w:]+))");
/**
* Constructs a ClientService in offline mode. No network connection will be
* attempted and calls to processCommand will throw a RuntimeException.
* Removes escape characters from a string, specifically converting escaped single quotes (\')
* back to regular single quotes (').
*
* @param offline true to create an offline (no-network) client service
* @param input The escaped string to process.
* @return The unescaped string.
*/
public ClientService(boolean offline) {
this.idGenerator = new AtomicInteger(0);
this.offlineMode = offline;
this.socket = null;
this.clienttcptransport = null;
this.executor = Executors.newSingleThreadExecutor();
private static String unescape(String input) {
return input.replaceAll("\\\\'", "'");
}
/**
* Returns true if this ClientService is running in offline mode (no network).
* Converts a list of raw string parameters into a list of {@link RequestParameter} objects. It
* uses a regex matcher to distinguish between quoted strings (which are unescaped) and
* primitive values.
*
* @param input A list of raw strings to be parsed.
* @return A list of parsed {@link RequestParameter} objects.
* @throws RuntimeException if a parameter does not match the expected format.
*/
public boolean isOffline() {
return offlineMode;
public static List<RequestParameter> convertToRequestParameters(List<String> input) {
return input.stream()
.map((String parString) -> responseRex.matcher(parString))
.filter(Matcher::matches)
.map(
(m) -> {
if (!(m.group("string") == null)) {
return new RequestParameter(
m.group("key"), unescape(m.group("string")));
} else if (!(m.group("primVal") == null)) {
return new RequestParameter(m.group("key"), m.group("primVal"));
} else {
throw new RuntimeException();
}
})
.toList();
}
/**
* Sends a command to the server and waits for the response. The command is
* sent using the TcpTransport, and the response is read in a loop until a
* valid response is received. The method handles "+OK" and "-ERROR" responses
* from the server and returns the actual response content.
* Sends a command to the server and processes the multi-line response. It handles the protocol
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until
* the "END" marker is reached.
*
* @param message The command message to be sent to the server.
* @return The response from the server as a string.
* @param message The raw command string to be sent to the transport layer.
* @return A list of response lines received from the server (excluding protocol markers).
* @throws RuntimeException if the server responds with an error or if a communication failure
* occurs.
*/
protected String processCommand(String message) {
if (offlineMode) {
throw new RuntimeException("ClientService is offline: cannot process command");
}
AtomicReference<String> response = new AtomicReference<>();
sendRequest(() -> {
try {
writeToTransport(message);
String responseLine = null;
do {
responseLine = clienttcptransport.read().payload();
System.out.println("Raw message '" + responseLine + "'");
if ("+OK".equals(responseLine)) {
return;
} else if (("-ERROR").equals(responseLine)) {
throw new RuntimeException(responseLine);
protected List<String> processCommand(String message) {
List<String> response = new ArrayList<>();
sendRequest(
() -> {
try {
writeToTransport(message);
String responseText = null;
responseText = clienttcptransport.read().payload();
logger.info("Raw message '" + responseText + "'");
Boolean success = null;
int count = 0;
for (String line : responseText.split("\n")) {
if (success == null) {
if ("+OK".equals(line)) {
success = true;
continue;
} else if (("-ERROR").equals(responseText)) {
success = false;
}
continue;
} else if ("END".equals(line)) {
break;
}
line = line.replaceFirst("^\t", "");
response.add(line);
}
if (success != null && success) {
return;
} else {
throw new RuntimeException("Error in " + message + ": " + response);
}
} catch (Exception e) {
throw getRuntimeException(e);
}
response.set(responseLine);
} while (true);
} catch (Exception e) {
throw getRuntimeException(e);
}
});
return response.get();
});
return response;
}
/**
* Helper method to send a request to the server using the ExecutorService. It
* submits the request as a Runnable task and waits for its completion. If
* the task is interrupted or encounters an execution exception, it throws a
* RuntimeException with the appropriate cause.
* Helper method to send a request to the server using the ExecutorService. It submits the
* request as a Runnable task and waits for its completion. If the task is interrupted or
* encounters an execution exception, it throws a RuntimeException with the appropriate cause.
*
* @param request The Runnable task representing the request to be sent to the
* server.
* @param request The Runnable task representing the request to be sent to the server.
*/
private void sendRequest(Runnable request) {
Future<?> future = executor.submit(request);
@@ -131,11 +169,10 @@ public class ClientService {
}
/**
* Helper method to extract the cause of an exception and return it as a
* RuntimeException. If the cause is null, it returns the original exception as
* a RuntimeException. If the cause is already a RuntimeException, it returns
* it directly. Otherwise, it wraps the cause in a new RuntimeException and
* returns it.
* Helper method to extract the cause of an exception and return it as a RuntimeException. If
* the cause is null, it returns the original exception as a RuntimeException. If the cause is
* already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new
* RuntimeException and returns it.
*
* @param e The exception from which to extract the cause.
* @return A RuntimeException representing the cause of the original exception.
@@ -153,38 +190,33 @@ public class ClientService {
}
/**
* Closes the socket connection to the server and shuts down the
* ExecutorService.
* It also closes the TcpTransport used for communication. If any IOException
* occurs during this process, it prints the exception to the console.
* Closes the socket connection to the server and shuts down the ExecutorService. It also closes
* the TcpTransport used for communication. If any IOException occurs during this process, it
* prints the exception to the console.
*/
public void closeSocket() {
try {
executor.shutdown();
if (clienttcptransport != null) {
clienttcptransport.close();
}
if (socket != null) {
socket.close();
}
clienttcptransport.close();
socket.close();
logger.info("Socket closed");
} catch (IOException j) {
System.out.println(j);
logger.debug(j);
}
}
/**
* Helper method to write a command string to the TcpTransport. It generates a
* unique ID for the command using the idGenerator and sends a RawPacket
* containing the ID and the command string to the server. If an IOException
* occurs during this process, it throws a RuntimeException with the cause.
* Method to write with the tcp transport to the server
*
* @param s The command string to be sent to the server.
* @throws IOException If an I/O error occurs while writing to the transport.
* @param s - Message to be sent
* @throws IOException
*/
private void writeToTransport(String s) throws IOException {
int id = this.idGenerator.incrementAndGet();
this.clienttcptransport.write(new RawPacket(id, s));
}
public void ping() {
processCommand("PING");
}
}
@@ -1,11 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
/**
* The CoreClient class provides basic functionalities for communicating with
* the
* server, such as sending a ping command to check connectivity and logging in
* with a username. It uses the ClientService to send commands and receive
* responses from the server.
* The CoreClient class provides basic functionalities for communicating with the server, such as
* sending a ping command to check connectivity and logging in with a username. It uses the
* ClientService to send commands and receive responses from the server.
*/
public class CoreClient {
private final ClientService clientService;
@@ -13,16 +11,16 @@ public class CoreClient {
/**
* Constructs a CoreClient with the given ClientService for communication.
*
* @param clientservice The ClientService instance used to send commands and
* receive responses from the server.
* @param clientservice The ClientService instance used to send commands and receive responses
* from the server.
*/
public CoreClient(ClientService clientservice) {
this.clientService = clientservice;
}
/**
* Sends a "PING" command to the server to check connectivity. The server
* should respond with a "PONG" message if the connection is successful.
* Sends a "PING" command to the server to check connectivity. The server should respond with a
* "PONG" message if the connection is successful.
*/
public void ping() {
clientService.processCommand("PING");
@@ -0,0 +1,112 @@
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 java.io.IOException;
import java.net.URL;
import java.util.List;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
public class ChatBoxController {
private String username;
private ChatController chatController;
@FXML private VBox chatBox;
@FXML private TabPane chatTabPane;
@FXML private MenuButton addWhisperChatButton;
private FXMLLoader fxmlLoader;
@FXML private List<MenuItem> whisperUsers;
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
public ChatBoxController(String username, ChatController chatController) {
this.username = username;
this.chatController = chatController;
}
/**
* Initializes the chat interface by creating the global chat model and adding the corresponding
* "GLOBAL" tab to the interface. It also registers the global chat in the {@link
* ChatController}'s model map.
*/
@FXML
public void initialize() {
ChatModel globalChatModel = new ChatModel(ChatType.GLOBAL, username, -1, null);
chatController
.getChatModelMap()
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
addChatTab("GLOBAL", globalChatModel);
// TODO: Button to add new Whisper Chat
}
/**
* Adds a specific user to the list of available whisper targets. Creates a new menu item for
* the user and defines the action to open a private chat tab when selected.
*
* @param targetUserName The username of the person to be added to the whisper list.
*/
public void addWhisperUser(String targetUserName) {
MenuItem menuItem = new MenuItem(targetUserName);
whisperUsers.add(menuItem);
addWhisperChatButton.getItems().add(menuItem);
menuItem.setOnAction(event -> addWhisperChat(targetUserName));
}
/**
* Creates a new private (whisper) chat model for a specific target user, registers it within
* the chat system, and opens a new chat tab.
*
* @param target The username of the recipient for the private messages.
*/
public void addWhisperChat(String target) {
ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, -1, target);
chatController
.getChatModelMap()
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
addChatTab(target, chatModel);
}
/**
* Dynamically loads a new chat tab from an FXML resource and attaches it to the TabPane. It
* initializes a {@link ChatViewController} for the tab and sets up a listener to display
* incoming messages in real-time.
*
* @param title The title to be displayed on the tab header.
* @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) {
URL resource = getClass().getResource(ressource);
FXMLLoader fxmlLoader = new FXMLLoader(resource);
try {
ChatViewController chatViewController =
new ChatViewController(this.chatController, chatModel, this.username);
fxmlLoader.setController(chatViewController);
Node load = fxmlLoader.load();
VBox.setVgrow(load, Priority.ALWAYS);
VBox vbox = new VBox();
VBox.setVgrow(vbox, Priority.ALWAYS);
vbox.getChildren().add(load);
chatModel.addListener((msg) -> chatViewController.showMessage(msg));
Tab newChat = new Tab(title, vbox);
this.chatTabPane.getTabs().add(newChat);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -2,106 +2,101 @@ 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.Message;
import java.net.URL;
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.layout.HBox;
import javafx.scene.layout.VBox;
import java.util.Timer;
import java.util.TimerTask;
/** Responsible for the presentation of the ChatModel to the Client */
public class ChatViewController implements Initializable {
/**
* Responsible for the presentation of the ChatModel to the Client
*/
@FXML private Button sendButton;
public class ChatViewController {
@FXML private TextField inputField;
@FXML private VBox chatInterfaceVBox;
@FXML private HBox controlBar;
@FXML private ScrollPane scrollPane;
@FXML private VBox chat;
private final ChatModel chatModel;
private final ChatModel chatmodel;
private final String username;
private final ChatController controller;
private final Timer timer;
@FXML
private VBox chatVBox;
@FXML
private TextField inputField;
@FXML
private ScrollPane chatScrollPane;
@FXML
private Button sendButton;
@FXML
private Button refreshButton;
private static final int CHAT_PADDING = 20;
public ChatViewController() {
this.username = null;
this.chatmodel = null;
this.controller = null;
this.timer = new Timer();
}
public ChatViewController(String username, ChatModel chatmodel, ChatController controller) {
public ChatViewController(ChatController chatController, ChatModel chatModel, String username) {
this.controller = chatController;
this.username = username;
this.chatmodel = chatmodel;
this.controller = controller;
this.timer = new Timer();
if (this.controller != null) {
timer.schedule(new TimerTask() {
@Override
public void run() {
System.err.println("tick");
if (ChatViewController.this.controller.receiveMessage()) {
showMessage();
}
}
}, 0, 1000);
}
this.chatModel = chatModel;
}
@FXML
public void initialize() {
if (inputField != null) {
inputField.setOnAction(event -> sendMessage());
}
if (sendButton != null) {
sendButton.setOnAction(event -> sendMessage());
}
if (refreshButton != null) {
refreshButton.setOnAction(event -> showMessage());
}
if (chatScrollPane != null && chatVBox != null) {
chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty());
}
}
public void showMessage() {
while (chatmodel.count > 0) {
String msg = chatmodel.viewNextMessage();
Label label = new Label(msg);
label.getStyleClass().add("info-text");
label.setWrapText(true);
label.maxWidthProperty().bind(chatVBox.widthProperty().subtract(CHAT_PADDING));
chatVBox.getChildren().add(label);
}
/**
* Initializes the controller after the FXML root element has been processed. Sets up event
* handlers for sending messages via the input field or button and ensures the ScrollPane
* automatically scrolls to the bottom when new messages are added.
*
* @param location The location used to resolve relative paths for the root object.
* @param resourceBundle The resources used to localize the root object.
*/
@Override
public void initialize(URL location, ResourceBundle resourceBundle) {
inputField.setOnAction(event -> sendMessage());
sendButton.setOnAction(event -> sendMessage());
scrollPane.vvalueProperty().bind(chat.heightProperty());
}
/**
* Retrieves the text from the input field, creates a new {@link Message} object using the
* current model state, and passes it to the {@link ChatController} for network transmission.
* The input field is cleared after sending.
*/
public void sendMessage() {
String message = inputField.getText().trim();
if (!message.isEmpty()) {
inputField.clear();
this.controller.sendMessage(message, username);
Message msg =
new Message(
chatModel.getChattype(),
chatModel.lobbyId,
username,
chatModel.getTarget(),
message);
controller.onSendToNetwork(msg);
}
}
public void endController() {
timer.cancel();
/**
* Displays a message in the chat interface. This method creates a new Label for the message
* text and adds it to the message container. It uses {@link Platform#runLater(Runnable)} to
* ensure the UI update happens on the JavaFX Application Thread.
*
* @param msg The {@link Message} object containing the content and metadata to display.
*/
public void showMessage(Message msg) {
Platform.runLater(
() -> {
String msgText =
String.format(
"[%s] %s: %s", msg.timestamp, msg.sender, msg.getMessage());
Label label = new Label(msgText);
label.getStyleClass().add("info-text");
label.setWrapText(true);
label.maxWidthProperty().bind(chat.widthProperty().subtract(CHAT_PADDING));
chat.getChildren().add(label);
});
}
}
@@ -3,6 +3,12 @@ package ch.unibas.dmi.dbis.cs108.casono.server;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest;
@@ -15,6 +21,9 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout.LogoutRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
@@ -26,12 +35,13 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** Application class for starting the server. */
public class ServerApp {
@@ -110,6 +120,20 @@ public class ServerApp {
commandRouter.register(
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
parserDispatcher.register("SEND_MESSAGE", new SendMessageParser());
commandRouter.register(
SendMessageRequest.class, new SendMessageHandler(responseDispatcher, userRegistry));
parserDispatcher.register("GET_MESSAGE_COUNT", new GetMessageCountParser());
commandRouter.register(
GetMessageCountRequest.class,
new GetMessageCountHandler(responseDispatcher, userRegistry));
parserDispatcher.register("GET_NEXT_MESSAGE", new GetNextMessageParser());
commandRouter.register(
GetNextMessageRequest.class,
new GetNextMessageHandler(responseDispatcher, userRegistry));
parserDispatcher.register("LIST_USERS", new ListUsersParser());
commandRouter.register(
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
@@ -1,15 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
/** Handles {@link CheckUsernameRequest}s to check whether a username is available. */
public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> {
private final UserRegistry userRegistry;
/**
* Creates a new handler for checking username availability.
*
@@ -20,7 +17,6 @@ public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> {
super(responseDispatcher);
this.userRegistry = userRegistry;
}
/**
* Executes the username availability check for the given request.
*
@@ -40,4 +36,4 @@ public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> {
}
responseDispatcher.dispatch(new CheckUsernameResponse(request.getContext(), availability));
}
}
}
@@ -1,9 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
/** Parses a primitive request into a {@link CheckUsernameRequest}. */
public class CheckUsernameParser implements CommandParser<CheckUsernameRequest> {
/**
@@ -18,4 +16,4 @@ public class CheckUsernameParser implements CommandParser<CheckUsernameRequest>
new RequestParameterAccessor(primitiveRequest.parameters());
return new CheckUsernameRequest(primitiveRequest.context(), accessor.require("USERNAME"));
}
}
}
@@ -1,12 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/** Request implementation used to check whether a username is available or already taken */
public class CheckUsernameRequest extends Request {
private final String username;
/**
* Constructs a new CheckUsernameRequest with the given context and username to check
*
@@ -18,7 +15,6 @@ public class CheckUsernameRequest extends Request {
super(context);
this.username = username;
}
/**
* Returns the provided username in the request
*
@@ -27,4 +23,4 @@ public class CheckUsernameRequest extends Request {
public String getUsername() {
return username;
}
}
}
@@ -3,7 +3,6 @@ package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
/** Response indicating the availability status of a username check. */
public class CheckUsernameResponse extends SuccessResponse {
/**
@@ -15,4 +14,4 @@ public class CheckUsernameResponse extends SuccessResponse {
public CheckUsernameResponse(RequestContext context, UsernameAvailability availability) {
super(context, new ResponseBodyBuilder().param("STATUS", availability).build());
}
}
}
@@ -1,7 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
/** Represents the availability status of a username */
enum UsernameAvailability {
public enum UsernameAvailability {
/** Username is available */
FREE,
@@ -0,0 +1,49 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
public class GetMessageCountHandler extends CommandHandler<GetMessageCountRequest> {
private final UserRegistry userRegistry;
/**
* Constructs a new GetMessageCountHandler with the necessary response dispatcher and user
* registry.
*
* @param responseDispatcher The dispatcher used to send the count or error back to the client.
* @param userRegistry The registry used to identify the user and access their message queue.
*/
public GetMessageCountHandler(
ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
super(responseDispatcher);
this.userRegistry = userRegistry;
}
/**
* Processes a request to retrieve the number of pending messages for a user. It looks up the
* user by their session ID; if found, it dispatches a {@link GetMessageCountResponse}
* containing the current count. Otherwise, it dispatches an {@link ErrorResponse}.
*
* @param request The {@link GetMessageCountRequest} containing the session details.
*/
@Override
public void execute(GetMessageCountRequest request) {
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
if (user.isPresent()) {
int count = user.get().getMessageCount();
GetMessageCountResponse response =
new GetMessageCountResponse(request.getContext(), count);
responseDispatcher.dispatch(response);
} else {
ErrorResponse response =
new ErrorResponse(
request.getContext(), "", "user could not be identified by SessionId");
responseDispatcher.dispatch(response);
}
}
}
@@ -0,0 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
public class GetMessageCountParser implements CommandParser<GetMessageCountRequest> {
/**
* Parses a raw {@link PrimitiveRequest} into a {@link GetMessageCountRequest}. This method
* wraps the request context from the network layer into a structured message count request
* object.
*
* @param primitiveRequest The raw request containing parameters and context from the network.
* @return A new {@link GetMessageCountRequest} instance.
*/
@Override
public GetMessageCountRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
return new GetMessageCountRequest(primitiveRequest.context());
}
}
@@ -0,0 +1,17 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class GetMessageCountRequest extends Request {
/**
* Constructs a new GetMessageCountRequest with the specified request context. This request is
* used by a client to query the number of pending messages currently waiting in their
* server-side queue.
*
* @param context The {@link RequestContext} associated with this request.
*/
public GetMessageCountRequest(RequestContext context) {
super(context);
}
}
@@ -0,0 +1,18 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
public class GetMessageCountResponse extends SuccessResponse {
/**
* Constructs a new GetMessageCountResponse. It creates a response body containing the "COUNT"
* parameter and associates it with the original request context.
*
* @param context The {@link RequestContext} of the original request.
* @param count The number of pending messages to be returned to the client.
*/
public GetMessageCountResponse(RequestContext context, int count) {
super(context, new ResponseBodyBuilder().param("COUNT", count).build());
}
}
@@ -0,0 +1,49 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
public class GetNextMessageHandler extends CommandHandler<GetNextMessageRequest> {
private final UserRegistry userRegistry;
/**
* Constructs a new GetNextMessageHandler with the required dispatcher and user registry.
*
* @param responseDispatcher The dispatcher used to send responses back to clients.
* @param userRegistry The registry used to look up users by their session information.
*/
public GetNextMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
super(responseDispatcher);
this.userRegistry = userRegistry;
}
/**
* Executes the request to retrieve the next message for a specific user. It identifies the user
* via their session ID, dequeues the next available message, and dispatches a {@link
* GetNextMessageResponse}. If the user cannot be identified, an {@link ErrorResponse} is sent
* instead.
*
* @param request The {@link GetNextMessageRequest} containing the session and context.
*/
@Override
public void execute(GetNextMessageRequest request) {
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
if (user.isPresent()) {
Message msg = user.get().dequeMessage();
GetNextMessageResponse response = new GetNextMessageResponse(request.getContext(), msg);
responseDispatcher.dispatch(response);
} else {
ErrorResponse response =
new ErrorResponse(
request.getContext(),
"NO_USER_ASSOCIATED",
"user could not be identified by SessionId");
responseDispatcher.dispatch(response);
}
}
}
@@ -0,0 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
public class GetNextMessageParser implements CommandParser<GetNextMessageRequest> {
/**
* Parses a raw {@link PrimitiveRequest} into a {@link GetNextMessageRequest}. This method
* initializes a parameter accessor (though not currently used for extraction) and returns a
* structured request object containing the original request context.
*
* @param primitiveRequest The raw request containing parameters and context from the network.
* @return A new {@link GetNextMessageRequest} instance.
*/
@Override
public GetNextMessageRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
return new GetNextMessageRequest(primitiveRequest.context());
}
}
@@ -0,0 +1,17 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class GetNextMessageRequest extends Request {
/**
* Constructs a new GetNextMessageRequest with the specified request context. This request is
* typically used by a client to poll or retrieve the next available message from the server's
* queue.
*
* @param context The {@link RequestContext} associated with this request.
*/
public GetNextMessageRequest(RequestContext context) {
super(context);
}
}
@@ -0,0 +1,19 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
public class GetNextMessageResponse extends SuccessResponse {
/**
* Constructs a new GetNextMessageResponse. It converts the provided {@link Message} into a
* network-compatible response body and associates it with the original request context.
*
* @param context The {@link RequestContext} of the request being answered.
* @param msg The {@link Message} to be sent back to the client.
*/
public GetNextMessageResponse(RequestContext context, Message msg) {
super(context, msg.toResponse(ResponseBody.builder()));
}
}
@@ -0,0 +1,47 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
private final UserRegistry userRegistry;
/**
* Constructs a new SendMessageHandler with the required dispatcher and user registry.
*
* @param responseDispatcher The dispatcher used to send responses back to clients.
* @param userRegistry The registry containing all currently connected users.
*/
public SendMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
super(responseDispatcher);
this.userRegistry = userRegistry;
}
/**
* Processes a message send request. This method extracts the message from the request,
* broadcasts it to all connected users, and dispatches a success response (OK) back to the
* sender.
*
* @param request The {@link SendMessageRequest} containing the message and context.
*/
@Override
public void execute(SendMessageRequest request) {
Message message = request.getMessage();
broadcast(message);
OkResponse response = new OkResponse(request.getContext());
responseDispatcher.dispatch(response);
}
/**
* Distributes a message to every user currently registered in the system. Each user's message
* queue is updated with the new message.
*
* @param message The {@link Message} object to be broadcast.
*/
public void broadcast(Message message) {
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
}
}
@@ -0,0 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
public class SendMessageParser implements CommandParser<SendMessageRequest> {
/**
* Parses a raw {@link PrimitiveRequest} into a specific {@link SendMessageRequest}. This method
* extracts the message details from the request parameters and wraps them along with the
* request context into a structured request object.
*
* @param primitiveRequest The raw request containing parameters and context from the network.
* @return A structured {@link SendMessageRequest} containing the parsed {@link Message}.
*/
@Override
public SendMessageRequest parse(PrimitiveRequest primitiveRequest) {
Message msg = Message.toMessageReqPars(primitiveRequest.parameters());
return new SendMessageRequest(primitiveRequest.context(), msg);
}
}
@@ -0,0 +1,30 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class SendMessageRequest extends Request {
private final Message msg;
/**
* Constructs a new SendMessageRequest with the given context and message.
*
* @param context The {@link RequestContext} associated with this request.
* @param msg The {@link Message} object to be processed.
*/
public SendMessageRequest(RequestContext context, Message msg) {
super(context);
this.msg = msg;
}
/**
* Returns the message contained within this request.
*
* @return The {@link Message} instance.
*/
public Message getMessage() {
return msg;
}
}
@@ -1,15 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.message;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
public class MessageManager {
private final UserRegistry userRegistry;
public MessageManager(UserRegistry userRegistry) {
this.userRegistry = userRegistry;
}
public void broadcast(Message message) {
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
}
}
@@ -1,10 +1,11 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.message.Message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -88,6 +89,14 @@ public class User {
messages.add(message);
}
public synchronized int getMessageCount() {
return messages.size();
}
public synchronized Message dequeMessage() throws NoSuchElementException {
return messages.remove();
}
public synchronized List<Message> dequeueAllMessages(Message message) {
List<Message> allMessages = new ArrayDeque<>(messages).stream().toList();
messages.clear();
@@ -9,14 +9,14 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.
* <p>Implementations of this class use the {@code +OK} prefix. It provides a protected constructor
* so subclasses can supply the response body content.
*/
public abstract class SuccessResponse extends Response {
public class SuccessResponse extends Response {
/**
* Create a successful response with the provided body.
*
* @param context the RequestContext of the request
* @param body the response body
*/
protected SuccessResponse(RequestContext context, ResponseBody body) {
public SuccessResponse(RequestContext context, ResponseBody body) {
super(context, body);
}
@@ -5,37 +5,37 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.image.*?>
<!-- Main container: Links the UI to the CasinoGameController and loads Casinogameui.css -->
<!-- Hauptcontainer: Verknüpft die UI mit dem CasinoGameController und lädt casinogameui.css -->
<AnchorPane xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameController"
styleClass="background"
stylesheets="@Casinogameui.css">
stylesheets="@casinogameui.css">
<GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
<columnConstraints>
<!-- Table-Box: 70% -->
<!-- Tisch-Box: 70% -->
<ColumnConstraints percentWidth="70.0" hgrow="ALWAYS" />
<!-- Table-Box: 5% -->
<!-- Leere-Box: 5% -->
<ColumnConstraints percentWidth="5.0" hgrow="ALWAYS" />
<!-- Table-Box: 20% -->
<!-- Chat-Box: 20% -->
<ColumnConstraints percentWidth="25.0" hgrow="ALWAYS" />
</columnConstraints>
<!-- Ensures that this line uses all available vertical space -->
<!-- Sorgt dafür, dass diese Zeile den gesamten verfügbaren vertikalen Platz nutzt -->
<rowConstraints>
<RowConstraints vgrow="ALWAYS" />
</rowConstraints>
<children>
<!-- Table box in column 0 -->
<!-- Tisch-Box in Spalte 0 -->
<StackPane GridPane.columnIndex="0">
<!-- Inner spacing: Keeps the content 10 pixels away from the top/bottom and 20 pixels from the sides -->
<!-- Innenabstand: Hält den Inhalt 10 Pixel von oben/unten und 20 Pixel von den Seiten fern -->
<padding>
<Insets top="20" right="10" bottom="120" left="20"/>
</padding>
<!-- Central Casino Table: Centers the content and utilizes the entire window due to its maximum size (1.7976931348623157E308) -->
<!-- Zentraler Casinotisch: Zentriert den Inhalt und nutzt durch die maximale Größe (1.7976931348623157E308) das gesamte Fenster -->
<VBox fx:id="casinoTable"
alignment="CENTER"
styleClass="casino-table"
@@ -44,7 +44,7 @@
maxWidth="Infinity"
maxHeight="Infinity">
<!-- TODO: Placeholder for the poker playing area: Cards, chips and opponents' bets will be dynamically displayed here later -->
<!-- TODO: Platzhalter für die Poker-Spielfläche: Hier werden später dynamisch Karten, Chips und Einsätze der Gegner angezeigt -->
<HBox alignment="CENTER" spacing="15">
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
@@ -52,7 +52,7 @@
</HBox>
<VBox alignment="CENTER" spacing="10">
<!-- Label with logo -->
<!-- Label mit Logo -->
<Label text="CAS0NO" styleClass="table-title">
<graphic>
<ImageView fitHeight="30.0" preserveRatio="true">
@@ -63,32 +63,32 @@
</graphic>
</Label>
<!-- TODO: Placeholder: Will be replaced once the game engine is finished and can process real-world scenarios -->
<!-- TODO: Platzhalter: Wird ersetzt, sobald die GameEngine fertig ist und echte Einsätze verarbeiten kann -->
<Label fx:id="welcomeText" text="Setzen Sie Ihren Einsatz" styleClass="table-title" />
</VBox>
</VBox>
</StackPane>
<!-- Empty box in column 1: Creates space between the table box and the chat box -->
<!-- Leere-Box in Spalte 1: Schafft Platz zwischen dem Tisch-Box und der Chat-Box -->
<VBox GridPane.columnIndex="1"
alignment="CENTER"
minWidth="100"
minHeight="100">
<!-- Leave blank -->
<!-- Bleibt leer -->
</VBox>
<!-- TODO: Placeholder: Chat box in column 2: -->
<!-- TODO: Platzhalter: Chat-Box in Spalte 2: -->
<fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/>
</children>
</GridPane>
<!-- movable taskbar -->
<!-- verschiebbare Taskbar -->
<AnchorPane>
<fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml"/>
<fx:include fx:id="taskbarInclude" source="gameuicomponents/taskbar.fxml"/>
</AnchorPane>
<!-- Opponent Status 1: One of three panels displaying the opponent's icon, name, and account balance -->
<!-- Gegner-Status 1: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
<GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="50">
@@ -99,11 +99,11 @@
</columnConstraints>
<children>
<fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
</children>
</GridPane>
<!-- Opponent Status 2: One of three panels displaying the opponent's icon, name, and account balance -->
<!-- Gegner-Status 2: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
<GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="20">
@@ -114,11 +114,11 @@
</columnConstraints>
<children>
<fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
</children>
</GridPane>
<!-- Opponent Status 3: One of three panels that displays the opponent's icon, name, and account balance -->
<!-- Gegner-Status 3: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
<GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="50">
@@ -129,7 +129,7 @@
</columnConstraints>
<children>
<fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
</children>
</GridPane>
</AnchorPane>
@@ -123,7 +123,7 @@
</VBox>
<!-- RECHTER BEREICH (Info-Box) -->
<fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/>
<fx:include source="components/chatui/chatbox.fxml" GridPane.columnIndex="2"/>
</children>
</GridPane>
</AnchorPane>
@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<!--
This window will later be linked to the server requests,
to display messages from other players and system messages in real time.
The chat internally distinguishes between:
- Player-to-Player (Private 2-person chat)
- Lobby Chat (Current room)
- Global Chat (Entire server)
Features planned for later implementation:
- Stylish chat bubbles with names and timestamps.
-->
<VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController"
alignment="CENTER"
stylesheets="@Chatui.css">
<!-- Inner spacing: Creates 30 pixels of space at the top, right and bottom, only 10 pixels on the left (asymmetrical) -->
<padding>
<Insets top="30" right="30" bottom="30" left="10"/>
</padding>
<children>
<VBox VBox.vgrow="ALWAYS"
styleClass="chat-box"
spacing="10"
maxWidth="Infinity">
<children>
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
<Region minHeight="4" styleClass="chat-separator"/>
<!-- Chat messages will be added here -->
<ScrollPane fx:id="chatScrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" VBox.vgrow="ALWAYS" styleClass="chat-scroll-pane">
<content>
<VBox fx:id="chatVBox" spacing="10" styleClass="chat-VBox"/>
</content>
</ScrollPane>
<!-- Input area -->
<HBox spacing="10">
<!-- TODO: Dynamically adjust the size of the text field -->
<TextField fx:id="inputField" HBox.hgrow="ALWAYS" promptText="Nachricht eingeben..." styleClass="gray-input-field" onAction="#sendMessage"/>
<Button fx:id="sendButton" text="SENDEN" onAction="#sendMessage" styleClass="yellow-button"/>
</HBox>
</children>
</VBox>
</children>
</VBox>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:id="chatBox"
VBox.vgrow="ALWAYS"
stylesheets="@chatui.css"
styleClass="chat-box"
spacing="10"
maxWidth="Infinity">
<HBox spacing="10"
fx:id="menuBox">
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
<Region HBox.hgrow="ALWAYS"/>
<MenuButton fx:id="addWhisperChatButton" styleClass="yellow-button" text="WHISPER CHAT"
alignment="CENTER_RIGHT">
</MenuButton>
</HBox>
<TabPane fx:id="chatTabPane"
VBox.vgrow="ALWAYS"
styleClass="tab-header-area">
</TabPane>
</VBox>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<!-- VBox for the global chat, should only be visible when activated via changeGlobalChatButton-->
<VBox fx:id="chatInterfaceVBox"
maxWidth="Infinity"
spacing="10"
styleClass="chat-box"
stylesheets="@chatui.css"
VBox.vgrow="ALWAYS"
xmlns="http://javafx.com/javafx/17.0.12"
xmlns:fx="http://javafx.com/fxml/1">
<children>
<!-- Chatnachrichten werden hier hinzugefügt -->
<ScrollPane fx:id="scrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" styleClass="chat-scroll-pane" VBox.vgrow="ALWAYS">
<content>
<VBox fx:id="chat" spacing="10" styleClass="chat-VBox" />
</content>
</ScrollPane>
<!-- Eingabebereich -->
<HBox fx:id="controlBar" spacing="10">
<!-- TODO: Größe des TextFields dynamisch anpassen -->
<TextField fx:id="inputField" promptText="Nachricht eingeben..." styleClass="gray-input-field" HBox.hgrow="ALWAYS" />
<Button fx:id="sendButton" styleClass="yellow-button" text="SENDEN" />
</HBox>
</children>
</VBox>
@@ -5,7 +5,7 @@
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 12;
-fx-padding: 20;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
/*-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);*/
}
.chat-header {
@@ -83,7 +83,7 @@
.gray-button {
-fx-background-color: #333333;
-fx-border-color: #666666;
-fx-text-fill: #cccccc;
-fx-text-fill: #ffffff;
}
.gray-button, .yellow-button, .red-button {
@@ -153,3 +153,54 @@
.scroll-pane .scroll-bar:horizontal .thumb:hover {
-fx-pref-height: 10;
}
.tab-pane .tab-header-area .tab-header-background {
-fx-opacity: 0;
-fx-background-color: #0d9e3b;
-fx-background-radius: 18;
-fx-border-radius: 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 5;
-fx-padding: 10;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
-fx-max-height: 10;
}
.tab-pane .tab {
-fx-background-color: #0d9e3b;
-fx-background-radius: 18;
-fx-border-radius: 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 5;
-fx-padding: 10;
}
.tab-pane .tab:selected {
-fx-background-color: #0d763b;
-fx-background-radius: 18;
-fx-border-radius: 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 5;
-fx-padding: 10;
}
.tab-pane .tab .tab-close-button {
-fx-text-fill: #0d9e3b;
}
.tab-pane .tab-header-area .headers-region {
-fx-background-color: #0d9e3b;
}
.tab .tab-label {
-fx-alignment: CENTER;
-fx-text-fill: #ffffff;
-fx-font-size: 12px;
-fx-font-weight: bold;
}
.tab:selected .tab-label {
-fx-alignment: CENTER;
-fx-text-fill: #ffffff;
}