Initial commit

This commit is contained in:
2023-03-24 15:48:54 +00:00
commit d77738eb38
26 changed files with 1957 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
.idea/cssdialects.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CssDialectMappings">
<file url="file://$PROJECT_DIR$/src/main/resources/chat.css" dialect="JavaFX" />
<file url="file://$PROJECT_DIR$/src/main/resources/global.css" dialect="JavaFX" />
</component>
</project>

7
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

14
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" default="true" project-jdk-name="19" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

124
.idea/uiDesigner.xml generated Normal file
View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# ECS Chat
Chat application with built-in game built as part of COMP1206 at the University of Southampton.
Thanks goes to Oli Bills for building the backend system and template for this software.

46
pom.xml Normal file
View File

@@ -0,0 +1,46 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>uk.ac.soton.comp1206</groupId>
<artifactId>app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>19.0.2.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.6</version>
<executions>
<execution>
<!-- Default configuration for running -->
<!-- Usage: mvn clean javafx:run -->
<id>default-cli</id>
<configuration>
<mainClass>uk.ac.soton.comp1206.App</mainClass>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,4 @@
module uk.ac.soton.comp1206 {
requires javafx.controls;
exports uk.ac.soton.comp1206;
}

View File

@@ -0,0 +1,30 @@
package uk.ac.soton.comp1206;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
/**
* JavaFX App
*/
public class App extends Application {
@Override
public void start(Stage stage) {
var javaVersion = SystemInfo.javaVersion();
var javafxVersion = SystemInfo.javafxVersion();
var label = new Label("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".");
var scene = new Scene(new StackPane(label), 640, 480);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}

View File

@@ -0,0 +1,13 @@
package uk.ac.soton.comp1206;
public class SystemInfo {
public static String javaVersion() {
return System.getProperty("java.version");
}
public static String javafxVersion() {
return System.getProperty("javafx.version");
}
}

View File

@@ -0,0 +1,252 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.animation.*;
import javafx.beans.property.DoubleProperty;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.SplitPane;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Window;
import javafx.util.Duration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.App;
import uk.mgrove.ac.soton.comp1206.network.Communicator;
import java.util.Arrays;
/**
* Base window class, providing template from which all windows should extend
*/
public abstract class BaseWindow extends Window {
protected static final Logger logger = LogManager.getLogger(ChatWindow.class);
protected final App app;
protected final Communicator communicator;
/**
* Duration over which menus should slide open/closed
*/
protected final static Duration menuTransitionDuration = Duration.millis(300);
/**
* Duration over which buttons should rotate - used for menu open/close buttons
*/
protected final static Duration buttonRotationDuration = Duration.millis(150);
/**
* Duration over which windows should fade in
*/
protected final static Duration windowFadeInDuration = Duration.millis(500);
@FXML
protected VBox rightPanel;
@FXML
protected VBox leftPanel;
@FXML
protected SplitPane mainSplitPane;
@FXML
protected ImageView showLeftPanel;
@FXML
protected ImageView showRightPanel;
@FXML
protected ImageView hideLeftPanel;
@FXML
protected ImageView hideRightPanel;
@FXML
protected StackPane mainStackPane;
protected Parent root;
protected Node rightPanelPane;
protected Node leftPanelPane;
protected double[] mainSplitPaneDividerPositions;
protected boolean[] sidePanelsVisible;
/**
* Initialise the window with an FXML template
* @param app app to use
* @param communicator communicator to use
* @param fxmlPath path to FXML file
*/
public BaseWindow(App app, Communicator communicator, String fxmlPath) {
this.app = app;
this.communicator = communicator;
//Load the Chat Window GUI
try {
//Instead of building this GUI programmatically, we are going to use FXML
var loader = new FXMLLoader(getClass().getResource(fxmlPath));
//Link the GUI in the FXML to this class
loader.setController(this);
root = loader.load();
} catch (Exception e) {
//Handle any exceptions with loading the FXML
logger.error("Unable to read file: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
root.setOpacity(0);
fadeInWindow();
//We are the login window
setScene(new Scene(root));
rightPanelPane = mainSplitPane.getItems().get(2);
leftPanelPane = mainSplitPane.getItems().get(0);
mainSplitPaneDividerPositions = mainSplitPane.getDividerPositions();
sidePanelsVisible = new boolean[2];
Arrays.fill(sidePanelsVisible, Boolean.TRUE);
mainStackPane.getChildren().removeAll(showLeftPanel, showRightPanel);
}
/**
* Fade the root node from invisible to visible
*/
protected void fadeInWindow() {
FadeTransition ft = new FadeTransition(windowFadeInDuration, root);
ft.setFromValue(0);
ft.setToValue(1);
ft.play();
}
/**
* Hide a given pane in a ScrollPane - used to hide menus
*
* @param panelPane the pane to hide
* @param dividerPositionProperty the property for the appropriate divider's position
* @param hideButton button to hide the pane
* @param showButton button to show the pane
* @param panelIndex index of the panel in local info stores (e.g. list of open/closed menus)
*/
protected void hidePanel(Pane panelPane, DoubleProperty dividerPositionProperty, Node hideButton, Node showButton, int panelIndex) {
panelPane.setMinWidth(0);
int finalDividerPosition = panelIndex == 0 ? 0 : 1;
KeyValue keyValue = new KeyValue(dividerPositionProperty, finalDividerPosition, Interpolator.EASE_BOTH);
Timeline hidePanel = new Timeline(new KeyFrame(menuTransitionDuration, keyValue));
RotateTransition rotateButton = new RotateTransition(buttonRotationDuration, hideButton);
rotateButton.setFromAngle(0);
rotateButton.setToAngle(panelIndex == 0 ? 180 : -180);
rotateButton.setOnFinished((event2) -> {
mainStackPane.getChildren().add(showButton);
mainStackPane.getChildren().remove(hideButton);
hideButton.setRotate(0);
});
hidePanel.play();
hidePanel.setOnFinished((event) -> {
rotateButton.play();
sidePanelsVisible[panelIndex] = false;
mainSplitPane.getItems().remove(panelPane);
if (panelIndex == 0 && sidePanelsVisible[1])
mainSplitPane.setDividerPosition(0, mainSplitPaneDividerPositions[1]);
});
}
/**
* Show a given pane in a ScrollPane - used to show menus
*
* @param panelPane the pane to show
* @param hideButton button to hide the pane
* @param showButton button to show the pane
* @param panelIndex index of the panel in local info stores (e.g. list of open/closed menus)
*/
protected void showPanel(Pane panelPane, Node hideButton, Node showButton, int panelIndex) {
if (panelIndex == 0) mainSplitPane.getItems().add(0, panelPane);
else mainSplitPane.getItems().add(panelPane);
DoubleProperty dividerPositionProperty = mainSplitPane.getDividers().get(panelIndex == 1 && !sidePanelsVisible[0] ? 0 : panelIndex).positionProperty();
int initialDividerPosition = panelIndex == 0 ? 0 : 1;
mainSplitPane.setDividerPosition(panelIndex == 1 && !sidePanelsVisible[0] ? 0 : panelIndex, initialDividerPosition);
KeyValue keyValue = new KeyValue(dividerPositionProperty, mainSplitPaneDividerPositions[panelIndex], Interpolator.EASE_BOTH);
Timeline showPanel = new Timeline(new KeyFrame(menuTransitionDuration, keyValue));
RotateTransition rotateButton = new RotateTransition(buttonRotationDuration, showButton);
rotateButton.setFromAngle(0);
rotateButton.setToAngle(panelIndex == 0 ? -180 : 180);
rotateButton.setOnFinished((event2) -> {
mainStackPane.getChildren().add(hideButton);
mainStackPane.getChildren().remove(showButton);
showButton.setRotate(0);
});
showPanel.play();
showPanel.setOnFinished((event1) -> {
rotateButton.play();
sidePanelsVisible[panelIndex] = true;
panelPane.setMinWidth(Region.USE_COMPUTED_SIZE);
if (panelIndex == 0 && sidePanelsVisible[1])
mainSplitPane.setDividerPosition(1, mainSplitPaneDividerPositions[1]);
});
}
/**
* Toggle the visibility of the right-hand sidebar
*/
protected void toggleRightPanel() {
if (sidePanelsVisible[1]) {
DoubleProperty positionProperty;
if (sidePanelsVisible[0]) {
mainSplitPaneDividerPositions[1] = mainSplitPane.getDividerPositions()[1];
positionProperty = mainSplitPane.getDividers().get(1).positionProperty();
} else {
mainSplitPaneDividerPositions[1] = mainSplitPane.getDividerPositions()[0];
positionProperty = mainSplitPane.getDividers().get(0).positionProperty();
}
hidePanel(rightPanel, positionProperty, hideRightPanel, showRightPanel, 1);
} else {
showPanel(rightPanel, hideRightPanel, showRightPanel, 1);
}
}
/**
* Toggle the visibility of the right-hand sidebar
*/
protected void toggleLeftPanel() {
if (sidePanelsVisible[0]) {
mainSplitPaneDividerPositions[0] = mainSplitPane.getDividerPositions()[0];
if (sidePanelsVisible[1]) mainSplitPaneDividerPositions[1] = mainSplitPane.getDividerPositions()[1];
hidePanel(leftPanel, mainSplitPane.getDividers().get(0).positionProperty(), hideLeftPanel, showLeftPanel, 0);
} else {
if (sidePanelsVisible[1]) mainSplitPaneDividerPositions[1] = mainSplitPane.getDividerPositions()[0];
showPanel(leftPanel, hideLeftPanel, showLeftPanel, 0);
}
}
/**
* Handle mouse click to toggle visibility of right-hand sidebar
*
* @param event mouse clicked
*/
public void toggleRightPanel(MouseEvent event) {
toggleRightPanel();
}
/**
* Handle mouse click to toggle visibility of right-hand sidebar
*
* @param event mouse clicked
*/
public void toggleLeftPanel(MouseEvent event) {
toggleLeftPanel();
}
}

View File

@@ -0,0 +1,262 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.util.Duration;
import uk.mgrove.ac.soton.comp1206.App;
import uk.mgrove.ac.soton.comp1206.network.Communicator;
import uk.mgrove.ac.soton.comp1206.utility.Utility;
import java.awt.event.ActionEvent;
import java.util.Arrays;
/**
* Chat window for displaying chat interface
*/
public class ChatWindow extends BaseWindow {
private final static Duration messageFadeInDuration = Duration.millis(150);
@FXML
private TextField usersSearchField;
@FXML
private Text myUsernameText;
@FXML
private TextField myUsernameInput;
@FXML
private ScrollPane messagesContainer;
@FXML
private Label noMessagesYetText;
@FXML
private VBox messages;
@FXML
private TextField messageToSend;
@FXML
private CheckBox audioEnabled;
@FXML
private HBox myUsernameInputContainer;
@FXML
private HBox myUsernameTextContainer;
@FXML
private VBox myUsernameContainer;
@FXML
private final UserList userList;
@FXML
private VBox rightPanel;
@FXML
private VBox leftPanel;
@FXML
private SplitPane mainSplitPane;
@FXML
private ImageView showLeftPanel;
@FXML
private ImageView showRightPanel;
@FXML
private ImageView hideLeftPanel;
@FXML
private ImageView hideRightPanel;
@FXML
private StackPane mainStackPane;
@FXML
private Button openGame;
private boolean messagesReceived;
private boolean scrollToBottom;
private boolean lastMessageFromMe;
/**
* Initialise the chat window
* @param app the app
* @param communicator the communicator
*/
public ChatWindow(App app, Communicator communicator) {
super(app, communicator, "/chat.fxml");
//Link the communicator to this window
communicator.setWindow(this);
getScene().addPostLayoutPulseListener(this::scrollToBottom);
StringProperty userSearchProperty = new SimpleStringProperty("");
userList = new UserList(userSearchProperty);
Platform.runLater(() -> rightPanel.getChildren().add(userList));
usersSearchField.textProperty().bindBidirectional(userSearchProperty);
usersSearchField.setOnKeyReleased((event) -> {
// refresh list of people
userList.updateDisplayedUsers();
});
hideMyUsernameInput(false);
rightPanelPane = mainSplitPane.getItems().get(2);
leftPanelPane = mainSplitPane.getItems().get(0);
mainSplitPaneDividerPositions = mainSplitPane.getDividerPositions();
sidePanelsVisible = new boolean[2];
Arrays.fill(sidePanelsVisible, Boolean.TRUE);
mainStackPane.getChildren().removeAll(showLeftPanel, showRightPanel);
messagesReceived = false;
myUsernameInput.textProperty().bindBidirectional(app.usernameProperty());
myUsernameText.textProperty().bind(app.usernameProperty());
audioEnabled.selectedProperty().bindBidirectional(Utility.audioEnabledProperty());
openGame.setOnAction((e) -> {
app.openGame();
});
communicator.addListener(this::receiveMessage);
Platform.runLater(() -> messageToSend.requestFocus());
}
/**
* Handle what happens when the user presses enter on the message to send field
*
* @param event key pressed
*/
@FXML
protected void handleUsernameKeypress(KeyEvent event) {
if (event.getCode() != KeyCode.ENTER) return;
// hide input
hideMyUsernameInput(true);
messageToSend.requestFocus();
}
/**
* Handle what happens when the user presses enter on the message to send field
*
* @param event key pressed
*/
@FXML
protected void handleMessageToSendKeypress(KeyEvent event) {
if (event.getCode() != KeyCode.ENTER) return;
sendCurrentMessage(messageToSend.getText());
}
/**
* Handle what happens when the user presses the send message button
*
* @param event button clicked
*/
@FXML
protected void handleSendMessage(ActionEvent event) {
String message = messageToSend.getText();
if (message.isBlank()) return;
sendCurrentMessage(message);
}
/**
* Handle event to show username input when button pressed
*
* @param event button clicked
*/
public void showMyUsernameInput(MouseEvent event) {
showMyUsernameInput();
}
/**
* Handle show username input
*/
public void showMyUsernameInput() {
myUsernameContainer.getChildren().remove(myUsernameTextContainer);
myUsernameContainer.getChildren().add(myUsernameInputContainer);
myUsernameInput.requestFocus();
myUsernameInput.end();
}
/**
* Handle event to hide username input when button pressed
*
* @param event button clicked
*/
public void hideMyUsernameInput(MouseEvent event) {
hideMyUsernameInput(true);
}
/**
* Handle hide username input, with option to only remove input container and not add text container
*
* @param showTextContainer add text container to window
*/
private void hideMyUsernameInput(boolean showTextContainer) {
myUsernameContainer.getChildren().remove(myUsernameInputContainer);
if (showTextContainer) {
myUsernameContainer.getChildren().add(myUsernameTextContainer);
messageToSend.requestFocus();
}
}
/**
* Handle an incoming message from the Communicator
*
* @param text The message that has been received, in the form User:Message
*/
public void receiveMessage(String text) {
if (!text.contains(":")) return;
if (!messagesReceived) {
messages.getChildren().remove(noMessagesYetText);
messagesReceived = true;
}
var components = text.split(":");
if (components.length < 2) return;
var username = components[0];
var message = String.join(":", Arrays.copyOfRange(components, 1, components.length));
userList.addUser(username);
Message receivedMessage = new Message(app, username, message);
receivedMessage.setOpacity(0);
FadeTransition fadeMessage = new FadeTransition(messageFadeInDuration, receivedMessage);
fadeMessage.setToValue(1);
messages.getChildren().add(receivedMessage);
fadeMessage.play();
if (messagesContainer.getVvalue() == 1.0f) scrollToBottom = true;
if (!lastMessageFromMe) Utility.playAudio("incoming.mp3");
else lastMessageFromMe = false;
}
/**
* Scroll to bottom of messages list.
*/
private void scrollToBottom() {
if (!scrollToBottom) return;
messagesContainer.setVvalue(1.0);
scrollToBottom = false;
}
/**
* Send an outgoing message from the Chat window.
*
* @param text The text of the message to send to the Communicator
*/
private void sendCurrentMessage(String text) {
if (!text.isBlank()) {
if (text.strip().equals("/game")) app.openGame();
else if (text.matches("/nick (.+)$")) app.setUsername(text.substring(6));
else {
communicator.send(app.getUsername() + ":" + text);
lastMessageFromMe = true;
}
messageToSend.clear();
}
}
/**
* Unhide this window
*/
public void show() {
super.show();
}
}

View File

@@ -0,0 +1,67 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.scene.control.Hyperlink;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import uk.mgrove.ac.soton.comp1206.App;
import uk.mgrove.ac.soton.comp1206.utility.Utility;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
/**
* Class for a message, which displays the message in a formatted way
*/
public class Message extends TextFlow {
private final App app;
private final String username;
private final String message;
private final LocalDateTime received;
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
/**
* Initialise the message with basic info
* @param app the app
* @param username username of user who sent the message
* @param message the message contents
*/
public Message(App app, String username, String message) {
this.app = app;
this.username = username;
this.message = message;
received = LocalDateTime.now();
build();
}
/**
* Build the message
*/
public void build() {
Text timeField = new Text("[" + formatter.format(received) + "] ");
timeField.getStyleClass().add("timestamp");
Text usernameField = new Text(username + ": ");
usernameField.getStyleClass().add("username");
getChildren().addAll(timeField,usernameField);
Matcher urlMatcher = Utility.getUrlPattern().matcher(message);
var previousMatchEnd = 0;
while (urlMatcher.find()) {
if (urlMatcher.start() > previousMatchEnd) {
getChildren().add(new Text(message.substring(previousMatchEnd, urlMatcher.start())));
}
var url = message.substring(urlMatcher.start(), urlMatcher.end());
var urlField = new Hyperlink(url);
urlField.setOnAction((event) -> app.getHostServices().showDocument(url));
getChildren().add(urlField);
previousMatchEnd = urlMatcher.end();
}
if (message.length() > previousMatchEnd) {
getChildren().add(new Text(message.substring(previousMatchEnd)));
}
getStyleClass().add("message-container");
}
}

View File

@@ -0,0 +1,15 @@
package uk.mgrove.ac.soton.comp1206.ui;
/**
* Interface for listeners that handle new messages being received by the Communicator class
* @see uk.mgrove.ac.soton.comp1206.network.Communicator
*/
public interface MessageListener {
/**
* Receive a message
* @param message the message to receive
*/
void receiveMessage(String message);
}

View File

@@ -0,0 +1,58 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.animation.FadeTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
/**
* Class for a user, which is an element to be displayed in the chat window
*/
public class User extends VBox {
private final String username;
private final IntegerProperty messageCount;
/**
* Initialise the user with a username
* @param username username of user
*/
public User(String username) {
setSpacing(2);
setPadding(new Insets(0, 12,0,12));
setFillWidth(true);
this.username = username;
this.messageCount = new SimpleIntegerProperty(1);
var messageCountLabel = new Label("");
messageCountLabel.getStyleClass().add("italic");
messageCountLabel.setWrapText(true);
messageCountLabel.textProperty().bind(Bindings.concat(messageCount.asString()," message(s)"));
var usernameLabel = new Label(username);
usernameLabel.setWrapText(true);
getChildren().addAll(
usernameLabel,
messageCountLabel
);
}
/**
* Get property for number of messages sent by user
* @return property
*/
public IntegerProperty messageCountProperty() {
return messageCount;
}
/**
* Get user's username
* @return username
*/
public String getUsername() {
return username;
}
}

View File

@@ -0,0 +1,81 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.animation.FadeTransition;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
/**
* List of users to be displayed in chat window
*/
public class UserList extends ScrollPane {
private final Map<String,IntegerProperty> userMessageCounts = new HashMap<>();
private final VBox container;
private final ArrayList<User> userElements = new ArrayList<>();
private final StringProperty userSearchProperty;
/**
* Initialise the list of users
* @param userSearchProperty property for content of users search field
*/
public UserList(StringProperty userSearchProperty) {
setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
setHbarPolicy(ScrollBarPolicy.NEVER);
getStyleClass().add("user-list");
setFitToWidth(true);
container = new VBox();
container.setSpacing(12);
container.setFillWidth(true);
setContent(container);
this.userSearchProperty = userSearchProperty;
}
/**
* Add user to list of users
* @param username username of user to add
*/
public void addUser(String username) {
if (userMessageCounts.containsKey(username)) {
var property = userMessageCounts.get(username);
property.set(property.get() + 1);
} else {
var newUser = new User(username);
userMessageCounts.put(username, newUser.messageCountProperty());
newUser.setOpacity(0);
FadeTransition fadeMessage = new FadeTransition(Duration.millis(150), newUser);
fadeMessage.setToValue(1);
userElements.add(newUser);
fadeMessage.play();
sortUserElements();
updateDisplayedUsers();
}
}
private void sortUserElements() {
userElements.sort(Comparator.comparing(User::getUsername));
}
/**
* Update the list of users displayed
*/
public void updateDisplayedUsers() {
if (userSearchProperty.get().isBlank()) {
container.getChildren().setAll(userElements);
} else {
container.getChildren().setAll(userElements.stream().filter((user) -> user.getUsername().toLowerCase().contains(userSearchProperty.get().toLowerCase())).toList());
}
}
}

View File

@@ -0,0 +1,14 @@
package uk.mgrove.ac.soton.comp1206.ui;
/**
* Interface for listeners that handle a game block being clicked
*/
public interface BlockClickedListener {
/**
* Handle game block being clicked
* @param block block that was clicked
*/
void blockClicked(GameBlock block);
}

View File

@@ -0,0 +1,114 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import java.util.concurrent.ThreadLocalRandom;
/**
* Class for a game block, which is a single block with a colour that can be changed
*/
public class GameBlock extends Canvas {
private final int x;
private final int y;
private final Color[] colours;
private final double width;
private final double height;
private final IntegerProperty value;
private final boolean showStrokes;
private final boolean roundedCorners;
/**
* Initialise the game block with a set of available colours, a set size,
* a set location in the grid, and outlines around the block
* @param colours available colours
* @param x column index of location in grid
* @param y row index of location in grid
* @param width block width
* @param height block height
*/
public GameBlock(Color[] colours, int x, int y, double width, double height) {
this(colours,x,y,width,height,true,false);
}
/**
* Initialise the game block with a set of available colours, a set size,
* a set location in the grid, and optional outlines around the block
* @param colours available colours
* @param x column index of location in grid
* @param y row index of location in grid
* @param width block width
* @param height block height
* @param showStrokes whether block should have an outline
* @param roundedCorners whether block should have rounded corners
*/
public GameBlock(Color[] colours, int x, int y, double width, double height, boolean showStrokes, boolean roundedCorners) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.colours = colours;
this.showStrokes = showStrokes;
this.roundedCorners = roundedCorners;
value = new SimpleIntegerProperty();
setWidth(width);
setHeight(height);
value.addListener(((observableValue, oldValue, newValue) -> paint()));
paint();
}
/**
* Paint the block - i.e. draw it
*/
public void paint() {
GraphicsContext gc = getGraphicsContext2D();
gc.clearRect(0,0,width,height);
if (value.get() >= 0) {
// fill in colour block
gc.setFill(colours[value.get()]);
gc.setStroke(Color.BLACK);
if (roundedCorners) {
gc.fillRoundRect(0,0,width,height,16,16);
if (showStrokes) gc.strokeRoundRect(0,0,width,height,16,16);
} else {
gc.fillRect(0,0,width,height);
if (showStrokes) gc.strokeRect(0,0,width,height);
}
}
}
/**
* Get the x-coordinate of this block in the grid
* @return x-coordinate
*/
public int getX() {
return x;
}
/**
* Get the y-coordinate of this block in the grid
* @return y-coordinate
*/
public int getY() {
return y;
}
/**
* Get the value property for binding, which holds the current colour value
* @return value property
*/
public IntegerProperty valueProperty() {
return value;
}
}

View File

@@ -0,0 +1,63 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.List;
/**
* Game grid class, which displays a grid of game blocks for the game
*/
public class GameGrid extends GridPane {
private final List<BlockClickedListener> listeners = new ArrayList<>();
/**
* Initialise game grid with an existing grid, a set size, and a set of available colours
* @param colours available colours
* @param grid existing grid to use
* @param width game grid width
* @param height game grid height
*/
public GameGrid(Color[] colours, Grid grid, int width, int height) {
var cols = grid.getCols();
var rows = grid.getRows();
setMaxWidth(width);
setMaxHeight(height);
setGridLinesVisible(true);
// create each internal game block
for (var y=0; y < rows; y++) {
for (var x=0; x < cols; x++) {
var block = new GameBlock(colours, x, y, (double) width/cols, (double) height/rows);
add(block,x,y);
block.valueProperty().bind(grid.getGridProperty(x,y));
// handle when game block is clicked
block.setOnMouseClicked((e) -> blockClicked(block));
}
}
}
/**
* Add listener for blocks being clicked
* @param listener listener
*/
public void addListener(BlockClickedListener listener) {
this.listeners.add(listener);
}
/**
* Handle block being clicked by calling all appropriate listeners
* @param block block that was clicked
*/
public void blockClicked(GameBlock block) {
for (BlockClickedListener listener : listeners) {
listener.blockClicked(block);
}
}
}

View File

@@ -0,0 +1,238 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.SplitPane;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.App;
import uk.mgrove.ac.soton.comp1206.network.Communicator;
import uk.mgrove.ac.soton.comp1206.utility.Utility;
import java.util.*;
/**
* Game window which will display mini-game for users to play
*/
public class GameWindow extends BaseWindow {
private static final Logger logger = LogManager.getLogger(GameWindow.class);
private static final int maxColourChanges = 30;
private final Grid grid;
private final Random random = new Random();
private final Color[] colours;
private final IntegerProperty currentColour = new SimpleIntegerProperty();
private final IntegerProperty score = new SimpleIntegerProperty(0);
private Timer gameTimer;
private int colourChangesCount;
private final List<String> scores = new ArrayList<>();
@FXML
private BorderPane gameBoardContainer;
@FXML
private Text currentScoreField;
@FXML
private VBox currentColourContainer;
@FXML
private VBox highScoresContainer;
@FXML
private VBox gameCoverContainer;
@FXML
private Text gameCoverTitle;
@FXML
private ProgressBar progressBar;
/**
* Initialise the game window
*
* @param app the app
* @param communicator the communicator
*/
public GameWindow(App app, Communicator communicator) {
super(app, communicator, "/game.fxml");
colours = new Color[]{
Color.web("0x005C84"),
Color.web("0x74C9E5"),
Color.web("0x4BB694"),
Color.web("0xC1D100"),
Color.web("0xEF7D00"),
Color.web("0xE73037"),
Color.web("0xD5007F"),
Color.web("0x8D3970")
};
grid = new Grid(6, 6);
currentScoreField.textProperty().bind(score.asString());
var currentColourBlock = new GameBlock(colours, 0, 0, 100, 100, false, true);
currentColourContainer.getChildren().add(currentColourBlock);
currentColourBlock.valueProperty().bind(currentColour);
communicator.addListener((message) -> {
if (!message.startsWith("SCORES")) return;
Platform.runLater(() -> this.receiveScore(message));
});
communicator.send("SCORES");
}
/**
* Events that occur at regular intervals to maintain the game: update the current colour and handle game ending
*/
public void gameLoop() {
if (colourChangesCount < maxColourChanges) {
var randomColour = random.nextInt(colours.length);
logger.info("Game loop - Colour chosen: {}", randomColour);
currentColour.set(randomColour);
colourChangesCount++;
progressBar.setProgress((double) (maxColourChanges - colourChangesCount + 1) / maxColourChanges);
} else {
progressBar.setProgress(0);
endGame();
}
}
/**
* Handle game block being clicked - adjust score appropriately
*
* @param block block that was clicked
*/
public void blockClicked(GameBlock block) {
var gridBlock = grid.getGridProperty(block.getX(), block.getY());
if (currentColour.get() == gridBlock.get()) {
logger.info("{} is {}, scoring 5 points", currentColour.get(), gridBlock.getValue());
score.set(score.get() + 5);
gridBlock.set(random.nextInt(colours.length));
} else {
logger.info("{} is not {}, losing 1 point", currentColour.get(), gridBlock.getValue());
if (score.get() > 0) {
score.set(score.get() - 1);
}
}
}
/**
* Receive and store scores from server
* @param message message containing scores
*/
private void receiveScore(String message) {
scores.clear();
for (var line : message.split("\n")) {
if (!line.equals("SCORES")) {
int splitIndex = line.lastIndexOf("=");
try {
String[] info = {line.substring(0,splitIndex), line.substring(splitIndex+1)};
scores.add(info[0] + ": " + info[1]);
} catch (Exception ignored) {
logger.error("Couldn't load score from message {}",message);
}
}
}
highScoresContainer.getChildren().clear();
for (var i=0; i < Math.min(scores.size(),5); i++) {
highScoresContainer.getChildren().add(new Text(scores.get(i)));
}
}
/**
* Show game over screen
*/
private void showGameOver() {
gameCoverTitle.setText("Game Over!");
Platform.runLater(() -> gameBoardContainer.setCenter(gameCoverContainer));
// TODO: include a timer bar
}
/**
* Start a new game
*/
private void startGame() {
colourChangesCount = 0;
score.set(0);
// set up grid with random values
for (int y = 0; y < grid.getRows(); y++) {
for (int x = 0; x < grid.getCols(); x++) {
grid.set(x, y, random.nextInt(colours.length));
}
}
currentColour.set(random.nextInt(colours.length));
var gameGrid = new GameGrid(colours, grid, 400, 350);
gameGrid.addListener(this::blockClicked);
gameBoardContainer.setCenter(gameGrid);
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
gameLoop();
}
};
gameTimer = new Timer("Timer");
gameTimer.schedule(timerTask, 0, 2000);
}
/**
* Start game when button clicked
* @param event button clicked
*/
public void startGame(ActionEvent event) {
startGame();
}
/**
* Return to the chat window when button clicked
* @param event button clicked
*/
public void returnToChat(ActionEvent event) {
stopGameTimer();
app.setGameOpen(false);
app.returnToChat(this);
}
/**
* Stop the game
*/
private void endGame() {
// game finished
stopGameTimer();
currentColour.set(-1);
communicator.send(String.format("SCORE %s %d",app.getUsername(),score.get()));
communicator.send("SCORES");
showGameOver();
}
/**
* Stop the game timer
*/
public void stopGameTimer() {
if (gameTimer != null) gameTimer.cancel();
}
}

View File

@@ -0,0 +1,79 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
/**
* Grid class, used to store a grid of game blocks for the game
*/
public class Grid {
private final int cols;
private final int rows;
final SimpleIntegerProperty[][] grid;
/**
* Initialise the grid
* @param cols number of columns in the grid
* @param rows number of rows in the grid
*/
public Grid(int cols, int rows) {
this.cols = cols;
this.rows = rows;
grid = new SimpleIntegerProperty[cols][rows];
for (var y=0; y < rows; y++) {
for (var x=0; x < cols; x++) {
grid[x][y] = new SimpleIntegerProperty(0);
}
}
}
/**
* Get the property relating to a specific element in the grid
* @param x column index
* @param y row index
* @return the property relating to the element at the given location
*/
public IntegerProperty getGridProperty(int x, int y) {
return grid[x][y];
}
/**
* Set the value of a specific element in the grid
* @param x column index
* @param y row index
* @param value the value to set
*/
public void set(int x, int y, int value) {
grid[x][y].set(value);
}
/**
* Get the value of a specific element in the grid
* @param x column index
* @param y row index
* @return the value of the element
*/
public int get(int x, int y) {
return grid[x][y].get();
}
/**
* Get number of columns in the grid
* @return number of columns
*/
public int getCols() {
return cols;
}
/**
* Get number of rows in the grid
* @return number of rows
*/
public int getRows() {
return rows;
}
}

View File

@@ -0,0 +1,204 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import java.net.URL?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.Cursor?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<SplitPane fx:id="mainSplitPane" dividerPositions="0.14, 0.75" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="500.0" minWidth="800.0" prefHeight="628.0" prefWidth="1105.0" styleClass=".root" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1">
<items>
<VBox fx:id="leftPanel" alignment="TOP_RIGHT" spacing="4.0">
<children>
<Button alignment="TOP_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false" styleClass="active" text="Chat" />
<Button alignment="TOP_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Game" />
<Button alignment="TOP_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Whiteboard" />
<Button alignment="TOP_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false">
<graphic>
<CheckBox fx:id="audioEnabled" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" selected="true" text="Sound" />
</graphic>
<VBox.margin>
<Insets top="12.0" />
</VBox.margin>
</Button>
</children>
<styleClass>
<String fx:value="menu" />
<String fx:value="left-menu" />
</styleClass>
</VBox>
<StackPane fx:id="chatContainerStackPane" prefHeight="150.0" prefWidth="200.0">
<children>
<VBox maxHeight="1.7976931348623157E308" spacing="4.0" styleClass="chat-container">
<children>
<HBox prefHeight="28.0" styleClass="header">
<children>
<ImageView fitHeight="42.0" fitWidth="42.0" pickOnBounds="true" preserveRatio="true" styleClass="logo">
<image>
<Image url="@uos_logo.png" />
</image>
<HBox.margin>
<Insets bottom="8.0" left="12.0" right="12.0" top="8.0" />
</HBox.margin>
</ImageView>
<VBox alignment="CENTER_LEFT" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" styleClass="h2" text="ECS Chat" />
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="The ultimate chatroom." />
</children>
<opaqueInsets>
<Insets />
</opaqueInsets>
<HBox.margin>
<Insets bottom="4.0" />
</HBox.margin>
</VBox>
</children>
</HBox>
<Separator prefWidth="200.0" />
<ScrollPane fx:id="messagesContainer" fitToWidth="true" focusTraversable="false" hbarPolicy="NEVER" maxHeight="1.7976931348623157E308" styleClass="messages-container" vvalue="1.0" VBox.vgrow="ALWAYS">
<content>
<VBox fx:id="messages" maxWidth="1.7976931348623157E308" styleClass="messages">
<children>
<Label fx:id="noMessagesYetText" text="No messages yet." wrapText="true" />
</children>
</VBox>
</content>
<opaqueInsets>
<Insets />
</opaqueInsets>
</ScrollPane>
<HBox alignment="CENTER_LEFT" maxWidth="1.7976931348623157E308" spacing="4.0">
<children>
<TextField fx:id="messageToSend" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" onKeyPressed="#handleMessageToSendKeypress" promptText="Type a message..." HBox.hgrow="ALWAYS">
<styleClass>
<String fx:value="message-input" />
<String fx:value="text-field-no-outline" />
</styleClass>
</TextField>
<Button mnemonicParsing="false" styleClass="button-primary" text="Send" />
</children>
<styleClass>
<String fx:value="send-message-container" />
<String fx:value="outline-section" />
</styleClass>
</HBox>
</children>
</VBox>
<ImageView fx:id="showRightPanel" fitHeight="24.0" fitWidth="24.0" onMouseClicked="#toggleRightPanel" pickOnBounds="true" preserveRatio="true" StackPane.alignment="CENTER_RIGHT">
<image>
<Image url="@angle_left_icon.png" />
</image>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</ImageView>
<ImageView fx:id="showLeftPanel" fitHeight="24.0" fitWidth="24.0" onMouseClicked="#toggleLeftPanel" pickOnBounds="true" preserveRatio="true" StackPane.alignment="CENTER_LEFT">
<image>
<Image url="@angle_right_icon.png" />
</image>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</ImageView>
<ImageView fx:id="hideLeftPanel" fitHeight="24.0" fitWidth="24.0" onMouseClicked="#toggleLeftPanel" pickOnBounds="true" preserveRatio="true" StackPane.alignment="CENTER_LEFT">
<image>
<Image url="@angle_left_icon.png" />
</image>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</ImageView>
<ImageView fx:id="hideRightPanel" fitHeight="24.0" fitWidth="24.0" onMouseClicked="#toggleRightPanel" pickOnBounds="true" preserveRatio="true" StackPane.alignment="CENTER_RIGHT">
<image>
<Image url="@angle_right_icon.png" />
</image>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</ImageView>
</children>
</StackPane>
<VBox fx:id="rightPanel" prefHeight="200.0" prefWidth="100.0" spacing="4.0">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" styleClass="h3" text="Connected Users" />
<HBox alignment="CENTER_LEFT" spacing="4.0" styleClass="outline-section">
<children>
<TextField fx:id="usersSearchField" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" promptText="Search" styleClass="text-field-no-outline" HBox.hgrow="ALWAYS" />
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@search_icon.png" />
</image>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</ImageView>
</children>
</HBox>
<VBox>
<children>
<VBox fx:id="myUsernameContainer" styleClass="outline-section">
<children>
<HBox fx:id="myUsernameTextContainer" alignment="CENTER_LEFT" maxWidth="1.7976931348623157E308" onMouseClicked="#showMyUsernameInput" spacing="4.0">
<children>
<VBox alignment="CENTER_LEFT" maxWidth="1.7976931348623157E308" styleClass="my-nickname-container" HBox.hgrow="ALWAYS">
<children>
<Text fx:id="myUsernameText" strokeType="OUTSIDE" strokeWidth="0.0" text="Guest" />
</children>
</VBox>
<ImageView id="editMyNickname" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@pen_icon.png" />
</image>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</ImageView>
</children>
</HBox>
<HBox id="editMyNickname" fx:id="myUsernameInputContainer" alignment="CENTER_LEFT" maxWidth="1.7976931348623157E308" spacing="4.0">
<children>
<TextField fx:id="myUsernameInput" maxHeight="1.7976931348623157E308" onKeyPressed="#handleUsernameKeypress" text="Guest" HBox.hgrow="ALWAYS">
<styleClass>
<String fx:value="edit-nickname-input" />
<String fx:value="text-field-no-outline" />
</styleClass>
</TextField>
<ImageView id="saveMyNickname" fitHeight="16.0" fitWidth="16.0" onMouseClicked="#hideMyUsernameInput" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@check_icon.png" />
</image>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</ImageView>
</children>
</HBox>
</children>
</VBox>
</children>
</VBox>
</children>
<styleClass>
<String fx:value="menu" />
<String fx:value="right-menu" />
</styleClass>
</VBox>
</items>
<stylesheets>
<URL value="@chat.css" />
<URL value="@global.css" />
</stylesheets>
</SplitPane>

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import java.net.URL?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.Cursor?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<SplitPane fx:id="mainSplitPane" dividerPositions="0.14, 0.75" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="500.0" minWidth="800.0" prefHeight="628.0" prefWidth="1105.0" styleClass=".root" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1">
<items>
<VBox fx:id="leftPanel" alignment="TOP_RIGHT" spacing="4.0">
<children>
<Button alignment="TOP_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false" styleClass="active" text="Chat" />
<Button alignment="TOP_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Game" />
<Button alignment="TOP_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Whiteboard" />
<Button alignment="TOP_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false">
<graphic>
<CheckBox fx:id="audioEnabled" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" selected="true" text="Sound" />
</graphic>
<VBox.margin>
<Insets top="12.0" />
</VBox.margin>
</Button>
</children>
<styleClass>
<String fx:value="menu" />
<String fx:value="left-menu" />
</styleClass>
</VBox>
<StackPane fx:id="mainStackPane" prefHeight="150.0" prefWidth="200.0">
<children>
<VBox maxHeight="1.7976931348623157E308" spacing="4.0" styleClass="chat-container">
<children>
<HBox prefHeight="28.0" styleClass="header">
<children>
<ImageView fitHeight="42.0" fitWidth="42.0" pickOnBounds="true" preserveRatio="true" styleClass="logo">
<image>
<Image url="@uos_logo.png" />
</image>
<HBox.margin>
<Insets bottom="8.0" left="12.0" right="12.0" top="8.0" />
</HBox.margin>
</ImageView>
<VBox alignment="CENTER_LEFT" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" styleClass="h2" text="ECS Chat" />
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="The ultimate chatroom." />
</children>
<opaqueInsets>
<Insets />
</opaqueInsets>
<HBox.margin>
<Insets bottom="4.0" />
</HBox.margin>
</VBox>
</children>
</HBox>
<Separator prefWidth="200.0" />
</children>
</VBox>
<ImageView fx:id="showRightPanel" fitHeight="24.0" fitWidth="24.0" onMouseClicked="#toggleRightPanel" pickOnBounds="true" preserveRatio="true" StackPane.alignment="CENTER_RIGHT">
<image>
<Image url="@angle_left_icon.png" />
</image>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</ImageView>
<ImageView fx:id="showLeftPanel" fitHeight="24.0" fitWidth="24.0" onMouseClicked="#toggleLeftPanel" pickOnBounds="true" preserveRatio="true" StackPane.alignment="CENTER_LEFT">
<image>
<Image url="@angle_right_icon.png" />
</image>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</ImageView>
<ImageView fx:id="hideLeftPanel" fitHeight="24.0" fitWidth="24.0" onMouseClicked="#toggleLeftPanel" pickOnBounds="true" preserveRatio="true" StackPane.alignment="CENTER_LEFT">
<image>
<Image url="@angle_left_icon.png" />
</image>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</ImageView>
<ImageView fx:id="hideRightPanel" fitHeight="24.0" fitWidth="24.0" onMouseClicked="#toggleRightPanel" pickOnBounds="true" preserveRatio="true" StackPane.alignment="CENTER_RIGHT">
<image>
<Image url="@angle_right_icon.png" />
</image>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</ImageView>
</children>
</StackPane>
<VBox fx:id="rightPanel" prefHeight="200.0" prefWidth="100.0" spacing="4.0">
<styleClass>
<String fx:value="menu" />
<String fx:value="right-menu" />
</styleClass>
</VBox>
</items>
<stylesheets>
<URL value="@chat.css" />
<URL value="@global.css" />
</stylesheets>
</SplitPane>

View File

@@ -0,0 +1,93 @@
/* global */
root {
display: block;
}
.root > * {
-fx-padding: 4px;
-fx-background-color:#ffffff;
}
* {
-fx-font-family: "Roboto";
}
/* menus */
.menu {
-fx-padding: 32px 0 0 0;
}
.menu > .button {
-fx-text-alignment: left;
-fx-alignment: top-left !important;
}
/* checkboxes */
.check-box > .box {
-fx-background-color: transparent;
-fx-border-width: 1px;
-fx-border-color: #9FB1BD;
-fx-border-radius: 4px;
}
.check-box:selected > .box {
-fx-background-color: #E1E8EC;
}
.check-box:selected > .box > .mark,
.check-box:indeterminate > .box > .mark {
-fx-background-color: #005C84;
}
/* buttons and text fields */
.button {
-fx-text-fill: #2a2a2a;
-fx-font-family: "Arial";
-fx-font-weight: bold;
-fx-text-background-color: #2a2a2a;
-fx-background-color: transparent;
-fx-border-width: 1px;
-fx-border-color: #E1E8EC;
-fx-border-radius: 8px;
-fx-background-radius: 8px;
}
.button.active {
-fx-background-color: #E1E8EC;
-fx-text-background-color: #005C84;
-fx-text-fill: #005C84;
-fx-border-color: #758D9A;
}
.button-primary {
-fx-background-color: #005C84;
-fx-text-background-color: #fff;
-fx-text-fill: #fff;
-fx-border-color: #005C84;
}
.text-field, .button {
-fx-padding: 12px;
}
.text-field {
-fx-background-color: transparent;
-fx-border-width: 1px;
-fx-border-color: #cccccc;
-fx-border-radius: 8px;
-fx-background-radius: 8px;
-fx-padding: 12px;
}
/* headings */
.h2 {
-fx-font-size: 2em;
}
.h3 {
-fx-font-size: 1.4em;
}
/* headers */
.header .logo {
-fx-start-margin: 8px;
-fx-end-margin: 8px;
}
/* sections */
.outline-section {
-fx-border-width: 1px;
-fx-border-color: #E1E8EC;
-fx-border-radius: 8px;
-fx-padding: 12px;
}