feat(contacts): ajout de la classe Contact.java, ContactMap.java, gestion des objets Contact dans l'interface graphique

This commit is contained in:
bouclyma 2024-12-10 21:34:55 +01:00
parent 45f18e9309
commit e9480590a7
14 changed files with 649 additions and 36 deletions

View file

@ -3,8 +3,12 @@ module rtgre.chat {
requires javafx.fxml;
requires java.logging;
requires java.desktop;
requires javafx.swing;
requires net.synedra.validatorfx;
opens rtgre.chat to javafx.fxml;
exports rtgre.chat;
exports rtgre.chat.graphisme;
opens rtgre.chat.graphisme to javafx.fxml;
}

View file

@ -15,8 +15,8 @@ import java.util.logging.Logger;
public class ChatApplication extends Application {
public static final Logger LOGGER = Logger.getLogger(ChatApplication.class.getCanonicalName());
public class EssaiLogger {
/* . . . */
static {
try {
InputStream is = EssaiLogger.class.getClassLoader()
@ -26,8 +26,8 @@ public class ChatApplication extends Application {
LOGGER.log(Level.INFO, "Cannot read configuration file", e);
}
}
/* . . . */
}
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(ChatApplication.class.getResource("chat-view.fxml"));

View file

@ -2,22 +2,47 @@ package rtgre.chat;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import net.synedra.validatorfx.Check;
import net.synedra.validatorfx.TooltipWrapper;
import net.synedra.validatorfx.Validator;
import rtgre.chat.graphisme.ContactListViewCell;
import rtgre.modeles.Contact;
import rtgre.modeles.ContactMap;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Date;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import static rtgre.chat.ChatApplication.LOGGER;
public class ChatController implements Initializable {
private static final Pattern LOGIN_PATTERN = Pattern.compile("^([a-z][a-z0-9]{2,7})$");
private static final Pattern HOST_PATTERN = Pattern.compile("/^[a-z]*((\\:?)\\d{1,5})?$/gm");
public MenuItem hostAddMenuItem;
public MenuItem avatarMenuItem;
public MenuItem aboutMenuItem;
@ -28,11 +53,15 @@ public class ChatController implements Initializable {
public SplitPane exchangeSplitPane;
public ListView postListView;
public ListView roomsListView;
public ListView contactListView;
public ListView contactsListView;
public TextField messageTextField;
public Button sendButton;
public Label statusLabel;
public Label dateTimeLabel;
public Contact contact;
private ContactMap contactMap = new ContactMap();
private ObservableList<Contact> contactObservableList = FXCollections.observableArrayList();
Validator validatorLogin = new Validator();
@Override
@ -53,13 +82,74 @@ public class ChatController implements Initializable {
statusLabel.setText("not connected to " + hostComboBox.getValue());
connectionButton.disableProperty().bind(validatorLogin.containsErrorsProperty());
connectionButton.selectedProperty().addListener(this::handleConnection);
loginTextField.disableProperty().bind(connectionButton.selectedProperty());
hostComboBox.disableProperty().bind(connectionButton.selectedProperty());
avatarMenuItem.setOnAction(this::handleAvatarChange);
avatarImageView.setOnMouseClicked(this::handleAvatarChange);
initContactListView();
validatorLogin.createCheck()
.dependsOn("login", loginTextField.textProperty())
.withMethod(this::checkLogin)
.decorates(loginTextField)
.immediate();
/* /!\ Set-up d'environnement de test /!\ */
/* -------------------------------------- */
loginTextField.setText("riri");
connectionButton.setSelected(true);
}
private void handleAvatarChange(Event event) {
/**
* Ouvre une fenêtre de dialogue permettant de choisir son avatar
*/
FileChooser fileChooser = new FileChooser();
Stage stage = (Stage) avatarImageView.getScene().getWindow();
fileChooser.setTitle("Select Avatar");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg")
);
File selectedFile = fileChooser.showOpenDialog(stage);
if (selectedFile != null) {
avatarImageView.setImage(new Image(selectedFile.toURI().toString()));
}
}
private void handleConnection(Observable observable) {
/**
*
*/
if (connectionButton.isSelected()) {
java.awt.Image img = SwingFXUtils.fromFXImage(this.avatarImageView.getImage(), null);
this.contact = new Contact(loginTextField.getText(), img);
contactMap.put(this.contact.getLogin(), this.contact);
System.out.println("Nouveau contact : " + contact);
System.out.println(contactMap);
}
}
private void checkLogin(Check.Context context) {
String login = context.get("login");
if (!LOGIN_PATTERN.matcher(login).matches()) {
context.error("Format de login non respecté");
}
if (login.equals("system")) {
context.error("Le login ne peut pas être system");
}
}
private void statusNameUpdate(Event event) {
statusLabel.setText("not connected to " + hostComboBox.getValue());
}
@ -75,4 +165,20 @@ public class ChatController implements Initializable {
}
}
private void initContactListView() {
try {
contactsListView.setCellFactory(contactListView -> new ContactListViewCell());
contactsListView.setItems(contactObservableList);
File avatars = new File(getClass().getResource("avatars.png").toURI());
Contact riri = new Contact("riri", false, avatars);
Contact fifi = new Contact("fifi", true, avatars);
contactObservableList.add(riri);
contactMap.add(riri);
contactObservableList.add(fifi);
contactMap.add(fifi);
} catch (Exception e) {
LOGGER.severe(e.getMessage());
}
}
}

View file

@ -0,0 +1,88 @@
package rtgre.chat.graphisme;
import rtgre.chat.ChatController;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Pos;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import rtgre.modeles.Contact;
import java.awt.image.BufferedImage;
import static rtgre.chat.ChatApplication.LOGGER;
/**
* Classe modélisant la fabrique de cellule de la vue des contacts
* visibles/connectés {@link ChatController#contactsListView}.
*
* @see ListCell
*/
public class ContactListViewCell extends ListCell<Contact> {
/**
* Callback déclenchée à chaque modification d'un objet d'une liste d'observable.
*
* @param contact Le contact à mettre à jour
* @param empty La liste de cellule doit-elle être complètement remise à zéro ?
*/
@Override
protected void updateItem(Contact contact, boolean empty) {
super.updateItem(contact, empty);
if (empty) {
setGraphic(null);
}
else {
// Cas d'un contact
updateContact(contact);
}
}
/**
* Mise à jour de la cellule d'un contact.
*
* @param contact Le contact à mettre à jour
*/
private void updateContact(Contact contact) {
LOGGER.finest("Mise à jour de " + contact);
String unreadCountNotif = (contact.getUnreadCount() == 0) ? "" : " (%d)".formatted(contact.getUnreadCount());
LOGGER.finest("unread: %s %s".formatted(contact.getLogin(), unreadCountNotif));
Text loginText = new Text(contact.getLogin() + unreadCountNotif);
loginText.setFont(Font.font(null, 12)); // FontWeight.BOLD, 14));
loginText.setFill(contact.isConnected() ? Color.BLACK : Color.GRAY);
Circle circle = new Circle(5, 5, 5);
circle.setFill(contact.isConnected() ? Color.CADETBLUE : Color.FIREBRICK);
// circle.setOpacity(contact.is_connected() ? 1 : 0.5);
Image avatar;
ImageView view = new ImageView();
if (contact.getAvatar() != null) {
avatar = SwingFXUtils.toFXImage((BufferedImage) contact.getAvatar(), null);
view = new ImageView(avatar);
}
view.setOpacity(contact.isConnected() ? 1 : 0.5);
view.setFitWidth(15);
view.setFitHeight(15);
HBox temp = new HBox(circle);
temp.setAlignment(Pos.CENTER_RIGHT);
HBox.setHgrow(temp, Priority.ALWAYS);
HBox hBox = new HBox(view, loginText, temp);
hBox.setSpacing(5.0);
hBox.setAlignment(Pos.CENTER_LEFT);
setGraphic(hBox);
}
}

View file

@ -1,18 +1,65 @@
package rtgre.modeles;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
public class Contact {
protected String login;
protected java.awt.Image avatar;
protected boolean connected;
protected String currentRoom;
Contact(String login, java.awt.Image avatar) {
public Contact(String login, java.awt.Image avatar) {
/**
* Crée un objet Contact
* @param: String login
* @param: java.awt.Image avatar
*/
this.login = login;
this.avatar = avatar;
this.connected = false;
this.currentRoom = null;
}
public Contact(String login, boolean connected, java.awt.Image avatar) {
/**
* Crée un objet Contact
* @param: String login
* @param: boolean connected
* @param: java.awt.Image avatar
*/
this.login = login;
this.avatar = avatar;
this.connected = connected;
this.currentRoom = null;
}
public String getCurrentRoom() {
return currentRoom;
}
public Contact(String login, boolean connected, File banques_avatars) {
/**
* Crée un objet Contact
* @param: String login
* @param: boolean connected
* @param: File banques_avatars
*/
this.login = login;
try {
this.avatar = avatarFromLogin(banques_avatars, login);
} catch (IOException e) {
System.out.println("Impossible de créer l'utilisateur " + login);
System.out.println(e.getMessage());
System.out.println(banques_avatars);
}
this.connected = connected;
this.currentRoom = null;
}
public String getLogin() {
return this.login;
@ -24,20 +71,52 @@ public class Contact {
@Override
public String toString() {
return "";
return "@" + this.login;
}
public boolean isConnected() {
return this.connected;
}
public void setConnected(boolean connected) {
this.connected = connected;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Contact contact = (Contact) o;
return Objects.equals(login, contact.login);
}
public boolean equals(Object o) {return true;}
/*if (this.login == o.login) {
return true;
} else {
return false;
@Override
public int hashCode() {
return Objects.hashCode(login);
}
public int getUnreadCount() {
return 0;
}
public static BufferedImage avatarFromLogin(File fichier, String login) throws IOException {
/**
* Renvoie une sous-image en fonction d'une banque d'image et d'un login.
* @param: File fichier
* @param: String login
*/
BufferedImage img = ImageIO.read(fichier);
int width = img.getWidth() / 9;
int height = img.getHeight();
int n = Integer.remainderUnsigned(login.hashCode(), 9);
return img.getSubimage(n*width, 0, width, height);
}
public void setAvatarFromFile(File f) {
try {
this.avatar = avatarFromLogin(f, this.login);
} catch (IOException e) {
System.out.println("Erreur : " + e.getMessage());
}
}*/
}
}

View file

@ -0,0 +1,12 @@
package rtgre.modeles;
import java.util.TreeMap;
public class ContactMap extends TreeMap<String, Contact> {
public void add(Contact contact) {
this.put(contact.login, contact);
}
public Contact getContact(String login) {
return this.get(login);
}
}