package envoy.client.ui.controller; import java.awt.Toolkit; import java.awt.datatransfer.StringSelection; 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.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; 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.IconUtil; import envoy.client.ui.SceneContext; import envoy.client.ui.listcell.ContactListCell; import envoy.client.ui.listcell.MessageListCell; import envoy.data.*; import envoy.event.EventBus; import envoy.event.MessageStatusChange; import envoy.event.UserStatusChange; import envoy.event.contact.ContactOperation; 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; @FXML private Label infoLabel; @FXML private MenuItem deleteContactMenuItem; private LocalDB localDB; private Client client; private WriteProxy writeProxy; private SceneContext sceneContext; private boolean postingPermanentlyDisabled = false; 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(MessageListCell::new); userList.setCellFactory(ContactListCell::new); settingsButton.setGraphic(new ImageView(IconUtil.load("/icons/settings.png", 16))); // 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(); scrollToMessageListEnd(); }); } }); }); // Listen to message status changes eventBus.register(MessageStatusChange.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(UserStatusChange.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(ContactOperation.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()))); contactLabel.setText(localDB.getUser().getName()); MessageListCell.setUser(localDB.getUser()); if (!client.isOnline()) updateInfoLabel("You are offline", Color.YELLOW); } /** * 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()))) { logger.log(Level.FINEST, "Loading chat with " + user); // LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes // Load the chat currentChat = localDB.getChat(user.getID()).get(); messageList.setItems(FXCollections.observableList(currentChat.getMessages())); deleteContactMenuItem.setText("Delete " + user.getName()); // 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 || postingPermanentlyDisabled); } /** * 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 (!postingPermanentlyDisabled) { 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); } else { final var noMoreMessaging = "Go online to send messages"; if (!infoLabel.getText().equals(noMoreMessaging)) // Informing the user that he is a f*cking moron and should use Envoy online // because he ran out of messageIDs to use updateInfoLabel(noMoreMessaging, Color.RED); } } /** * 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() { postingPermanentlyDisabled = !(client.isOnline() || localDB.getIDGenerator().hasNext()); if (postingPermanentlyDisabled) { postButton.setDisable(true); messageTextArea.setDisable(true); messageTextArea.clear(); updateInfoLabel("You need to go online to send more messages", Color.RED); return; } 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); scrollToMessageListEnd(); // 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(); } /** * Scrolls to the bottom of the {@code messageList}. * * @since Envoy Client v0.1-beta */ private void scrollToMessageListEnd() { messageList.scrollTo(messageList.getItems().size() - 1); } /** * Updates the {@code infoLabel}. * * @param text the text to use * @param textfill the color in which to display information * @since Envoy Client v0.1-beta */ private void updateInfoLabel(String text, Paint textfill) { infoLabel.setText(text); infoLabel.setTextFill(textfill); infoLabel.setVisible(true); } // Context menu actions @FXML private void copyMessage() { try { Toolkit.getDefaultToolkit() .getSystemClipboard() .setContents(new StringSelection(messageList.getSelectionModel().getSelectedItem().getText()), null); } catch (final NullPointerException e) {} } @FXML private void deleteMessage() { try {} catch (final NullPointerException e) {} } @FXML private void forwardMessage() { try {} catch (final NullPointerException e) {} } @FXML private void quoteMessage() { try {} catch (final NullPointerException e) {} } @FXML private void deleteContact() { try {} catch (final NullPointerException e) {} } @FXML private void copyAndPostMessage() { final var messageText = messageTextArea.getText(); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null); postMessage(); messageTextArea.setText(messageText); updateRemainingCharsLabel(); postButton.setDisable(messageText.isBlank()); } @FXML private void loadMessageInfoScene() { try {} catch (final NullPointerException e) {} } }