diff --git a/client/src/main/java/envoy/client/ui/SceneContext.java b/client/src/main/java/envoy/client/ui/SceneContext.java index a027395..a7ab403 100644 --- a/client/src/main/java/envoy/client/ui/SceneContext.java +++ b/client/src/main/java/envoy/client/ui/SceneContext.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.Stack; import java.util.logging.Level; +import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; @@ -125,8 +126,12 @@ public final class SceneContext { sceneStack.push(scene); stage.setScene(scene); - applyCSS(); + // The LoginScene is the only scene not intended to be resized + // As strange as it seems, this is needed as otherwise the LoginScene won't be + // displayed on some OS (...Debian...) stage.sizeToScene(); + Platform.runLater(() -> stage.setResizable(sceneInfo != SceneInfo.LOGIN_SCENE)); + applyCSS(); stage.show(); } catch (final IOException e) { EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE, String.format("Could not load scene for %s: ", sceneInfo), e); diff --git a/client/src/main/java/envoy/client/ui/controller/ChatScene.java b/client/src/main/java/envoy/client/ui/controller/ChatScene.java index a708dd0..a041b9d 100644 --- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -17,6 +17,8 @@ import java.util.logging.Logger; import javafx.animation.RotateTransition; import javafx.application.Platform; import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.*; @@ -27,6 +29,7 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.GridPane; import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; import javafx.stage.FileChooser; import javafx.util.Duration; @@ -39,9 +42,7 @@ import envoy.client.event.SendEvent; import envoy.client.net.Client; import envoy.client.net.WriteProxy; import envoy.client.ui.*; -import envoy.client.ui.listcell.ChatControl; -import envoy.client.ui.listcell.ListCellFactory; -import envoy.client.ui.listcell.MessageControl; +import envoy.client.ui.listcell.*; import envoy.data.*; import envoy.data.Attachment.AttachmentType; import envoy.event.*; @@ -101,6 +102,24 @@ public final class ChatScene implements Restorable { @FXML private ImageView attachmentView; + @FXML + private Label topBarContactLabel; + + @FXML + private Label topBarStatusLabel; + + @FXML + private Button messageSearchButton; + + @FXML + private ImageView clientProfilePic; + + @FXML + private ImageView recipientProfilePic; + + @FXML + private TextArea contactSearch; + private LocalDB localDB; private Client client; private WriteProxy writeProxy; @@ -122,6 +141,8 @@ public final class ChatScene implements Restorable { private static final int MAX_MESSAGE_LENGTH = 255; private static final int DEFAULT_ICON_SIZE = 16; + private FilteredList chats; + /** * Initializes the appearance of certain visual components. * @@ -129,9 +150,8 @@ public final class ChatScene implements Restorable { */ @FXML private void initialize() { - // Initialize message and user rendering - messageList.setCellFactory(new ListCellFactory<>(MessageControl::new)); + messageList.setCellFactory(MessageListCell::new); chatList.setCellFactory(new ListCellFactory<>(ChatControl::new)); settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE))); @@ -139,6 +159,14 @@ public final class ChatScene implements Restorable { 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)))); + messageSearchButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE))); + clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); + Rectangle clip = new Rectangle(); + clip.setWidth(43); + clip.setHeight(43); + clip.setArcHeight(43); + clip.setArcWidth(43); + clientProfilePic.setClip(clip); // Listen to received messages eventBus.register(MessageCreationEvent.class, e -> { @@ -163,8 +191,9 @@ public final class ChatScene implements Restorable { // Move chat with most recent unread messages to the top Platform.runLater(() -> { - chatList.getItems().remove(chat); - chatList.getItems().add(0, chat); + chats.getSource().remove(chat); + ((ObservableList) chats.getSource()).add(0, chat); + if (chat.equals(currentChat)) chatList.getSelectionModel().select(0); }); }); @@ -187,7 +216,7 @@ public final class ChatScene implements Restorable { // Listen to user status changes eventBus.register(UserStatusChange.class, - e -> chatList.getItems() + e -> chats.getSource() .stream() .filter(c -> c.getRecipient().getID() == e.getID()) .findAny() @@ -201,10 +230,11 @@ public final class ChatScene implements Restorable { case ADD: if (contact instanceof User) localDB.getUsers().put(contact.getName(), (User) contact); final Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact); - Platform.runLater(() -> chatList.getItems().add(chat)); + Platform.runLater(() -> ((ObservableList) chats.getSource()).add(0, chat)); + break; case REMOVE: - Platform.runLater(() -> chatList.getItems().removeIf(c -> c.getRecipient().equals(contact))); + Platform.runLater(() -> chats.getSource().removeIf(c -> c.getRecipient().equals(contact))); break; } }); @@ -242,10 +272,12 @@ public final class ChatScene implements Restorable { this.client = client; this.writeProxy = writeProxy; - MessageControl.setUser(localDB.getUser()); - MessageControl.setSceneContext(sceneContext); - chatList.setItems(FXCollections.observableList(localDB.getChats())); + chats = new FilteredList<>(FXCollections.observableList(localDB.getChats())); + chatList.setItems(chats); contactLabel.setText(localDB.getUser().getName()); + MessageControl.setLocalDB(localDB); + MessageControl.setSceneContext(sceneContext); + if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info"); recorder = new AudioRecorder(); @@ -273,7 +305,7 @@ public final class ChatScene implements Restorable { currentChat = localDB.getChat(user.getID()).get(); messageList.setItems(FXCollections.observableList(currentChat.getMessages())); - final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount() - 1; + final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount(); messageList.scrollTo(scrollIndex); logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex); deleteContactMenuItem.setText("Delete " + user.getName()); @@ -301,6 +333,27 @@ public final class ChatScene implements Restorable { voiceButton.setDisable(!recorder.isSupported()); attachmentButton.setDisable(false); chatList.refresh(); + + if (currentChat != null) { + topBarContactLabel.setText(currentChat.getRecipient().getName()); + if (currentChat.getRecipient() instanceof User) { + String status = ((User) currentChat.getRecipient()).getStatus().toString(); + topBarStatusLabel.setText(status); + topBarStatusLabel.getStyleClass().add(status.toLowerCase()); + recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); + } else { + topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " members"); + recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43)); + } + Rectangle clip = new Rectangle(); + clip.setWidth(43); + clip.setHeight(43); + clip.setArcHeight(43); + clip.setArcWidth(43); + recipientProfilePic.setClip(clip); + + messageSearchButton.setVisible(true); + } } /** @@ -561,8 +614,8 @@ public final class ChatScene implements Restorable { currentChat.insert(message); // Moving currentChat to the top Platform.runLater(() -> { - chatList.getItems().remove(currentChat); - chatList.getItems().add(0, currentChat); + chats.getSource().remove(currentChat); + ((ObservableList) chats.getSource()).add(0, currentChat); chatList.getSelectionModel().select(0); localDB.getChats().remove(currentChat); localDB.getChats().add(0, currentChat); @@ -638,4 +691,10 @@ public final class ChatScene implements Restorable { if (attachmentView.getImage() != null) attachmentView.setVisible(true); pendingAttachment = messageAttachment; } + + @FXML + private void searchContacts() { + chats.setPredicate(contactSearch.getText().isBlank() ? c -> true + : c -> c.getRecipient().getName().toLowerCase().contains(contactSearch.getText().toLowerCase())); + } } diff --git a/client/src/main/java/envoy/client/ui/controller/LoginScene.java b/client/src/main/java/envoy/client/ui/controller/LoginScene.java index 2b03a5b..d3f1956 100644 --- a/client/src/main/java/envoy/client/ui/controller/LoginScene.java +++ b/client/src/main/java/envoy/client/ui/controller/LoginScene.java @@ -9,8 +9,10 @@ import java.util.logging.Logger; import javafx.application.Platform; import javafx.fxml.FXML; +import javafx.geometry.Insets; import javafx.scene.control.*; import javafx.scene.control.Alert.AlertType; +import javafx.scene.image.ImageView; import envoy.client.data.*; import envoy.client.net.Client; @@ -37,7 +39,7 @@ import envoy.util.EnvoyLog; public final class LoginScene { @FXML - private ClearableTextField userTextField; + private TextField userTextField; @FXML private PasswordField passwordField; @@ -46,19 +48,30 @@ public final class LoginScene { private PasswordField repeatPasswordField; @FXML - private Label repeatPasswordLabel; - - @FXML - private CheckBox registerCheckBox; + private Button registerSwitch; @FXML private Label connectionLabel; + @FXML + private Button loginButton; + + @FXML + private Button offlineModeButton; + + @FXML + private Label registerTextLabel; + + @FXML + private ImageView logo; + private Client client; private LocalDB localDB; private CacheMap cacheMap; private SceneContext sceneContext; + private boolean registration = false; + private static final Logger logger = EnvoyLog.getLogger(LoginScene.class); private static final EventBus eventBus = EventBus.getInstance(); private static final ClientConfig config = ClientConfig.getInstance(); @@ -70,6 +83,8 @@ public final class LoginScene { // Show an alert after an unsuccessful handshake eventBus.register(HandshakeRejection.class, e -> Platform.runLater(() -> { new Alert(AlertType.ERROR, e.get()).showAndWait(); })); + + logo.setImage(IconUtil.loadIcon("envoy_logo")); } /** @@ -102,28 +117,41 @@ public final class LoginScene { private void loginButtonPressed() { // Prevent registration with unequal passwords - if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) { + if (registration && !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())) { + } else if (!Bounds.isValidContactName(userTextField.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, loadLastSync(userTextField.getTextField().getText()))); + userTextField.clear(); + } else performHandshake(new LoginCredentials(userTextField.getText(), passwordField.getText(), registration, + Startup.VERSION, loadLastSync(userTextField.getText()))); } @FXML private void offlineModeButtonPressed() { - attemptOfflineMode( - new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), false, Startup.VERSION, localDB.getLastSync())); + attemptOfflineMode(new LoginCredentials(userTextField.getText(), passwordField.getText(), false, Startup.VERSION, + loadLastSync(userTextField.getText()))); } @FXML - private void registerCheckboxChanged() { - + private void registerSwitchPressed() { + if (!registration) { + // case if the current mode is login + loginButton.setText("Register"); + loginButton.setPadding(new Insets(2, 116, 2, 116)); + registerTextLabel.setText("Already an account?"); + registerSwitch.setText("Login"); + } else { + // case if the current mode is registration + loginButton.setText("Login"); + loginButton.setPadding(new Insets(2, 125, 2, 125)); + registerTextLabel.setText("No account yet?"); + registerSwitch.setText("Register"); + } + registration = !registration; // Make repeat password field and label visible / invisible - repeatPasswordField.setVisible(registerCheckBox.isSelected()); - repeatPasswordLabel.setVisible(registerCheckBox.isSelected()); + repeatPasswordField.setVisible(registration); + offlineModeButton.setDisable(registration); } @FXML @@ -206,9 +234,10 @@ public final class LoginScene { // Load ChatScene sceneContext.pop(); sceneContext.getStage().setMinHeight(400); - sceneContext.getStage().setMinWidth(350); + sceneContext.getStage().setMinWidth(843); sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE); sceneContext.getController().initializeData(sceneContext, localDB, client, writeProxy); + sceneContext.getStage().centerOnScreen(); if (StatusTrayIcon.isSupported()) { diff --git a/client/src/main/java/envoy/client/ui/custom/ProfilePicImageView.java b/client/src/main/java/envoy/client/ui/custom/ProfilePicImageView.java new file mode 100644 index 0000000..4708974 --- /dev/null +++ b/client/src/main/java/envoy/client/ui/custom/ProfilePicImageView.java @@ -0,0 +1,61 @@ +package envoy.client.ui.custom; + +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.shape.Rectangle; + +/** + * Provides a set of convenience constructors for images that are displayed as profile pictures. + *

+ * Project: envoy-client
+ * File: ProfilePicImageView.java
+ * Created: 30.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.2-beta + */ +public final class ProfilePicImageView extends ImageView { + + /** + * Creates a new {@code ProfilePicImageView} without a default image. + * + * @since Envoy Client v0.2-beta + */ + public ProfilePicImageView() { this(null); } + + /** + * Creates a new {@code ProfilePicImageView}. + * + * @param image the image to display + * @since Envoy Client v0.2-beta + */ + public ProfilePicImageView(Image image) { this(image, 40); } + + /** + * Creates a new {@code ProfilePicImageView}. + * + * @param image the image to display + * @param sizeAndRounding the size and rounding for a circular + * {@code ProfilePicImageView} + * @since Envoy Client v0.2-beta + */ + public ProfilePicImageView(Image image, double sizeAndRounding) { this(image, sizeAndRounding, sizeAndRounding); } + + /** + * Creates a new {@code ProfilePicImageView}. + * + * @param image the image to display + * @param size the size of this {@code ProfilePicImageView} + * @param rounding how rounded this {@code ProfilePicImageView} should be + * @since Envoy Client v0.2-beta + */ + public ProfilePicImageView(Image image, double size, double rounding) { + super(image); + final var clip = new Rectangle(); + clip.setWidth(size); + clip.setHeight(size); + clip.setArcHeight(rounding); + clip.setArcWidth(rounding); + setClip(clip); + } +} diff --git a/client/src/main/java/envoy/client/ui/custom/package-info.java b/client/src/main/java/envoy/client/ui/custom/package-info.java new file mode 100644 index 0000000..97a8f58 --- /dev/null +++ b/client/src/main/java/envoy/client/ui/custom/package-info.java @@ -0,0 +1,14 @@ +/** + * This package stores custom components for use in JavaFX. + * These components are also expected to be used via FXML. + *

+ * Project: envoy-client
+ * File: package-info.java
+ * Created: 30.07.2020
+ * + * @author Leon Hofmeister + * @author Maximilian Käfer + * @author Kai S. K. Engelbart + * @since Envoy Client v0.2-beta + */ +package envoy.client.ui.custom; diff --git a/client/src/main/java/envoy/client/ui/listcell/AbstractListCell.java b/client/src/main/java/envoy/client/ui/listcell/AbstractListCell.java new file mode 100644 index 0000000..e294b15 --- /dev/null +++ b/client/src/main/java/envoy/client/ui/listcell/AbstractListCell.java @@ -0,0 +1,48 @@ +package envoy.client.ui.listcell; + +import javafx.scene.Node; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; + +/** + * Provides a convenience frame for list cell creation. + *

+ * Project: envoy-client
+ * File: AbstractListCell.java
+ * Created: 18.07.2020
+ * + * @author Kai S. K. Engelbart + * @param the type of element displayed by the list cell + * @param the type of node as which the list element will be displayed + * @since Envoy Client v0.1-beta + */ +public abstract class AbstractListCell extends ListCell { + + protected ListView listView; + + /** + * @param listView the list view inside of which the cell will be displayed + * @since Envoy Client v0.1-beta + */ + public AbstractListCell(ListView listView) { + this.listView = listView; + setContentDisplay(ContentDisplay.GRAPHIC_ONLY); + getStyleClass().add("listElement"); + } + + @Override + protected final void updateItem(T item, boolean empty) { + super.updateItem(item, empty); + setGraphic(empty || item == null ? null : renderItem(item)); + } + + /** + * Converts a list item to a node. This can have side effects on the list cell. + * + * @param item the item to render + * @return a node representing the item + * @since Envoy Client v0.1-beta + */ + protected abstract U renderItem(T item); +} diff --git a/client/src/main/java/envoy/client/ui/listcell/ChatControl.java b/client/src/main/java/envoy/client/ui/listcell/ChatControl.java index e19cd54..8e2a698 100644 --- a/client/src/main/java/envoy/client/ui/listcell/ChatControl.java +++ b/client/src/main/java/envoy/client/ui/listcell/ChatControl.java @@ -1,10 +1,15 @@ package envoy.client.ui.listcell; +import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; +import javafx.scene.image.ImageView; import javafx.scene.layout.*; +import javafx.scene.shape.Rectangle; import envoy.client.data.Chat; +import envoy.client.ui.IconUtil; +import envoy.data.Group; /** * Displays a chat using a contact control for the recipient and a label for the @@ -25,10 +30,27 @@ public class ChatControl extends HBox { * @since Envoy Client v0.1-beta */ public ChatControl(Chat chat) { - + setAlignment(Pos.CENTER_LEFT); + setPadding(new Insets(0, 0, 3, 0)); + // profile pic + ImageView contactProfilePic; + if (chat.getRecipient() instanceof Group) contactProfilePic = new ImageView(IconUtil.loadIconThemeSensitive("group_icon", 32)); + else contactProfilePic = new ImageView(IconUtil.loadIconThemeSensitive("user_icon", 32)); + Rectangle clip = new Rectangle(); + clip.setWidth(32); + clip.setHeight(32); + clip.setArcHeight(32); + clip.setArcWidth(32); + contactProfilePic.setClip(clip); + getChildren().add(contactProfilePic); + // spacing + Region leftSpacing = new Region(); + leftSpacing.setPrefSize(8, 0); + leftSpacing.setMinSize(8, 0); + leftSpacing.setMaxSize(8, 0); + getChildren().add(leftSpacing); // Contact control getChildren().add(new ContactControl(chat.getRecipient())); - // Unread messages if (chat.getUnreadAmount() != 0) { final var spacing = new Region(); @@ -43,5 +65,6 @@ public class ChatControl extends HBox { vBox2.getChildren().add(unreadMessagesLabel); getChildren().add(vBox2); } + getStyleClass().add("listElement"); } } diff --git a/client/src/main/java/envoy/client/ui/listcell/ContactControl.java b/client/src/main/java/envoy/client/ui/listcell/ContactControl.java index c8f9107..eb4a483 100644 --- a/client/src/main/java/envoy/client/ui/listcell/ContactControl.java +++ b/client/src/main/java/envoy/client/ui/listcell/ContactControl.java @@ -39,5 +39,6 @@ public class ContactControl extends VBox { } else { getChildren().add(new Label(contact.getContacts().size() + " members")); } + getStyleClass().add("listElement"); } } diff --git a/client/src/main/java/envoy/client/ui/listcell/GenericListCell.java b/client/src/main/java/envoy/client/ui/listcell/GenericListCell.java new file mode 100644 index 0000000..ad2b364 --- /dev/null +++ b/client/src/main/java/envoy/client/ui/listcell/GenericListCell.java @@ -0,0 +1,36 @@ +package envoy.client.ui.listcell; + +import java.util.function.Function; + +import javafx.scene.Node; +import javafx.scene.control.ListView; + +/** + * A generic list cell rendering an item using a provided render function. + *

+ * Project: envoy-client
+ * File: GenericListCell.java
+ * Created: 18.07.2020
+ * + * @author Kai S. K. Engelbart + * @param the type of element displayed by the list cell + * @param the type of node as which the list element will be displayed + * @since Envoy Client v0.2-beta + */ +public final class GenericListCell extends AbstractListCell { + + private Function renderer; + + /** + * @param listView the list view inside of which the cell will be displayed + * @param renderer a function converting a list item to a node + * @since Envoy Client v0.1-beta + */ + public GenericListCell(ListView listView, Function renderer) { + super(listView); + this.renderer = renderer; + } + + @Override + protected U renderItem(T item) { return renderer.apply(item); } +} diff --git a/client/src/main/java/envoy/client/ui/listcell/ListCellFactory.java b/client/src/main/java/envoy/client/ui/listcell/ListCellFactory.java index 86270ce..018a0ba 100644 --- a/client/src/main/java/envoy/client/ui/listcell/ListCellFactory.java +++ b/client/src/main/java/envoy/client/ui/listcell/ListCellFactory.java @@ -17,38 +17,19 @@ import javafx.util.Callback; * * @author Kai S. K. Engelbart * @param the type of object to display + * @param the type of node displayed * @since Envoy Client v0.1-beta */ -public final class ListCellFactory implements Callback, ListCell> { +public final class ListCellFactory implements Callback, ListCell> { - private final class GenericListCell extends ListCell { - - private ListView listView; - - private GenericListCell(ListView listView) { this.listView = listView; } - - @Override - protected void updateItem(T item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { - setText(null); - setGraphic(null); - } else { - final var control = converter.apply(item); - prefWidthProperty().bind(listView.widthProperty().subtract(40)); - setGraphic(control); - } - } - } - - private final Function converter; + private final Function renderer; /** - * @param converter a function converting the type to display into a node + * @param renderer a function converting the type to display into a node * @since Envoy Client v0.1-beta */ - public ListCellFactory(Function converter) { this.converter = converter; } + public ListCellFactory(Function renderer) { this.renderer = renderer; } @Override - public ListCell call(ListView listView) { return new GenericListCell(listView); } + public ListCell call(ListView listView) { return new GenericListCell<>(listView, renderer); } } diff --git a/client/src/main/java/envoy/client/ui/listcell/MessageControl.java b/client/src/main/java/envoy/client/ui/listcell/MessageControl.java index eec8612..9b66c6c 100644 --- a/client/src/main/java/envoy/client/ui/listcell/MessageControl.java +++ b/client/src/main/java/envoy/client/ui/listcell/MessageControl.java @@ -10,18 +10,22 @@ import java.util.logging.Level; import java.util.logging.Logger; import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.layout.VBox; +import javafx.scene.layout.*; import javafx.stage.FileChooser; +import envoy.client.data.LocalDB; import envoy.client.data.Settings; import envoy.client.ui.AudioControl; import envoy.client.ui.IconUtil; import envoy.client.ui.SceneContext; + +import envoy.data.GroupMessage; import envoy.data.Message; import envoy.data.Message.MessageStatus; import envoy.data.User; @@ -35,11 +39,14 @@ import envoy.util.EnvoyLog; * Created: 01.07.2020
* * @author Leon Hofmeister + * @author Maximilian Käfer * @since Envoy Client v0.1-beta */ public class MessageControl extends Label { - private static User client; + private boolean ownMessage; + + private static LocalDB localDB; private static SceneContext sceneContext; @@ -56,15 +63,31 @@ public class MessageControl extends Label { */ public MessageControl(Message message) { // Creating the underlying VBox and the dateLabel - final var vbox = new VBox(new Label(dateFormat.format(message.getCreationDate()))); + final var hbox = new HBox(); + if (message.getSenderID() != localDB.getUser().getID() && message instanceof GroupMessage) { + // Displaying the name of the sender in a group + final var label = new Label(); + label.getStyleClass().add("groupMemberNames"); + label.setText(localDB.getUsers() + .values() + .stream() + .filter(c -> c.getID() == message.getSenderID()) + .findFirst() + .map(User::getName) + .orElse("Unknown User")); + label.setPadding(new Insets(0, 5, 0, 0)); + hbox.getChildren().add(label); + } + hbox.getChildren().add(new Label(dateFormat.format(message.getCreationDate()))); + final var vbox = new VBox(hbox); // Creating the actions for the MenuItems - final ContextMenu contextMenu = new ContextMenu(); - final MenuItem copyMenuItem = new MenuItem("Copy"); - final MenuItem deleteMenuItem = new MenuItem("Delete"); - final MenuItem forwardMenuItem = new MenuItem("Forward"); - final MenuItem quoteMenuItem = new MenuItem("Quote"); - final MenuItem infoMenuItem = new MenuItem("Info"); + final var contextMenu = new ContextMenu(); + final var copyMenuItem = new MenuItem("Copy"); + final var deleteMenuItem = new MenuItem("Delete"); + final var forwardMenuItem = new MenuItem("Forward"); + final var quoteMenuItem = new MenuItem("Quote"); + final var infoMenuItem = new MenuItem("Info"); copyMenuItem.setOnAction(e -> copyMessage(message)); deleteMenuItem.setOnAction(e -> deleteMessage(message)); forwardMenuItem.setOnAction(e -> forwardMessage(message)); @@ -93,15 +116,27 @@ public class MessageControl extends Label { } // Creating the textLabel final var textLabel = new Label(message.getText()); + textLabel.setMaxWidth(430); textLabel.setWrapText(true); - vbox.getChildren().add(textLabel); + final var hBoxBottom = new HBox(); + hBoxBottom.getChildren().add(textLabel); // Setting the message status icon and background color - if (message.getSenderID() == client.getID()) { + if (message.getSenderID() == localDB.getUser().getID()) { final var statusIcon = new ImageView(statusImages.get(message.getStatus())); statusIcon.setPreserveRatio(true); - vbox.getChildren().add(statusIcon); + final var space = new Region(); + HBox.setHgrow(space, Priority.ALWAYS); + hBoxBottom.getChildren().add(space); + hBoxBottom.getChildren().add(statusIcon); + hBoxBottom.setAlignment(Pos.BOTTOM_RIGHT); getStyleClass().add("own-message"); - } else getStyleClass().add("received-message"); + ownMessage = true; + hbox.setAlignment(Pos.CENTER_RIGHT); + } else { + getStyleClass().add("received-message"); + ownMessage = false; + } + vbox.getChildren().add(hBoxBottom); // Adjusting height and weight of the cell to the corresponding ListView paddingProperty().setValue(new Insets(5, 20, 5, 20)); setContextMenu(contextMenu); @@ -144,10 +179,17 @@ public class MessageControl extends Label { } /** - * @param client the user who has logged in + * @param localDB the localDB used by the current user + * @since Envoy Client v0.2-beta + */ + public static void setLocalDB(LocalDB localDB) { MessageControl.localDB = localDB; } + + /** + * @return whether the message stored by this {@code MessageControl} has been + * sent by this user of Envoy * @since Envoy Client v0.1-beta */ - public static void setUser(User client) { MessageControl.client = client; } + public boolean isOwnMessage() { return ownMessage; } /** * @param sceneContext the scene context storing the stage used in Envoy diff --git a/client/src/main/java/envoy/client/ui/listcell/MessageListCell.java b/client/src/main/java/envoy/client/ui/listcell/MessageListCell.java new file mode 100644 index 0000000..80cf215 --- /dev/null +++ b/client/src/main/java/envoy/client/ui/listcell/MessageListCell.java @@ -0,0 +1,41 @@ +package envoy.client.ui.listcell; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.ListView; + +import envoy.data.Message; + +/** + * A list cell containing messages represented as message controls. + *

+ * Project: envoy-client
+ * File: MessageListCell.java
+ * Created: 18.07.2020
+ * + * @author Kai S. K. Engelbart + * @since Envoy Client v0.1-beta + */ +public final class MessageListCell extends AbstractListCell { + + /** + * @param listView the list view inside of which the cell will be displayed + * @since Envoy Client v0.1-beta + */ + public MessageListCell(ListView listView) { super(listView); } + + @Override + protected MessageControl renderItem(Message message) { + final var control = new MessageControl(message); + listView.widthProperty().addListener((observable, oldValue, newValue) -> adjustPadding(newValue.intValue(), control.isOwnMessage())); + adjustPadding((int) listView.getWidth(), control.isOwnMessage()); + if (control.isOwnMessage()) setAlignment(Pos.CENTER_RIGHT); + else setAlignment(Pos.CENTER_LEFT); + return control; + } + + private void adjustPadding(int listWidth, boolean ownMessage) { + int padding = 10 + Math.max((listWidth - 1000) / 2, 0); + setPadding(ownMessage ? new Insets(0, padding, 6, 0) : new Insets(0, 0, 6, padding)); + } +} diff --git a/client/src/main/java/module-info.java b/client/src/main/java/module-info.java index 1f0d0b1..2b39906 100644 --- a/client/src/main/java/module-info.java +++ b/client/src/main/java/module-info.java @@ -20,4 +20,5 @@ module envoy { opens envoy.client.ui to javafx.graphics, javafx.fxml; opens envoy.client.ui.controller to javafx.graphics, javafx.fxml; + opens envoy.client.ui.custom to javafx.graphics, javafx.fxml; } diff --git a/client/src/main/resources/css/base.css b/client/src/main/resources/css/base.css index 78fff9e..4c289bb 100644 --- a/client/src/main/resources/css/base.css +++ b/client/src/main/resources/css/base.css @@ -1,5 +1,5 @@ .button, .list-cell, .progress-bar * { - -fx-background-radius: 5.0em; + -fx-background-radius: 0.3em; } .context-menu, .context-menu > * { @@ -8,6 +8,28 @@ -fx-background-color: transparent; } +#textEnterContainer, #contact-search-enter-container { + -fx-background-radius: 5.0em; +} + +#roundButton { + -fx-background-radius: 5.0em; +} + +.text-area { + -fx-background-color: transparent; +} + +.text-area .scroll-pane { + -fx-background-color: transparent; +} +.text-area .scroll-pane .viewport{ + -fx-background-color: transparent; +} +.text-area .scroll-pane .content{ + -fx-background-color: transparent; +} + .menu-item { -fx-background-radius: 15.0px; } @@ -48,13 +70,13 @@ .received-message { -fx-alignment: center-left; - -fx-background-radius: 4.0em; + -fx-background-radius: 1.3em; -fx-text-alignment: right; } .own-message { -fx-alignment: center-right; - -fx-background-radius: 4.0em; + -fx-background-radius: 1.3em; -fx-text-alignment: left; } @@ -65,6 +87,21 @@ -fx-text-alignment: center; } +#loginButton { + -fx-background-radius: 1.0em; +} + +#registerSwitch { + -fx-background-color: transparent; + -fx-text-fill: orange; +} + +#loginInputField { + -fx-background-color: transparent; + -fx-border: solid; + -fx-border-width: 0.0 0.0 1.0 0.0; +} + #remainingCharsLabel { -fx-text-fill: #00FF00; -fx-background-color: transparent; @@ -85,3 +122,15 @@ #infoLabel-error { -fx-text-fill: red; } + +#transparentBackground { + -fx-background-color: transparent; +} + +#profilePic { + -fx-radius: 1.0em; +} + +.listElement { + -fx-background-color: transparent; +} diff --git a/client/src/main/resources/css/dark.css b/client/src/main/resources/css/dark.css index eaad3a9..ca98909 100644 --- a/client/src/main/resources/css/dark.css +++ b/client/src/main/resources/css/dark.css @@ -18,8 +18,8 @@ -fx-background-color: lightgray; } -.list-view, .list-cell, .text-area .content, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item { - -fx-background-color: dimgray; +#messageList, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item { + -fx-background-color: #222222; } .list-cell:selected, .list-cell:selected > *, .menu-item:hover { @@ -37,3 +37,48 @@ .alert.information.dialog-pane, .alert.warning.dialog-pane, .alert.error.dialog-pane { -fx-background-color: black; } + +#loginInputField { + -fx-border-color: white; +} + +#loginBackground { + -fx-background-color: rgb(25, 25, 25); +} + +#chatList, #topBar, #search-panel { + -fx-background-color: #303030; +} + +#textEnterContainer { + -fx-background-color: #363636; +} + +#contact-search-enter-container { + -fx-background-color: #202020; +} + +#underline { + -fx-border: solid; + -fx-border-width: 0.0 0.0 1.0 0.0; + -fx-border-color: #202020; +} + +.groupMemberNames { + -fx-text-fill: rgb(105.0,0.0,153.0); + -fx-font-weight: bold; +} + +.scroll-bar:vertical, .scroll-bar:vertical .track, .scroll-bar:vertical .increment-button , .scroll-bar:vertical .decrement-button { + -fx-background-color: transparent; +} + +.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow { + -fx-background-color: transparent; +} + +.scroll-bar:vertical .thumb { + -fx-background-color: #707070; + -fx-background-insets : 4.0, 0.0, 0.0; + -fx-background-radius : 2.0em; +} diff --git a/client/src/main/resources/css/light.css b/client/src/main/resources/css/light.css index 066de55..8486dcb 100644 --- a/client/src/main/resources/css/light.css +++ b/client/src/main/resources/css/light.css @@ -14,3 +14,7 @@ .own-message { -fx-background-color: lightgreen; } + +#loginInputField { + -fx-border-color: black; +} diff --git a/client/src/main/resources/fxml/ChatScene.fxml b/client/src/main/resources/fxml/ChatScene.fxml index 11c4c3e..8fb2fe2 100644 --- a/client/src/main/resources/fxml/ChatScene.fxml +++ b/client/src/main/resources/fxml/ChatScene.fxml @@ -3,7 +3,6 @@ - @@ -13,192 +12,214 @@ + + + + - + - - - + + - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -206,5 +227,33 @@ + + + + + + + + + + + + + + + + + + + diff --git a/client/src/main/resources/fxml/LoginScene.fxml b/client/src/main/resources/fxml/LoginScene.fxml index 1e915a2..9a6d7d9 100644 --- a/client/src/main/resources/fxml/LoginScene.fxml +++ b/client/src/main/resources/fxml/LoginScene.fxml @@ -1,94 +1,76 @@ - - - + + + + - + -

- -
- - - - - - - diff --git a/client/src/main/resources/icons/dark/group_icon.png b/client/src/main/resources/icons/dark/group_icon.png new file mode 100644 index 0000000..5b1660e Binary files /dev/null and b/client/src/main/resources/icons/dark/group_icon.png differ diff --git a/client/src/main/resources/icons/dark/user_icon.png b/client/src/main/resources/icons/dark/user_icon.png new file mode 100644 index 0000000..6d7b8b0 Binary files /dev/null and b/client/src/main/resources/icons/dark/user_icon.png differ diff --git a/client/src/main/resources/icons/light/group_icon.png b/client/src/main/resources/icons/light/group_icon.png new file mode 100644 index 0000000..5b1660e Binary files /dev/null and b/client/src/main/resources/icons/light/group_icon.png differ diff --git a/client/src/main/resources/icons/light/user_icon.png b/client/src/main/resources/icons/light/user_icon.png new file mode 100644 index 0000000..6d7b8b0 Binary files /dev/null and b/client/src/main/resources/icons/light/user_icon.png differ diff --git a/common/src/main/java/envoy/util/Bounds.java b/common/src/main/java/envoy/util/Bounds.java index 315e047..7540b88 100644 --- a/common/src/main/java/envoy/util/Bounds.java +++ b/common/src/main/java/envoy/util/Bounds.java @@ -23,6 +23,8 @@ public class Bounds { */ public static final Pattern CONTACT_NAME_PATTERN = Pattern.compile("^\\w[a-zA-Z0-9-]{2,15}$"); + // KAI: Trust of Chain - das berühmte Konzept aus der Kryptographie + /** * @param contactName the contact name to validate * @return {@code true} if the given contact name is valid