diff --git a/.github/PULL_REQUEST_TEMPLATE/bugfix.md b/.github/PULL_REQUEST_TEMPLATE/bugfix.md index e189f31..2a918ad 100644 --- a/.github/PULL_REQUEST_TEMPLATE/bugfix.md +++ b/.github/PULL_REQUEST_TEMPLATE/bugfix.md @@ -5,6 +5,7 @@ labels: bug assignees: CyB3RC0nN0R, delvh, DieGurke reviewers: CyB3RC0nN0R, delvh projects: Envoy -milestone: Envoy v0.3-alpha +milestone: Envoy v0.1-beta + --- Fixes #{issue} diff --git a/.github/PULL_REQUEST_TEMPLATE/feature_integration.md b/.github/PULL_REQUEST_TEMPLATE/feature_integration.md index 945e7d3..7d5e167 100644 --- a/.github/PULL_REQUEST_TEMPLATE/feature_integration.md +++ b/.github/PULL_REQUEST_TEMPLATE/feature_integration.md @@ -1,9 +1,10 @@ --- name: Feature integration title: Added feature -labels: enhancement +labels: feature assignees: CyB3RC0nN0R, delvh, DieGurke reviewers: CyB3RC0nN0R, delvh projects: Envoy -milestone: Envoy v0.3-alpha +milestone: Envoy v0.1-beta + --- diff --git a/.github/PULL_REQUEST_TEMPLATE/javadoc_upgrade.md b/.github/PULL_REQUEST_TEMPLATE/javadoc_update.md similarity index 84% rename from .github/PULL_REQUEST_TEMPLATE/javadoc_upgrade.md rename to .github/PULL_REQUEST_TEMPLATE/javadoc_update.md index 7f1786d..68b0afd 100644 --- a/.github/PULL_REQUEST_TEMPLATE/javadoc_upgrade.md +++ b/.github/PULL_REQUEST_TEMPLATE/javadoc_update.md @@ -5,5 +5,6 @@ labels: documentation assignees: CyB3RC0nN0R, delvh reviewers: CyB3RC0nN0R, delvh projects: Envoy -milestone: Envoy v0.3-alpha +milestone: Envoy v0.1-beta + --- diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index da5f736..e9f97db 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -4,14 +4,25 @@ on: [push] jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Set up JDK 11 uses: actions/setup-java@v1 with: java-version: 11 + - name: Cache Maven packages + uses: actions/cache@v2 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 - name: Build with Maven run: mvn -B package --file pom.xml + - name: Stage build artifacts + run: mkdir staging && cp target/*.jar staging + - uses: actions/upload-artifact@v1 + with: + name: envoy-client-artifacts + path: staging diff --git a/src/main/java/envoy/client/data/Chat.java b/src/main/java/envoy/client/data/Chat.java index 207fb1f..318cf24 100644 --- a/src/main/java/envoy/client/data/Chat.java +++ b/src/main/java/envoy/client/data/Chat.java @@ -114,6 +114,21 @@ public final class Chat implements Serializable { */ public boolean isUnread() { return !messages.isEmpty() && messages.get(messages.size() - 1).getStatus() != MessageStatus.READ; } + /** + * Inserts a message at the correct place according to its creation date. + * + * @param message the message to insert + * @since Envoy Client v0.1-beta + */ + public void insert(Message message) { + for (int i = messages.size() - 1; i >= 0; --i) + if (message.getCreationDate().isAfter(messages.get(i).getCreationDate())) { + messages.add(i + 1, message); + return; + } + messages.add(0, message); + } + /** * @return all messages in the current chat * @since Envoy Client v0.1-beta diff --git a/src/main/java/envoy/client/ui/ClearableTextField.java b/src/main/java/envoy/client/ui/ClearableTextField.java new file mode 100644 index 0000000..f19155b --- /dev/null +++ b/src/main/java/envoy/client/ui/ClearableTextField.java @@ -0,0 +1,174 @@ +package envoy.client.ui; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.StringProperty; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.*; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Background; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; + +import envoy.client.data.Settings; + +/** + * This class offers a text field that is automatically equipped with a clear + * button. + *

+ * Project: envoy-client
+ * File: ClearableTextField.java
+ * Created: 25.06.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.1-beta + */ +public class ClearableTextField extends GridPane { + + private final TextField textField; + + private final Button clearButton; + + /** + * Constructs a new {@code ClearableTextField} with no initial text and icon + * size 16. + * + * @since Envoy Client v0.1-beta + */ + public ClearableTextField() { this("", 16); } + + /** + * Constructs a new {@code ClearableTextField} with initial text and a + * predetermined icon size. + * + * @param text the text that should be displayed by default + * @param size the size of the icon + * @since Envoy Client v0.1-beta + */ + public ClearableTextField(String text, int size) { + // initializing the textField and the button + textField = new TextField(text); + clearButton = new Button("", + new ImageView(IconUtil.load( + Settings.getInstance().getCurrentTheme().equals("dark") ? "/icons/clear_button_white.png" : "/icons/clear_button_black.png", + size))); + clearButton.setOnAction(e -> textField.clear()); + clearButton.setFocusTraversable(false); + clearButton.getStyleClass().clear(); + clearButton.setBackground(Background.EMPTY); + // Adding the two elements to the GridPane + add(textField, 0, 0, 2, 1); + add(clearButton, 1, 0, 1, 1); + // Setting the percent - widths of the two columns. + // Used to locate the button on the right. + final var columnConstraints = new ColumnConstraints(); + columnConstraints.setPercentWidth(90); + getColumnConstraints().add(columnConstraints); + final var columnConstraints2 = new ColumnConstraints(); + columnConstraints2.setPercentWidth(10); + getColumnConstraints().add(columnConstraints2); + } + + /** + * @return the underlying {@code textField} + * @since Envoy Client v0.1-beta + */ + public TextField getTextField() { return textField; } + + /** + * This method offers the freedom to perform custom actions when the + * {@code clearButton} has been pressed. + *

+ * The default is + * e -> {clearableTextField.getTextField().clear();} + * + * @param onClearButtonAction the action that should be performed + * @since Envoy Client v0.1-beta + */ + public void setClearButtonListener(EventHandler onClearButtonAction) { clearButton.setOnAction(onClearButtonAction); } + + /** + * @return the current property of the prompt text + * @see javafx.scene.control.TextInputControl#promptTextProperty() + * @since Envoy Client v0.1-beta + */ + public final StringProperty promptTextProperty() { return textField.promptTextProperty(); } + + /** + * @return the current prompt text + * @see javafx.scene.control.TextInputControl#getPromptText() + * @since Envoy Client v0.1-beta + */ + public final String getPromptText() { return textField.getPromptText(); } + + /** + * @param value the prompt text to display + * @see javafx.scene.control.TextInputControl#setPromptText(java.lang.String) + * @since Envoy Client v0.1-beta + */ + public final void setPromptText(String value) { textField.setPromptText(value); } + + /** + * @return the current property of the tooltip + * @see javafx.scene.control.Control#tooltipProperty() + * @since Envoy Client v0.1-beta + */ + public final ObjectProperty tooltipProperty() { return textField.tooltipProperty(); } + + /** + * @param value the new tooltip + * @see javafx.scene.control.Control#setTooltip(javafx.scene.control.Tooltip) + * @since Envoy Client v0.1-beta + */ + public final void setTooltip(Tooltip value) { textField.setTooltip(value); } + + /** + * @return the current tooltip + * @see javafx.scene.control.Control#getTooltip() + * @since Envoy Client v0.1-beta + */ + public final Tooltip getTooltip() { return textField.getTooltip(); } + + /** + * @return the current property of the context menu + * @see javafx.scene.control.Control#contextMenuProperty() + * @since Envoy Client v0.1-beta + */ + public final ObjectProperty contextMenuProperty() { return textField.contextMenuProperty(); } + + /** + * @param value the new context menu + * @see javafx.scene.control.Control#setContextMenu(javafx.scene.control.ContextMenu) + * @since Envoy Client v0.1-beta + */ + public final void setContextMenu(ContextMenu value) { textField.setContextMenu(value); } + + /** + * @return the current context menu + * @see javafx.scene.control.Control#getContextMenu() + * @since Envoy Client v0.1-beta + */ + public final ContextMenu getContextMenu() { return textField.getContextMenu(); } + + /** + * @param value whether this ClearableTextField should be editable + * @see javafx.scene.control.TextInputControl#setEditable(boolean) + * @since Envoy Client v0.1-beta + */ + public final void setEditable(boolean value) { textField.setEditable(value); } + + /** + * @return the current property whether this ClearableTextField is editable + * @see javafx.scene.control.TextInputControl#editableProperty() + * @since Envoy Client v0.1-beta + */ + public final BooleanProperty editableProperty() { return textField.editableProperty(); } + + /** + * @return whether this {@code ClearableTextField} is editable + * @see javafx.scene.control.TextInputControl#isEditable() + * @since Envoy Client v0.1-beta + */ + public final boolean isEditable() { return textField.isEditable(); } +} diff --git a/src/main/java/envoy/client/ui/ContactListCell.java b/src/main/java/envoy/client/ui/ContactListCell.java deleted file mode 100644 index 283a89a..0000000 --- a/src/main/java/envoy/client/ui/ContactListCell.java +++ /dev/null @@ -1,49 +0,0 @@ -package envoy.client.ui; - -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.layout.VBox; - -import envoy.data.Contact; -import envoy.data.Group; -import envoy.data.User; - -/** - * Project: envoy-client
- * File: UserListCell.java
- * Created: 28.03.2020
- * - * @author Kai S. K. Engelbart - * @since Envoy Client v0.1-beta - */ -public class ContactListCell extends ListCell { - - /** - * Displays the name of a contact. If the contact is a user, their online status - * is displayed as well. - * - * @since Envoy Client v0.1-beta - */ - @Override - protected void updateItem(Contact contact, boolean empty) { - super.updateItem(contact, empty); - if (empty || contact == null) { - setText(null); - setGraphic(null); - } else { - // Container with contact name - final var vbox = new VBox(new Label(contact.getName())); - if (contact instanceof User) { - // Online status - final var user = (User) contact; - final var statusLabel = new Label(user.getStatus().toString()); - statusLabel.getStyleClass().add(user.getStatus().toString().toLowerCase()); - vbox.getChildren().add(statusLabel); - } else { - // Member count - vbox.getChildren().add(new Label(((Group) contact).getContacts().size() + " members")); - } - setGraphic(vbox); - } - } -} diff --git a/src/main/java/envoy/client/ui/MessageListCell.java b/src/main/java/envoy/client/ui/MessageListCell.java deleted file mode 100644 index 21a094e..0000000 --- a/src/main/java/envoy/client/ui/MessageListCell.java +++ /dev/null @@ -1,60 +0,0 @@ -package envoy.client.ui; - -import java.time.format.DateTimeFormatter; -import java.util.Map; - -import javafx.geometry.Insets; -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.VBox; - -import envoy.data.Message; -import envoy.data.Message.MessageStatus; -import envoy.data.User; - -/** - * Displays a single message inside the message list. - *

- * Project: envoy-client
- * File: MessageListCell.java
- * Created: 28.03.2020
- * - * @author Kai S. K. Engelbart - * @since Envoy Client v0.1-beta - */ -public class MessageListCell extends ListCell { - - private static User client; - private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); - private static final Map statusImages = IconUtil.loadByEnum(MessageStatus.class, 16); - - /** - * Displays the text, the data of creation and the status of a message. - * - * @since Envoy v0.1-beta - */ - @Override - protected void updateItem(Message message, boolean empty) { - super.updateItem(message, empty); - if (empty || message == null) { - setText(null); - setGraphic(null); - } else { - final var cell = new VBox(new Label(dateFormat.format(message.getCreationDate())), new Label(message.getText())); - if (message.getRecipientID() == client.getID()) { - cell.getChildren().add(new Label("", new ImageView(statusImages.get(message.getStatus())))); - cell.getStyleClass().add("own-message"); - } else cell.getStyleClass().add("received-message"); - cell.paddingProperty().setValue(new Insets(5, 20, 5, 20)); - setGraphic(cell); - } - } - - /** - * @param client the user who chats with another person - * @since Envoy Client v0.1-beta - */ - public static void setUser(User client) { MessageListCell.client = client; } -} diff --git a/src/main/java/envoy/client/ui/Restorable.java b/src/main/java/envoy/client/ui/Restorable.java new file mode 100644 index 0000000..e9e40eb --- /dev/null +++ b/src/main/java/envoy/client/ui/Restorable.java @@ -0,0 +1,25 @@ +package envoy.client.ui; + +/** + * This interface defines an action that should be performed when a scene gets + * restored from the scene stack in {@link SceneContext}. + *

+ * Project: envoy-client
+ * File: Restorable.java
+ * Created: 03.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.1-beta + */ +@FunctionalInterface +public interface Restorable { + + /** + * This method is getting called when a scene gets restored.
+ * Hence, it can contain anything that should be done when the underlying scene + * gets restored. + * + * @since Envoy Client v0.1-beta + */ + void onRestore(); +} diff --git a/src/main/java/envoy/client/ui/SceneContext.java b/src/main/java/envoy/client/ui/SceneContext.java index 52c847b..a027395 100644 --- a/src/main/java/envoy/client/ui/SceneContext.java +++ b/src/main/java/envoy/client/ui/SceneContext.java @@ -40,21 +40,21 @@ public final class SceneContext { public enum SceneInfo { /** - * The main scene in which chats are displayed. + * The main scene in which the chat screen is displayed. * * @since Envoy Client v0.1-beta */ CHAT_SCENE("/fxml/ChatScene.fxml"), /** - * The scene in which settings are displayed. + * The scene in which the settings screen is displayed. * * @since Envoy Client v0.1-beta */ SETTINGS_SCENE("/fxml/SettingsScene.fxml"), /** - * The scene in which the contact search is displayed. + * The scene in which the contact search screen is displayed. * * @since Envoy Client v0.1-beta */ @@ -72,7 +72,14 @@ public final class SceneContext { * * @since Envoy Client v0.1-beta */ - LOGIN_SCENE("/fxml/LoginScene.fxml"); + LOGIN_SCENE("/fxml/LoginScene.fxml"), + + /** + * The scene in which the info screen is displayed. + * + * @since Envoy Client v0.1-beta + */ + MESSAGE_INFO_SCENE("/fxml/MessageInfoScene.fxml"); /** * The path to the FXML resource. @@ -83,8 +90,9 @@ public final class SceneContext { } private final Stage stage; - private final FXMLLoader loader = new FXMLLoader(); - private final Stack sceneStack = new Stack<>(); + private final FXMLLoader loader = new FXMLLoader(); + private final Stack sceneStack = new Stack<>(); + private final Stack controllerStack = new Stack<>(); private static final Settings settings = Settings.getInstance(); @@ -113,6 +121,7 @@ public final class SceneContext { try { final var rootNode = (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path)); final var scene = new Scene(rootNode); + controllerStack.push(loader.getController()); sceneStack.push(scene); stage.setScene(scene); @@ -132,10 +141,16 @@ public final class SceneContext { */ public void pop() { sceneStack.pop(); + controllerStack.pop(); if (!sceneStack.isEmpty()) { - stage.setScene(sceneStack.peek()); + final var newScene = sceneStack.peek(); + stage.setScene(newScene); applyCSS(); stage.sizeToScene(); + // If the controller implements the Restorable interface, + // the actions to perform on restoration will be executed here + final var controller = controllerStack.peek(); + if (controller instanceof Restorable) ((Restorable) controller).onRestore(); } stage.show(); } @@ -154,7 +169,7 @@ public final class SceneContext { * @return the controller used by the current scene * @since Envoy Client v0.1-beta */ - public T getController() { return loader.getController(); } + public T getController() { return (T) controllerStack.peek(); } /** * @return the stage in which the scenes are displayed diff --git a/src/main/java/envoy/client/ui/controller/ChatScene.java b/src/main/java/envoy/client/ui/controller/ChatScene.java index f5f454b..0705b0f 100644 --- a/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -16,7 +16,6 @@ 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; @@ -24,7 +23,12 @@ import envoy.client.data.Settings; import envoy.client.event.MessageCreationEvent; import envoy.client.net.Client; import envoy.client.net.WriteProxy; -import envoy.client.ui.*; +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.event.*; import envoy.event.contact.ContactOperation; @@ -38,7 +42,7 @@ import envoy.util.EnvoyLog; * @author Kai S. K. Engelbart * @since Envoy Client v0.1-beta */ -public final class ChatScene { +public final class ChatScene implements Restorable { @FXML private Label contactLabel; @@ -90,26 +94,16 @@ public final class ChatScene { private void initialize() { // Initialize message and user rendering - messageList.setCellFactory(listView -> new MessageListCell()); - userList.setCellFactory(listView -> new ContactListCell()); + messageList.setCellFactory(MessageListCellFactory::new); + userList.setCellFactory(ContactListCellFactory::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(); - if (message.getClass().equals(Message.class)) { - localDB.getChat(message.getSenderID()).ifPresent(chat -> { - chat.getMessages().add(message); - - // Update UI if in current chat - if (chat == currentChat) - Platform.runLater(messageList::refresh); - }); - } else { - localDB.getChat(message.getRecipientID()).ifPresent(chat -> { - chat.getMessages().add(message); - + localDB.getChat(message instanceof GroupMessage ? message.getSenderID() : message.getRecipientID()).ifPresent(chat -> { + chat.insert(message); if (chat.equals(currentChat)) { try { currentChat.read(writeProxy); @@ -118,8 +112,7 @@ public final class ChatScene { } Platform.runLater(() -> { messageList.refresh(); scrollToMessageListEnd(); }); } - }); - } + }); }); // Listen to message status changes @@ -128,7 +121,7 @@ public final class ChatScene { // 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()); @@ -180,10 +173,13 @@ public final class ChatScene { 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); + MessageControl.setUser(localDB.getUser()); + if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info"); } + @Override + public void onRestore() { updateRemainingCharsLabel(); } + /** * Actions to perform when the list of contacts has been clicked. * @@ -193,7 +189,6 @@ public final class ChatScene { 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 @@ -201,6 +196,9 @@ public final class ChatScene { currentChat = localDB.getChat(user.getID()).get(); messageList.setItems(FXCollections.observableList(currentChat.getMessages())); + final var scrollIndex = messageList.getItems().size() - 1; + messageList.scrollTo(scrollIndex); + logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex); deleteContactMenuItem.setText("Delete " + user.getName()); // Read the current chat @@ -271,7 +269,7 @@ public final class ChatScene { 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); + updateInfoLabel(noMoreMessaging, "infoLabel-error"); } } @@ -317,35 +315,24 @@ public final class ChatScene { postButton.setDisable(true); messageTextArea.setDisable(true); messageTextArea.clear(); - updateInfoLabel("You need to go online to send more messages", Color.RED); + updateInfoLabel("You need to go online to send more messages", "infoLabel-error"); return; } final var text = messageTextArea.getText().strip(); if (text.isBlank()) throw new IllegalArgumentException("A message without visible text can not be sent."); try { - if (currentChat.getRecipient().getClass().equals(Group.class)) { - // Create and send groupMessage - final var groupMessage = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) - .setText(messageTextArea.getText().strip()) - .buildGroupMessage((Group) currentChat.getRecipient()); + // Create and send message + final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) + .setText(text); + final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient()) + : builder.build(); - // Send groupMessage - writeProxy.writeMessage(groupMessage); + // Send message + writeProxy.writeMessage(message); - // Add message to LocalDB and update UI - messageList.getItems().add(groupMessage); - } else { - // Create and send message - final var message = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) - .setText(messageTextArea.getText().strip()) - .build(); - - // Send message - writeProxy.writeMessage(message); - - // Add message to LocalDB and update UI - messageList.getItems().add(message); - } + // Add message to LocalDB and update UI + currentChat.insert(message); + messageList.refresh(); scrollToMessageListEnd(); // Request a new ID generator if all IDs were used @@ -372,13 +359,14 @@ public final class ChatScene { /** * Updates the {@code infoLabel}. * - * @param text the text to use - * @param textfill the color in which to display information + * @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, Paint textfill) { + private void updateInfoLabel(String text, String infoLabelID) { infoLabel.setText(text); - infoLabel.setTextFill(textfill); + infoLabel.setId(infoLabelID); infoLabel.setVisible(true); } @@ -414,4 +402,7 @@ public final class ChatScene { updateRemainingCharsLabel(); postButton.setDisable(messageText.isBlank()); } + + @FXML + private void loadMessageInfoScene() { try {} catch (final NullPointerException e) {} } } diff --git a/src/main/java/envoy/client/ui/controller/ContactSearchScene.java b/src/main/java/envoy/client/ui/controller/ContactSearchScene.java index 54c505c..4fe48d2 100644 --- a/src/main/java/envoy/client/ui/controller/ContactSearchScene.java +++ b/src/main/java/envoy/client/ui/controller/ContactSearchScene.java @@ -5,13 +5,16 @@ import java.util.logging.Logger; import javafx.application.Platform; import javafx.fxml.FXML; -import javafx.scene.control.*; +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.LocalDB; import envoy.client.event.SendEvent; -import envoy.client.ui.ContactListCell; +import envoy.client.ui.ClearableTextField; import envoy.client.ui.SceneContext; +import envoy.client.ui.listcell.ContactListCellFactory; import envoy.data.Contact; import envoy.event.ElementOperation; import envoy.event.EventBus; @@ -31,19 +34,7 @@ import envoy.util.EnvoyLog; public class ContactSearchScene { @FXML - private Button backButton; - - @FXML - private Button clearButton; - - @FXML - private Button searchButton; - - @FXML - private Button newGroupButton; - - @FXML - private TextField searchBar; + private ClearableTextField searchBar; @FXML private ListView contactList; @@ -67,7 +58,8 @@ public class ContactSearchScene { @FXML private void initialize() { - contactList.setCellFactory(e -> new ContactListCell()); + contactList.setCellFactory(ContactListCellFactory::new); + searchBar.setClearButtonListener(e -> { searchBar.getTextField().clear(); contactList.getItems().clear(); }); eventBus.register(ContactSearchResult.class, response -> Platform.runLater(() -> { contactList.getItems().clear(); contactList.getItems().addAll(response.get()); })); } @@ -78,20 +70,12 @@ public class ContactSearchScene { * @since Envoy Client v0.1-beta */ @FXML - private void checkClearButton() { - final var containsContent = searchBar.getText().strip().isEmpty(); - clearButton.setDisable(containsContent); - searchButton.setDisable(containsContent); + private void sendRequest() { + final var text = searchBar.getTextField().getText().strip(); + if (!text.isBlank()) eventBus.dispatch(new SendEvent(new ContactSearchRequest(text))); + else contactList.getItems().clear(); } - /** - * Sends a {@link ContactSearchRequest} to the server. - * - * @since Envoy Client v0.1-beta - */ - @FXML - private void suggestContacts() { eventBus.dispatch(new SendEvent(new ContactSearchRequest(searchBar.getText()))); } - /** * Clears the text in the search bar and the items shown in the list. * Additionally disables both clear and search button. @@ -100,10 +84,8 @@ public class ContactSearchScene { */ @FXML private void clear() { - searchBar.setText(null); + searchBar.getTextField().setText(null); contactList.getItems().clear(); - clearButton.setDisable(true); - searchButton.setDisable(true); } /** @@ -119,14 +101,17 @@ public class ContactSearchScene { final var alert = new Alert(AlertType.CONFIRMATION); alert.setTitle("Add Contact to Contact List"); alert.setHeaderText("Add the user " + contact.getName() + " to your contact list?"); - alert.showAndWait().filter(btn -> btn == ButtonType.OK).ifPresent(btn -> { + // 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(contact, 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 " + contact); - }); + })); } } diff --git a/src/main/java/envoy/client/ui/controller/GroupCreationScene.java b/src/main/java/envoy/client/ui/controller/GroupCreationScene.java index c6d7d05..76573b8 100644 --- a/src/main/java/envoy/client/ui/controller/GroupCreationScene.java +++ b/src/main/java/envoy/client/ui/controller/GroupCreationScene.java @@ -9,8 +9,9 @@ import javafx.scene.control.Alert.AlertType; import envoy.client.data.LocalDB; import envoy.client.event.SendEvent; -import envoy.client.ui.ContactListCell; +import envoy.client.ui.ClearableTextField; import envoy.client.ui.SceneContext; +import envoy.client.ui.listcell.ContactListCellFactory; import envoy.data.Contact; import envoy.event.EventBus; import envoy.event.GroupCreation; @@ -30,7 +31,7 @@ public class GroupCreationScene { private Button createButton; @FXML - private TextField groupNameField; + private ClearableTextField groupNameField; @FXML private ListView contactList; @@ -41,8 +42,9 @@ public class GroupCreationScene { @FXML private void initialize() { - contactList.setCellFactory(e -> new ContactListCell()); + contactList.setCellFactory(ContactListCellFactory::new); contactList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + groupNameField.setClearButtonListener(e -> { groupNameField.getTextField().clear(); createButton.setDisable(true); }); } /** @@ -59,11 +61,22 @@ public class GroupCreationScene { /** * Enables the {@code createButton} if at least one contact is selected. - * + * * @since Envoy Client v0.1-beta */ @FXML - private void contactListClicked() { createButton.setDisable(contactList.getSelectionModel().isEmpty()); } + private void contactListClicked() { + createButton.setDisable(contactList.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. @@ -74,10 +87,10 @@ public class GroupCreationScene { */ @FXML private void createButtonClicked() { - final var name = groupNameField.getText(); + 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.clear(); + groupNameField.getTextField().clear(); } else { eventBus.dispatch(new SendEvent(new GroupCreation(name, contactList.getSelectionModel().getSelectedItems().stream().map(Contact::getID).collect(Collectors.toSet())))); diff --git a/src/main/java/envoy/client/ui/controller/LoginScene.java b/src/main/java/envoy/client/ui/controller/LoginScene.java index f3dec05..c59dc4b 100644 --- a/src/main/java/envoy/client/ui/controller/LoginScene.java +++ b/src/main/java/envoy/client/ui/controller/LoginScene.java @@ -13,6 +13,7 @@ import javafx.scene.control.Alert.AlertType; import envoy.client.data.*; import envoy.client.net.Client; +import envoy.client.ui.ClearableTextField; import envoy.client.ui.SceneContext; import envoy.client.ui.Startup; import envoy.data.*; @@ -34,7 +35,7 @@ import envoy.util.EnvoyLog; public final class LoginScene { @FXML - private TextField userTextField; + private ClearableTextField userTextField; @FXML private PasswordField passwordField; @@ -119,17 +120,17 @@ public final class LoginScene { 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.getText())) { + } else if (!Bounds.isValidContactName(userTextField.getTextField().getText())) { new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait(); - userTextField.clear(); - } else - performHandshake( - new LoginCredentials(userTextField.getText(), passwordField.getText().toCharArray(), registerCheckBox.isSelected(), Startup.VERSION)); + userTextField.getTextField().clear(); + } else performHandshake(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText().toCharArray(), + registerCheckBox.isSelected(), Startup.VERSION)); } @FXML private void offlineModeButtonPressed() { - attemptOfflineMode(new LoginCredentials(userTextField.getText(), passwordField.getText().toCharArray(), false, Startup.VERSION)); + attemptOfflineMode( + new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText().toCharArray(), false, Startup.VERSION)); } @FXML @@ -162,8 +163,7 @@ public final class LoginScene { loadChatScene(); } } catch (IOException | InterruptedException | TimeoutException e) { - logger.log(Level.WARNING, "Could not connect to server: ", e); - logger.log(Level.FINER, "Attempting offline mode..."); + logger.log(Level.INFO, "Could not connect to server. Entering offline mode..."); attemptOfflineMode(credentials); } } diff --git a/src/main/java/envoy/client/ui/listcell/ContactControl.java b/src/main/java/envoy/client/ui/listcell/ContactControl.java new file mode 100644 index 0000000..2a802fc --- /dev/null +++ b/src/main/java/envoy/client/ui/listcell/ContactControl.java @@ -0,0 +1,40 @@ +package envoy.client.ui.listcell; + +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; + +import envoy.data.Contact; +import envoy.data.Group; +import envoy.data.User; + +/** + * This class formats a single {@link Contact} into a UI component. + *

+ * Project: envoy-client
+ * File: ContactControl.java
+ * Created: 01.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.1-beta + */ +public class ContactControl extends VBox { + + /** + * @param contact the contact that should be formatted + * @since Envoy Client v0.1-beta + */ + public ContactControl(Contact contact) { + // Container with contact name + final var nameLabel = new Label(contact.getName()); + nameLabel.setWrapText(true); + getChildren().add(nameLabel); + if (contact instanceof User) { + // Online status + final var user = (User) contact; + final var statusLabel = new Label(user.getStatus().toString()); + statusLabel.getStyleClass().add(user.getStatus().toString().toLowerCase()); + getChildren().add(statusLabel); + } else // Member count + getChildren().add(new Label(((Group) contact).getContacts().size() + " members")); + } +} diff --git a/src/main/java/envoy/client/ui/listcell/ContactListCellFactory.java b/src/main/java/envoy/client/ui/listcell/ContactListCellFactory.java new file mode 100644 index 0000000..8f8d3f0 --- /dev/null +++ b/src/main/java/envoy/client/ui/listcell/ContactListCellFactory.java @@ -0,0 +1,44 @@ +package envoy.client.ui.listcell; + +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; + +import envoy.data.Contact; + +/** + * Project: envoy-client
+ * File: UserListCell.java
+ * Created: 28.03.2020
+ * + * @author Kai S. K. Engelbart + * @since Envoy Client v0.1-beta + */ +public class ContactListCellFactory extends ListCell { + + private final ListView listView; + + /** + * @param listView the list view inside which this cell is contained + * @since Envoy Client v0.1-beta + */ + public ContactListCellFactory(ListView listView) { this.listView = listView; } + + /** + * Displays the name of a contact. If the contact is a user, their online status + * is displayed as well. + * + * @since Envoy Client v0.1-beta + */ + @Override + protected void updateItem(Contact contact, boolean empty) { + super.updateItem(contact, empty); + if (empty || contact == null) { + setText(null); + setGraphic(null); + } else { + final var control = new ContactControl(contact); + prefWidthProperty().bind(listView.widthProperty().subtract(40)); + setGraphic(control); + } + } +} diff --git a/src/main/java/envoy/client/ui/listcell/MessageControl.java b/src/main/java/envoy/client/ui/listcell/MessageControl.java new file mode 100644 index 0000000..d68acdd --- /dev/null +++ b/src/main/java/envoy/client/ui/listcell/MessageControl.java @@ -0,0 +1,60 @@ +package envoy.client.ui.listcell; + +import java.time.format.DateTimeFormatter; +import java.util.Map; + +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; + +import envoy.client.ui.IconUtil; +import envoy.data.Message; +import envoy.data.Message.MessageStatus; +import envoy.data.User; + +/** + * This class formats a single {@link Message} into a UI component. + *

+ * Project: envoy-client
+ * File: MessageControl.java
+ * Created: 01.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.1-beta + */ +public class MessageControl extends VBox { + + private static User client; + private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); + private static final Map statusImages = IconUtil.loadByEnum(MessageStatus.class, 16); + + /** + * + * @param message the message that should be formatted + * @since Envoy Client v0.1-beta + */ + public MessageControl(Message message) { + // Creating the underlying VBox, the dateLabel and the textLabel + super(new Label(dateFormat.format(message.getCreationDate()))); + final var textLabel = new Label(message.getText()); + textLabel.setWrapText(true); + getChildren().add(textLabel); + // Setting the message status icon and background color + if (message.getRecipientID() != client.getID()) { + final var statusIcon = new ImageView(statusImages.get(message.getStatus())); + statusIcon.setPreserveRatio(true); + getChildren().add(statusIcon); + getStyleClass().add("own-message"); + } else getStyleClass().add("received-message"); + // Adjusting height and weight of the cell to the corresponding ListView + paddingProperty().setValue(new Insets(5, 20, 5, 20)); + } + + /** + * @param client the user who has logged in + * @since Envoy Client v0.1-beta + */ + public static void setUser(User client) { MessageControl.client = client; } +} diff --git a/src/main/java/envoy/client/ui/listcell/MessageListCellFactory.java b/src/main/java/envoy/client/ui/listcell/MessageListCellFactory.java new file mode 100644 index 0000000..f8e4fa2 --- /dev/null +++ b/src/main/java/envoy/client/ui/listcell/MessageListCellFactory.java @@ -0,0 +1,52 @@ +package envoy.client.ui.listcell; + +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.Tooltip; +import javafx.stage.PopupWindow.AnchorLocation; + +import envoy.data.Message; + +/** + * Displays a single message inside the message list. + *

+ * Project: envoy-client
+ * File: MessageListCellFactory.java
+ * Created: 28.03.2020
+ * + * @author Kai S. K. Engelbart + * @since Envoy Client v0.1-beta + */ +public class MessageListCellFactory extends ListCell { + + private final ListView listView; + + /** + * @param listView the list view inside which this cell is contained + * @since Envoy Client v0.1-beta + */ + public MessageListCellFactory(ListView listView) { this.listView = listView; } + + /** + * Displays the text, the data of creation and the status of a message. + * + * @since Envoy v0.1-beta + */ + @Override + protected void updateItem(Message message, boolean empty) { + super.updateItem(message, empty); + if (empty || message == null) { + setText(null); + setGraphic(null); + } else { + final var control = new MessageControl(message); + control.prefWidthProperty().bind(listView.widthProperty().subtract(40)); + // Creating the Tooltip to deselect a message + final var tooltip = new Tooltip("You can select a message by clicking on it \nand deselect it by pressing \"ctrl\" and clicking on it"); + tooltip.setWrapText(true); + tooltip.setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT); + setTooltip(tooltip); + setGraphic(control); + } + } +} diff --git a/src/main/java/envoy/client/ui/listcell/package-info.java b/src/main/java/envoy/client/ui/listcell/package-info.java new file mode 100644 index 0000000..ab0788e --- /dev/null +++ b/src/main/java/envoy/client/ui/listcell/package-info.java @@ -0,0 +1,12 @@ +/** + * This package contains custom list cells that are used to display certain + * things. + *

+ * Project: envoy-client
+ * File: package-info.java
+ * Created: 30.06.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.1-beta + */ +package envoy.client.ui.listcell; diff --git a/src/main/other/CustomComponents.jar b/src/main/other/CustomComponents.jar new file mode 100644 index 0000000..083f070 Binary files /dev/null and b/src/main/other/CustomComponents.jar differ diff --git a/src/main/resources/css/base.css b/src/main/resources/css/base.css index f9f1ce2..9230b0f 100644 --- a/src/main/resources/css/base.css +++ b/src/main/resources/css/base.css @@ -3,7 +3,7 @@ } .context-menu, .context-menu > * { - -fx-background-radius: 15px; + -fx-background-radius: 15.0px; /*TODO: solution below does not work */ -fx-background-color: transparent; } @@ -58,3 +58,19 @@ -fx-text-fill: #00FF00; -fx-background-color: transparent; } + +#infoLabel-success { + -fx-text-fill: #00FF00; +} + +#infoLabel-info { + -fx-text-fill: yellow; +} + +#infoLabel-warning { + -fx-text-fill: orange; +} + +#infoLabel-error { + -fx-text-fill: red; +} diff --git a/src/main/resources/fxml/ChatScene.fxml b/src/main/resources/fxml/ChatScene.fxml index fa1a4ef..8ed1185 100644 --- a/src/main/resources/fxml/ChatScene.fxml +++ b/src/main/resources/fxml/ChatScene.fxml @@ -12,9 +12,9 @@ - @@ -47,7 +47,7 @@ prefHeight="211.0" prefWidth="300.0" GridPane.rowIndex="1" GridPane.rowSpan="2147483647"> - + @@ -64,7 +64,7 @@