Move Envoy Client to client/ subdirectory
This commit is contained in:
570
client/src/main/java/envoy/client/ui/controller/ChatScene.java
Normal file
570
client/src/main/java/envoy/client/ui/controller/ChatScene.java
Normal file
@ -0,0 +1,570 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Random;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javafx.animation.RotateTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.data.audio.AudioRecorder;
|
||||
import envoy.client.event.MessageCreationEvent;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.net.WriteProxy;
|
||||
import envoy.client.ui.IconUtil;
|
||||
import envoy.client.ui.Restorable;
|
||||
import envoy.client.ui.SceneContext;
|
||||
import envoy.client.ui.listcell.ContactListCellFactory;
|
||||
import envoy.client.ui.listcell.MessageControl;
|
||||
import envoy.client.ui.listcell.MessageListCellFactory;
|
||||
import envoy.data.*;
|
||||
import envoy.data.Attachment.AttachmentType;
|
||||
import envoy.event.*;
|
||||
import envoy.event.contact.ContactOperation;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ChatSceneController.java</strong><br>
|
||||
* Created: <strong>26.03.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class ChatScene implements Restorable {
|
||||
|
||||
@FXML
|
||||
private GridPane scene;
|
||||
|
||||
@FXML
|
||||
private Label contactLabel;
|
||||
|
||||
@FXML
|
||||
private ListView<Message> messageList;
|
||||
|
||||
@FXML
|
||||
private ListView<Chat> chatList;
|
||||
|
||||
@FXML
|
||||
private Button postButton;
|
||||
|
||||
@FXML
|
||||
private Button voiceButton;
|
||||
|
||||
@FXML
|
||||
private Button attachmentButton;
|
||||
|
||||
@FXML
|
||||
private Button settingsButton;
|
||||
|
||||
@FXML
|
||||
private Button rotateButton;
|
||||
|
||||
@FXML
|
||||
private TextArea messageTextArea;
|
||||
|
||||
@FXML
|
||||
private Label remainingChars;
|
||||
|
||||
@FXML
|
||||
private Label infoLabel;
|
||||
|
||||
@FXML
|
||||
private MenuItem deleteContactMenuItem;
|
||||
|
||||
@FXML
|
||||
private ImageView attachmentView;
|
||||
|
||||
private LocalDB localDB;
|
||||
private Client client;
|
||||
private WriteProxy writeProxy;
|
||||
private SceneContext sceneContext;
|
||||
|
||||
private Chat currentChat;
|
||||
private AudioRecorder recorder;
|
||||
private boolean recording;
|
||||
private Attachment pendingAttachment;
|
||||
private boolean postingPermanentlyDisabled;
|
||||
|
||||
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 Image DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
|
||||
private static final int MAX_MESSAGE_LENGTH = 255;
|
||||
private static final int DEFAULT_ICON_SIZE = 16;
|
||||
|
||||
/**
|
||||
* Initializes the appearance of certain visual components.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void initialize() {
|
||||
|
||||
// Initialize message and user rendering
|
||||
messageList.setCellFactory(MessageListCellFactory::new);
|
||||
chatList.setCellFactory(ContactListCellFactory::new);
|
||||
|
||||
settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
|
||||
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
|
||||
attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
|
||||
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
|
||||
rotateButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("rotate", (int) (DEFAULT_ICON_SIZE * 1.5))));
|
||||
|
||||
// Listen to received messages
|
||||
eventBus.register(MessageCreationEvent.class, e -> {
|
||||
final var message = e.get();
|
||||
localDB.getChat(message instanceof GroupMessage ? message.getRecipientID() : message.getSenderID()).ifPresent(chat -> {
|
||||
chat.insert(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(); });
|
||||
} else chat.incrementUnreadAmount();
|
||||
// Moving chat with most recent unreadMessages to the top
|
||||
Platform.runLater(() -> {
|
||||
chatList.getItems().remove(chat);
|
||||
chatList.getItems().add(0, chat);
|
||||
if (chat.equals(currentChat)) chatList.getSelectionModel().select(0);
|
||||
localDB.getChats().remove(chat);
|
||||
localDB.getChats().add(0, chat);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 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);
|
||||
}));
|
||||
|
||||
eventBus.register(GroupMessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(groupMessage -> {
|
||||
((GroupMessage) groupMessage).getMemberStatuses().replace(e.getMemberID(), e.get());
|
||||
|
||||
// Update UI if in current chat
|
||||
if (currentChat != null && groupMessage.getRecipientID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh);
|
||||
}));
|
||||
|
||||
// Listen to user status changes
|
||||
eventBus.register(UserStatusChange.class,
|
||||
e -> chatList.getItems()
|
||||
.stream()
|
||||
.filter(c -> c.getRecipient().getID() == e.getID())
|
||||
.findAny()
|
||||
.map(Chat::getRecipient)
|
||||
.ifPresent(u -> { ((User) u).setStatus(e.get()); Platform.runLater(chatList::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);
|
||||
Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
|
||||
localDB.getChats().add(chat);
|
||||
Platform.runLater(() -> chatList.getItems().add(chat));
|
||||
break;
|
||||
case REMOVE:
|
||||
localDB.getUsers().remove(contact.getName());
|
||||
localDB.getChats().removeIf(c -> c.getRecipient().getID() == contact.getID());
|
||||
Platform.runLater(() -> chatList.getItems().removeIf(c -> c.getRecipient().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;
|
||||
|
||||
chatList.setItems(FXCollections.observableList(localDB.getChats()));
|
||||
contactLabel.setText(localDB.getUser().getName());
|
||||
MessageControl.setUser(localDB.getUser());
|
||||
if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
|
||||
|
||||
recorder = new AudioRecorder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestore() { updateRemainingCharsLabel(); }
|
||||
|
||||
/**
|
||||
* Actions to perform when the list of contacts has been clicked.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void chatListClicked() {
|
||||
final Contact user = chatList.getSelectionModel().getSelectedItem().getRecipient();
|
||||
if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) {
|
||||
|
||||
// LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes
|
||||
|
||||
// Load the chat
|
||||
currentChat = localDB.getChat(user.getID()).get();
|
||||
|
||||
messageList.setItems(FXCollections.observableList(currentChat.getMessages()));
|
||||
final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount() - 1;
|
||||
messageList.scrollTo(scrollIndex);
|
||||
logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex);
|
||||
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);
|
||||
}
|
||||
|
||||
// Discard the pending attachment
|
||||
if (recorder.isRecording()) {
|
||||
recorder.cancel();
|
||||
recording = false;
|
||||
}
|
||||
pendingAttachment = null;
|
||||
updateAttachmentView(false);
|
||||
|
||||
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);
|
||||
voiceButton.setDisable(!recorder.isSupported());
|
||||
attachmentButton.setDisable(false);
|
||||
chatList.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.<SettingsScene>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.<ContactSearchScene>getController().initializeData(sceneContext, localDB);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void voiceButtonClicked() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
if (!recording) {
|
||||
recording = true;
|
||||
Platform.runLater(() -> {
|
||||
voiceButton.setText("Recording");
|
||||
voiceButton.setGraphic(new ImageView(IconUtil.loadIcon("microphone_recording", DEFAULT_ICON_SIZE)));
|
||||
});
|
||||
recorder.start();
|
||||
} else {
|
||||
pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE);
|
||||
recording = false;
|
||||
Platform.runLater(() -> {
|
||||
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
|
||||
voiceButton.setText(null);
|
||||
checkPostConditions(false);
|
||||
updateAttachmentView(true);
|
||||
});
|
||||
}
|
||||
} catch (final EnvoyException e) {
|
||||
logger.log(Level.SEVERE, "Could not record audio: ", e);
|
||||
Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void attachmentButtonClicked() {
|
||||
|
||||
// Display file chooser
|
||||
final var fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Add Attachment");
|
||||
fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
|
||||
fileChooser.getExtensionFilters()
|
||||
.addAll(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"),
|
||||
new FileChooser.ExtensionFilter("Videos", "*.mp4"),
|
||||
new FileChooser.ExtensionFilter("All Files", "*.*"));
|
||||
final var file = fileChooser.showOpenDialog(sceneContext.getStage());
|
||||
|
||||
if (file != null) {
|
||||
|
||||
// Check max file size
|
||||
if (file.length() > 16E6) {
|
||||
new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 16MB!").showAndWait();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get attachment type (default is document)
|
||||
AttachmentType type = AttachmentType.DOCUMENT;
|
||||
switch (fileChooser.getSelectedExtensionFilter().getDescription()) {
|
||||
case "Pictures":
|
||||
type = AttachmentType.PICTURE;
|
||||
break;
|
||||
case "Videos":
|
||||
type = AttachmentType.VIDEO;
|
||||
break;
|
||||
}
|
||||
|
||||
// Create the pending attachment
|
||||
try {
|
||||
final var fileBytes = Files.readAllBytes(file.toPath());
|
||||
pendingAttachment = new Attachment(fileBytes, type);
|
||||
// Setting the preview image as image of the attachmentView
|
||||
if (type == AttachmentType.PICTURE)
|
||||
attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes), DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true));
|
||||
attachmentView.setVisible(true);
|
||||
} catch (final IOException e) {
|
||||
new Alert(AlertType.ERROR, "The selected file could not be loaded!").showAndWait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates every element in our application by 360° in at most 2.75s.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void doABarrelRoll() {
|
||||
// contains all Node objects in ChatScene in alphabetical order
|
||||
final var rotatableNodes = new Node[] { attachmentButton, attachmentView, contactLabel, infoLabel, messageList, messageTextArea,
|
||||
postButton, remainingChars, rotateButton, scene, settingsButton, userList, voiceButton };
|
||||
final var random = new Random();
|
||||
for (final var node : rotatableNodes) {
|
||||
// Defines at most four whole rotation in at most 4s
|
||||
final var rotateTransition = new RotateTransition(Duration.seconds(random.nextDouble() * 3 + 1), node);
|
||||
rotateTransition.setByAngle((random.nextInt(3) + 1) * 360);
|
||||
rotateTransition.play();
|
||||
// This is needed as for some strange reason objects could stop before being
|
||||
// rotated back to 0°
|
||||
rotateTransition.setOnFinished(e -> node.setRotate(0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
checkPostConditions(settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
|
||||
|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown());
|
||||
}
|
||||
|
||||
private void checkPostConditions(boolean sendKeyPressed) {
|
||||
if (!postingPermanentlyDisabled) {
|
||||
if (!postButton.isDisabled() && sendKeyPressed) postMessage();
|
||||
postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || 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, "infoLabel-error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {@link Message} or {@link GroupMessage} to the server based on
|
||||
* the text entered in the {@code messageTextArea} and the given attachment.
|
||||
*
|
||||
* @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", "infoLabel-error");
|
||||
return;
|
||||
}
|
||||
final var text = messageTextArea.getText().strip();
|
||||
try {
|
||||
// Creating the message and its metadata
|
||||
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
|
||||
.setText(text);
|
||||
// Setting an attachment, if present
|
||||
if (pendingAttachment != null) {
|
||||
builder.setAttachment(pendingAttachment);
|
||||
pendingAttachment = null;
|
||||
updateAttachmentView(false);
|
||||
}
|
||||
// Building the final message
|
||||
final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())
|
||||
: builder.build();
|
||||
|
||||
// Send message
|
||||
writeProxy.writeMessage(message);
|
||||
|
||||
// Add message to LocalDB and update UI
|
||||
currentChat.insert(message);
|
||||
// Moving currentChat to the top
|
||||
Platform.runLater(() -> {
|
||||
chatList.getItems().remove(currentChat);
|
||||
chatList.getItems().add(0, currentChat);
|
||||
chatList.getSelectionModel().select(0);
|
||||
localDB.getChats().remove(currentChat);
|
||||
localDB.getChats().add(0, currentChat);
|
||||
});
|
||||
messageList.refresh();
|
||||
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 infoLabelID the id the the {@code infoLabel} should have so that it
|
||||
* can be styled accordingly in CSS
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
private void updateInfoLabel(String text, String infoLabelID) {
|
||||
infoLabel.setText(text);
|
||||
infoLabel.setId(infoLabelID);
|
||||
infoLabel.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the {@code attachmentView} in terms of visibility.<br>
|
||||
* Additionally resets the shown image to
|
||||
* {@code DEFAULT_ATTACHMENT_VIEW_IMAGE} if another image is currently
|
||||
* present.
|
||||
*
|
||||
* @param visible whether the {@code attachmentView} should be displayed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
private void updateAttachmentView(boolean visible) {
|
||||
if (!attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE)) attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
|
||||
attachmentView.setVisible(visible);
|
||||
}
|
||||
|
||||
// Context menu actions
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.ListView;
|
||||
|
||||
import envoy.client.data.Chat;
|
||||
import envoy.client.data.LocalDB;
|
||||
import envoy.client.event.SendEvent;
|
||||
import envoy.client.ui.ClearableTextField;
|
||||
import envoy.client.ui.SceneContext;
|
||||
import envoy.client.ui.listcell.ContactListCellFactory;
|
||||
import envoy.event.ElementOperation;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.event.contact.ContactOperation;
|
||||
import envoy.event.contact.ContactSearchRequest;
|
||||
import envoy.event.contact.ContactSearchResult;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ContactSearchSceneController.java</strong><br>
|
||||
* Created: <strong>07.06.2020</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class ContactSearchScene {
|
||||
|
||||
@FXML
|
||||
private ClearableTextField searchBar;
|
||||
|
||||
@FXML
|
||||
private ListView<Chat> chatList;
|
||||
|
||||
private SceneContext sceneContext;
|
||||
|
||||
private LocalDB localDB;
|
||||
|
||||
private static EventBus eventBus = EventBus.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
|
||||
|
||||
/**
|
||||
* @param sceneContext enables the user to return to the chat scene
|
||||
* @param localDB the local database to which new contacts are added
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void initializeData(SceneContext sceneContext, LocalDB localDB) {
|
||||
this.sceneContext = sceneContext;
|
||||
this.localDB = localDB;
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
chatList.setCellFactory(ContactListCellFactory::new);
|
||||
searchBar.setClearButtonListener(e -> { searchBar.getTextField().clear(); chatList.getItems().clear(); });
|
||||
eventBus.register(ContactSearchResult.class,
|
||||
response -> Platform.runLater(() -> {
|
||||
chatList.getItems().clear();
|
||||
chatList.getItems().addAll(response.get().stream().map(Chat::new).collect(Collectors.toList()));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the clear and search button if no text is present in the search bar.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void sendRequest() {
|
||||
final var text = searchBar.getTextField().getText().strip();
|
||||
if (!text.isBlank()) eventBus.dispatch(new SendEvent(new ContactSearchRequest(text)));
|
||||
else chatList.getItems().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the text in the search bar and the items shown in the list.
|
||||
* Additionally disables both clear and search button.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void clear() {
|
||||
searchBar.getTextField().setText(null);
|
||||
chatList.getItems().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an {@link ContactOperation} for every selected contact to the
|
||||
* server.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void chatListClicked() {
|
||||
final var chat = chatList.getSelectionModel().getSelectedItem();
|
||||
if (chat != null) {
|
||||
final var alert = new Alert(AlertType.CONFIRMATION);
|
||||
alert.setTitle("Add Contact to Contact List");
|
||||
alert.setHeaderText("Add the user " + chat.getRecipient().getName() + " to your contact list?");
|
||||
// Normally, this would be total BS (we are already on the FX Thread), however
|
||||
// it could be proven that the creation of this dialog wrapped in
|
||||
// Platform.runLater is less error-prone than without it
|
||||
Platform.runLater(() -> alert.showAndWait().filter(btn -> btn == ButtonType.OK).ifPresent(btn -> {
|
||||
final var event = new ContactOperation(chat.getRecipient(), ElementOperation.ADD);
|
||||
// Sends the event to the server
|
||||
eventBus.dispatch(new SendEvent(event));
|
||||
// Updates the UI
|
||||
eventBus.dispatch(event);
|
||||
logger.log(Level.INFO, "Added contact " + chat.getRecipient());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void newGroupButtonClicked() {
|
||||
sceneContext.load(SceneContext.SceneInfo.GROUP_CREATION_SCENE);
|
||||
sceneContext.<GroupCreationScene>getController().initializeData(sceneContext, localDB);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void backButtonClicked() { sceneContext.pop(); }
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
|
||||
import envoy.client.data.Chat;
|
||||
import envoy.client.data.LocalDB;
|
||||
import envoy.client.event.SendEvent;
|
||||
import envoy.client.ui.ClearableTextField;
|
||||
import envoy.client.ui.SceneContext;
|
||||
import envoy.client.ui.listcell.ContactListCellFactory;
|
||||
import envoy.data.Group;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.event.GroupCreation;
|
||||
import envoy.util.Bounds;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ContactSearchSceneController.java</strong><br>
|
||||
* Created: <strong>07.06.2020</strong><br>
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class GroupCreationScene {
|
||||
|
||||
@FXML
|
||||
private Button createButton;
|
||||
|
||||
@FXML
|
||||
private ClearableTextField groupNameField;
|
||||
|
||||
@FXML
|
||||
private ListView<Chat> chatList;
|
||||
|
||||
private SceneContext sceneContext;
|
||||
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
chatList.setCellFactory(ContactListCellFactory::new);
|
||||
chatList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
groupNameField.setClearButtonListener(e -> { groupNameField.getTextField().clear(); createButton.setDisable(true); });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sceneContext enables the user to return to the chat scene
|
||||
* @param localDB the local database from which potential group members can
|
||||
* be selected
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void initializeData(SceneContext sceneContext, LocalDB localDB) {
|
||||
this.sceneContext = sceneContext;
|
||||
Platform.runLater(() -> chatList.getItems()
|
||||
.addAll(localDB.getChats()
|
||||
.stream()
|
||||
.filter(c -> !(c.getRecipient() instanceof Group))
|
||||
.filter(c -> c.getRecipient().getID() != localDB.getUser().getID())
|
||||
.collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the {@code createButton} if at least one contact is selected.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void chatListClicked() {
|
||||
createButton.setDisable(chatList.getSelectionModel().isEmpty() || groupNameField.getTextField().getText().isBlank());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, whether the {@code createButton} can be enabled because text is
|
||||
* present in the textfield.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void textUpdated() { createButton.setDisable(groupNameField.getTextField().getText().isBlank()); }
|
||||
|
||||
/**
|
||||
* Sends a {@link GroupCreation} to the server and closes this scene.
|
||||
* <p>
|
||||
* If the given group name is not valid, an error is displayed instead.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void createButtonClicked() {
|
||||
final var name = groupNameField.getTextField().getText();
|
||||
if (!Bounds.isValidContactName(name)) {
|
||||
new Alert(AlertType.ERROR, "The entered group name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
|
||||
groupNameField.getTextField().clear();
|
||||
} else {
|
||||
eventBus.dispatch(new SendEvent(new GroupCreation(name,
|
||||
chatList.getSelectionModel().getSelectedItems().stream().map(c -> c.getRecipient().getID()).collect(Collectors.toSet()))));
|
||||
new Alert(AlertType.INFORMATION, String.format("Group '%s' successfully created.", name)).showAndWait();
|
||||
sceneContext.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void backButtonClicked() { sceneContext.pop(); }
|
||||
}
|
199
client/src/main/java/envoy/client/ui/controller/LoginScene.java
Normal file
199
client/src/main/java/envoy/client/ui/controller/LoginScene.java
Normal file
@ -0,0 +1,199 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.net.WriteProxy;
|
||||
import envoy.client.ui.ClearableTextField;
|
||||
import envoy.client.ui.SceneContext;
|
||||
import envoy.client.ui.Startup;
|
||||
import envoy.data.LoginCredentials;
|
||||
import envoy.data.User;
|
||||
import envoy.data.User.UserStatus;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.event.HandshakeRejection;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.Bounds;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>LoginDialog.java</strong><br>
|
||||
* Created: <strong>03.04.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class LoginScene {
|
||||
|
||||
@FXML
|
||||
private ClearableTextField userTextField;
|
||||
|
||||
@FXML
|
||||
private PasswordField passwordField;
|
||||
|
||||
@FXML
|
||||
private PasswordField repeatPasswordField;
|
||||
|
||||
@FXML
|
||||
private Label repeatPasswordLabel;
|
||||
|
||||
@FXML
|
||||
private CheckBox registerCheckBox;
|
||||
|
||||
@FXML
|
||||
private Label connectionLabel;
|
||||
|
||||
private Client client;
|
||||
private LocalDB localDB;
|
||||
private CacheMap cacheMap;
|
||||
private SceneContext sceneContext;
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class);
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
private static final ClientConfig config = ClientConfig.getInstance();
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
connectionLabel.setText("Server: " + config.getServer() + ":" + config.getPort());
|
||||
|
||||
// Show an alert after an unsuccessful handshake
|
||||
eventBus.register(HandshakeRejection.class, e -> Platform.runLater(() -> { new Alert(AlertType.ERROR, e.get()).showAndWait(); }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the login dialog using the FXML file {@code LoginDialog.fxml}.
|
||||
*
|
||||
* @param client the client used to perform the handshake
|
||||
* @param localDB the local database used for offline login
|
||||
* @param cacheMap the map of all caches needed
|
||||
* @param sceneContext the scene context used to initialize the chat scene
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void initializeData(Client client, LocalDB localDB, CacheMap cacheMap, SceneContext sceneContext) {
|
||||
this.client = client;
|
||||
this.localDB = localDB;
|
||||
this.cacheMap = cacheMap;
|
||||
this.sceneContext = sceneContext;
|
||||
|
||||
// Prepare handshake
|
||||
localDB.loadIDGenerator();
|
||||
|
||||
// Set initial cursor
|
||||
userTextField.requestFocus();
|
||||
|
||||
// Perform automatic login if configured
|
||||
if (config.hasLoginCredentials()) performHandshake(config.getLoginCredentials());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void loginButtonPressed() {
|
||||
|
||||
// Prevent registration with unequal passwords
|
||||
if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) {
|
||||
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
|
||||
repeatPasswordField.clear();
|
||||
} else if (!Bounds.isValidContactName(userTextField.getTextField().getText())) {
|
||||
new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
|
||||
userTextField.getTextField().clear();
|
||||
} else performHandshake(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), registerCheckBox.isSelected(),
|
||||
Startup.VERSION));
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void offlineModeButtonPressed() {
|
||||
attemptOfflineMode(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), false, Startup.VERSION));
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void registerCheckboxChanged() {
|
||||
|
||||
// Make repeat password field and label visible / invisible
|
||||
repeatPasswordField.setVisible(registerCheckBox.isSelected());
|
||||
repeatPasswordLabel.setVisible(registerCheckBox.isSelected());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void abortLogin() {
|
||||
logger.log(Level.INFO, "The login process has been cancelled. Exiting...");
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private void performHandshake(LoginCredentials credentials) {
|
||||
try {
|
||||
client.performHandshake(credentials, cacheMap);
|
||||
if (client.isOnline()) {
|
||||
loadChatScene();
|
||||
client.initReceiver(localDB, cacheMap);
|
||||
}
|
||||
} catch (IOException | InterruptedException | TimeoutException e) {
|
||||
logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
|
||||
attemptOfflineMode(credentials);
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptOfflineMode(LoginCredentials credentials) {
|
||||
try {
|
||||
// Try entering offline mode
|
||||
localDB.loadUsers();
|
||||
final User clientUser = (User) localDB.getUsers().get(credentials.getIdentifier());
|
||||
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
|
||||
client.setSender(clientUser);
|
||||
loadChatScene();
|
||||
} catch (final Exception e) {
|
||||
new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
|
||||
logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadChatScene() {
|
||||
|
||||
// Set client user in local database
|
||||
localDB.setUser(client.getSender());
|
||||
|
||||
// Initialize chats in local database
|
||||
try {
|
||||
localDB.initializeUserStorage();
|
||||
localDB.loadUserData();
|
||||
} catch (final FileNotFoundException e) {
|
||||
// The local database file has not yet been created, probably first login
|
||||
} catch (final Exception e) {
|
||||
new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait();
|
||||
logger.log(Level.WARNING, "Could not load local database: ", e);
|
||||
}
|
||||
|
||||
// Initialize write proxy
|
||||
final var writeProxy = new WriteProxy(client, localDB);
|
||||
|
||||
localDB.synchronize();
|
||||
|
||||
if (client.isOnline()) writeProxy.flushCache();
|
||||
else
|
||||
// Set all contacts to offline mode
|
||||
localDB.getChats()
|
||||
.stream()
|
||||
.map(Chat::getRecipient)
|
||||
.filter(User.class::isInstance)
|
||||
.map(User.class::cast)
|
||||
.forEach(u -> u.setStatus(UserStatus.OFFLINE));
|
||||
|
||||
// Load ChatScene
|
||||
sceneContext.pop();
|
||||
sceneContext.getStage().setMinHeight(400);
|
||||
sceneContext.getStage().setMinWidth(350);
|
||||
sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE);
|
||||
sceneContext.<ChatScene>getController().initializeData(sceneContext, localDB, client, writeProxy);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import envoy.client.ui.SceneContext;
|
||||
import envoy.client.ui.settings.GeneralSettingsPane;
|
||||
import envoy.client.ui.settings.SettingsPane;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>SettingsSceneController.java</strong><br>
|
||||
* Created: <strong>10.04.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class SettingsScene {
|
||||
|
||||
@FXML
|
||||
private ListView<SettingsPane> settingsList;
|
||||
|
||||
@FXML
|
||||
private TitledPane titledPane;
|
||||
|
||||
private SceneContext sceneContext;
|
||||
|
||||
/**
|
||||
* @param sceneContext enables the user to return to the chat scene
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void initializeData(SceneContext sceneContext) { this.sceneContext = sceneContext; }
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
settingsList.setCellFactory(listView -> new ListCell<>() {
|
||||
|
||||
@Override
|
||||
protected void updateItem(SettingsPane item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (!empty && item != null) setGraphic(new Label(item.getTitle()));
|
||||
}
|
||||
});
|
||||
|
||||
settingsList.getItems().add(new GeneralSettingsPane());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void settingsListClicked() {
|
||||
final var pane = settingsList.getSelectionModel().getSelectedItem();
|
||||
if (pane != null) {
|
||||
titledPane.setText(pane.getTitle());
|
||||
titledPane.setContent(pane);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void backButtonClicked() { sceneContext.pop(); }
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Contains JavaFX scene controllers.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>package-info.java</strong><br>
|
||||
* Created: <strong>08.06.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
package envoy.client.ui.controller;
|
Reference in New Issue
Block a user