package envoy.client.ui.controller; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.control.Alert.AlertType; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.paint.Color; import envoy.client.data.Chat; import envoy.client.data.LocalDB; import envoy.client.data.Settings; import envoy.client.event.MessageCreationEvent; import envoy.client.net.Client; import envoy.client.net.WriteProxy; import envoy.client.ui.ContactListCell; import envoy.client.ui.MessageListCell; import envoy.client.ui.SceneContext; import envoy.data.*; import envoy.event.EventBus; import envoy.event.MessageStatusChangeEvent; import envoy.event.UserStatusChangeEvent; import envoy.event.contact.ContactOperationEvent; import envoy.util.EnvoyLog; /** * Project: envoy-client
* File: ChatSceneController.java
* Created: 26.03.2020
* * @author Kai S. K. Engelbart * @since Envoy Client v0.1-beta */ public final class ChatScene { @FXML private Label contactLabel; @FXML private ListView messageList; @FXML private ListView userList; @FXML private Button postButton; @FXML private Button settingsButton; @FXML private TextArea messageTextArea; @FXML private Label remainingChars; private LocalDB localDB; private Client client; private WriteProxy writeProxy; private SceneContext sceneContext; private Chat currentChat; private static final Settings settings = Settings.getInstance(); private static final EventBus eventBus = EventBus.getInstance(); private static final Logger logger = EnvoyLog.getLogger(ChatScene.class); private static final int MAX_MESSAGE_LENGTH = 255; /** * Initializes the appearance of certain visual components. * * @since Envoy Client v0.1-beta */ @FXML private void initialize() { // Initialize message and user rendering messageList.setCellFactory(listView -> new MessageListCell()); userList.setCellFactory(listView -> new ContactListCell()); // Listen to received messages eventBus.register(MessageCreationEvent.class, e -> { final var message = e.get(); localDB.getChat(message.getSenderID()).ifPresent(chat -> { chat.getMessages().add(message); if (chat.equals(currentChat)) { try { currentChat.read(writeProxy); } catch (final IOException e1) { logger.log(Level.WARNING, "Could not read current chat: ", e1); } Platform.runLater(messageList::refresh); } }); }); // Listen to message status changes eventBus.register(MessageStatusChangeEvent.class, e -> localDB.getMessage(e.getID()).ifPresent(message -> { message.setStatus(e.get()); // Update UI if in current chat if (currentChat != null && message.getSenderID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh); })); // Listen to user status changes eventBus.register(UserStatusChangeEvent.class, e -> userList.getItems() .stream() .filter(c -> c.getID() == e.getID()) .findAny() .ifPresent(u -> { ((User) u).setStatus(e.get()); Platform.runLater(userList::refresh); })); // Listen to contacts changes eventBus.register(ContactOperationEvent.class, e -> { final var contact = e.get(); switch (e.getOperationType()) { case ADD: localDB.getUsers().put(contact.getName(), contact); localDB.getChats().add(new Chat(contact)); Platform.runLater(() -> userList.getItems().add(contact)); break; case REMOVE: localDB.getUsers().remove(contact.getName()); localDB.getChats().removeIf(c -> c.getRecipient().getID() == contact.getID()); Platform.runLater(() -> userList.getItems().removeIf(c -> c.getID() == contact.getID())); break; } }); } /** * Initializes all necessary data via dependency injection- * * @param sceneContext the scene context used to load other scenes * @param localDB the local database form which chats and users are loaded * @param client the client used to request ID generators * @param writeProxy the write proxy used to send messages and other data to * the server * @since Envoy Client v0.1-beta */ public void initializeData(SceneContext sceneContext, LocalDB localDB, Client client, WriteProxy writeProxy) { this.sceneContext = sceneContext; this.localDB = localDB; this.client = client; this.writeProxy = writeProxy; userList.setItems(FXCollections.observableList(localDB.getChats().stream().map(Chat::getRecipient).collect(Collectors.toList()))); } /** * Actions to perform when the list of contacts has been clicked. * * @since Envoy Client v0.1-beta */ @FXML private void userListClicked() { final Contact user = userList.getSelectionModel().getSelectedItem(); if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) { contactLabel.setText(user.getName()); // LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes // Load the chat or create a new one and add it to the LocalDB currentChat = localDB.getChat(user.getID()) .orElseGet(() -> { final var chat = new Chat(user); localDB.getChats().add(chat); return chat; }); messageList.setItems(FXCollections.observableList(currentChat.getMessages())); // Read the current chat try { currentChat.read(writeProxy); } catch (final IOException e) { logger.log(Level.WARNING, "Could not read current chat.", e); } remainingChars.setVisible(true); remainingChars .setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH)); } messageTextArea.setDisable(currentChat == null); } /** * Actions to perform when the Settings Button has been clicked. * * @since Envoy Client v0.1-beta */ @FXML private void settingsButtonClicked() { sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE); sceneContext.getController().initializeData(sceneContext); } /** * Actions to perform when the "Add Contact" - Button has been clicked. * * @since Envoy Client v0.1-beta */ @FXML private void addContactButtonClicked() { sceneContext.load(SceneContext.SceneInfo.CONTACT_SEARCH_SCENE); sceneContext.getController().initializeData(sceneContext, localDB); } /** * Checks the text length of the {@code messageTextArea}, adjusts the * {@code remainingChars} label and checks whether to send the message * automatically. * * @param e the key event that will be analyzed for a post request * @since Envoy Client v0.1-beta */ @FXML private void checkKeyCombination(KeyEvent e) { // Checks whether the text is too long messageTextUpdated(); // Automatic sending of messages via (ctrl +) enter checkPostConditions(e); } /** * @param e the keys that have been pressed * @since Envoy Client v0.1-beta */ @FXML private void checkPostConditions(KeyEvent e) { if (!postButton.isDisabled() && (settings.isEnterToSend() && e.getCode() == KeyCode.ENTER || !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown())) postMessage(); postButton.setDisable(messageTextArea.getText().isBlank() || currentChat == null); } /** * Actions to perform when the text was updated in the messageTextArea. * * @since Envoy Client v0.1-beta */ @FXML private void messageTextUpdated() { // Truncating messages that are too long and staying at the same position if (messageTextArea.getText().length() >= MAX_MESSAGE_LENGTH) { messageTextArea.setText(messageTextArea.getText().substring(0, MAX_MESSAGE_LENGTH)); messageTextArea.positionCaret(MAX_MESSAGE_LENGTH); messageTextArea.setScrollTop(Double.MAX_VALUE); } updateRemainingCharsLabel(); } /** * Sets the text and text color of the {@code remainingChars} label. * * @since Envoy Client v0.1-beta */ private void updateRemainingCharsLabel() { final int currentLength = messageTextArea.getText().length(); final int remainingLength = MAX_MESSAGE_LENGTH - currentLength; remainingChars.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH)); remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1)); } /** * Sends a new message to the server based on the text entered in the * messageTextArea. * * @since Envoy Client v0.1-beta */ @FXML private void postMessage() { final var text = messageTextArea.getText().strip(); if (text.isBlank()) throw new IllegalArgumentException("A message without visible text can not be sent."); try { // Create and send message final var message = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) .setText(text) .build(); // Send message writeProxy.writeMessage(message); // Add message to LocalDB and update UI messageList.getItems().add(message); // Request a new ID generator if all IDs were used if (!localDB.getIDGenerator().hasNext() && client.isOnline()) client.requestIdGenerator(); } catch (final IOException e) { logger.log(Level.SEVERE, "Error while sending message: ", e); new Alert(AlertType.ERROR, "An error occured while sending the message!").showAndWait(); } // Clear text field and disable post button messageTextArea.setText(""); postButton.setDisable(true); updateRemainingCharsLabel(); } }