Merge pull request #183 from informatik-ag-ngl/f/pictures

Add attachment creation support, display picture messages
This commit is contained in:
Kai S. K. Engelbart 2020-07-11 08:01:23 +00:00 committed by GitHub
commit 408ac53777
5 changed files with 106 additions and 28 deletions

View File

@ -2,7 +2,10 @@ package envoy.client.ui.controller;
import java.awt.Toolkit; import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -12,10 +15,12 @@ import javafx.collections.FXCollections;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder; import envoy.client.data.audio.AudioRecorder;
@ -60,6 +65,9 @@ public final class ChatScene implements Restorable {
@FXML @FXML
private Button voiceButton; private Button voiceButton;
@FXML
private Button attachmentButton;
@FXML @FXML
private Button settingsButton; private Button settingsButton;
@ -87,12 +95,15 @@ public final class ChatScene implements Restorable {
private AudioRecorder recorder; private AudioRecorder recorder;
private boolean recording; private boolean recording;
private Attachment pendingAttachment; private Attachment pendingAttachment;
private boolean postingPermanentlyDisabled = false; private boolean postingPermanentlyDisabled;
private static final Settings settings = Settings.getInstance(); private static final Settings settings = Settings.getInstance();
private static final EventBus eventBus = EventBus.getInstance(); private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class); private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
private static final int MAX_MESSAGE_LENGTH = 255;
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. * Initializes the appearance of certain visual components.
@ -106,8 +117,10 @@ public final class ChatScene implements Restorable {
messageList.setCellFactory(MessageListCellFactory::new); messageList.setCellFactory(MessageListCellFactory::new);
userList.setCellFactory(ContactListCellFactory::new); userList.setCellFactory(ContactListCellFactory::new);
settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", 16))); settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", 20))); 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);
// Listen to received messages // Listen to received messages
eventBus.register(MessageCreationEvent.class, e -> { eventBus.register(MessageCreationEvent.class, e -> {
@ -226,7 +239,7 @@ public final class ChatScene implements Restorable {
recording = false; recording = false;
} }
pendingAttachment = null; pendingAttachment = null;
attachmentView.setVisible(false); updateAttachmentView(false);
remainingChars.setVisible(true); remainingChars.setVisible(true);
remainingChars remainingChars
@ -234,6 +247,7 @@ public final class ChatScene implements Restorable {
} }
messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled); messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled);
voiceButton.setDisable(!recorder.isSupported()); voiceButton.setDisable(!recorder.isSupported());
attachmentButton.setDisable(false);
} }
/** /**
@ -266,18 +280,17 @@ public final class ChatScene implements Restorable {
recording = true; recording = true;
Platform.runLater(() -> { Platform.runLater(() -> {
voiceButton.setText("Recording"); voiceButton.setText("Recording");
voiceButton.setGraphic(new ImageView(IconUtil.loadIcon("microphone_recording", 24))); voiceButton.setGraphic(new ImageView(IconUtil.loadIcon("microphone_recording", DEFAULT_ICON_SIZE)));
}); });
recorder.start(); recorder.start();
} else { } else {
pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE); pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE);
recording = false; recording = false;
Platform.runLater(() -> { Platform.runLater(() -> {
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", 20))); voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
voiceButton.setText(null); voiceButton.setText(null);
checkPostConditions(false); checkPostConditions(false);
attachmentView.setImage(IconUtil.loadIconThemeSensitive("attachment_present", 20)); updateAttachmentView(true);
attachmentView.setVisible(true);
}); });
} }
} catch (final EnvoyException e) { } catch (final EnvoyException e) {
@ -287,6 +300,52 @@ public final class ChatScene implements Restorable {
}).start(); }).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();
}
}
}
/** /**
* Checks the text length of the {@code messageTextArea}, adjusts the * Checks the text length of the {@code messageTextArea}, adjusts the
* {@code remainingChars} label and checks whether to send the message * {@code remainingChars} label and checks whether to send the message
@ -373,16 +432,16 @@ public final class ChatScene implements Restorable {
final var text = messageTextArea.getText().strip(); final var text = messageTextArea.getText().strip();
try { try {
// Creating the message and its metadata // Creating the message and its metadata
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(text); .setText(text);
// Setting an attachment, if present // Setting an attachment, if present
if (pendingAttachment != null) { if (pendingAttachment != null) {
builder.setAttachment(pendingAttachment); builder.setAttachment(pendingAttachment);
pendingAttachment = null; pendingAttachment = null;
attachmentView.setVisible(false); updateAttachmentView(false);
} }
// Building the final message // Building the final message
final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient()) final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())
: builder.build(); : builder.build();
// Send message // Send message
@ -428,8 +487,22 @@ public final class ChatScene implements Restorable {
infoLabel.setVisible(true); 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 // Context menu actions
@FXML @FXML
private void deleteContact() { try {} catch (final NullPointerException e) {} } private void deleteContact() { try {} catch (final NullPointerException e) {} }

View File

@ -2,6 +2,7 @@ package envoy.client.ui.listcell;
import java.awt.Toolkit; import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
import java.io.ByteArrayInputStream;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Map; import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
@ -67,6 +68,7 @@ public class MessageControl extends Label {
if (message.hasAttachment()) { if (message.hasAttachment()) {
switch (message.getAttachment().getType()) { switch (message.getAttachment().getType()) {
case PICTURE: case PICTURE:
vbox.getChildren().add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true)));
break; break;
case VIDEO: case VIDEO:
break; break;
@ -90,9 +92,7 @@ public class MessageControl extends Label {
statusIcon.setPreserveRatio(true); statusIcon.setPreserveRatio(true);
vbox.getChildren().add(statusIcon); vbox.getChildren().add(statusIcon);
getStyleClass().add("own-message"); getStyleClass().add("own-message");
} else { } else getStyleClass().add("received-message");
getStyleClass().add("received-message");
}
// Adjusting height and weight of the cell to the corresponding ListView // Adjusting height and weight of the cell to the corresponding ListView
paddingProperty().setValue(new Insets(5, 20, 5, 20)); paddingProperty().setValue(new Insets(5, 20, 5, 20));
setContextMenu(contextMenu); setContextMenu(contextMenu);

View File

@ -51,7 +51,7 @@
<Insets bottom="5.0" left="10.0" /> <Insets bottom="5.0" left="10.0" />
</GridPane.margin> </GridPane.margin>
<padding> <padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="2.0" top="5.0" />
</padding> </padding>
<contextMenu> <contextMenu>
<ContextMenu anchorLocation="CONTENT_TOP_LEFT"> <ContextMenu anchorLocation="CONTENT_TOP_LEFT">
@ -82,24 +82,29 @@
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding> </padding>
</Button> </Button>
<ListView fx:id="messageList" prefHeight="257.0" <ListView fx:id="messageList" GridPane.columnIndex="1"
prefWidth="465.0" GridPane.columnIndex="1"
GridPane.columnSpan="2147483647" GridPane.rowIndex="1" GridPane.columnSpan="2147483647" GridPane.rowIndex="1"
GridPane.rowSpan="2"> GridPane.rowSpan="2">
<GridPane.margin> <GridPane.margin>
<Insets left="5.0" right="10.0" /> <Insets left="5.0" right="10.0" />
</GridPane.margin> </GridPane.margin>
<padding> <padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="2.0" top="5.0" />
</padding> </padding>
</ListView> </ListView>
<ButtonBar prefWidth="436.0" GridPane.columnIndex="1" <ButtonBar buttonMinWidth="40.0" GridPane.columnIndex="1"
GridPane.columnSpan="2147483647" GridPane.halignment="CENTER" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER"
GridPane.rowIndex="5" GridPane.valignment="BOTTOM"> GridPane.rowIndex="5" GridPane.valignment="BOTTOM">
<GridPane.margin> <GridPane.margin>
<Insets right="10.0" /> <Insets right="10.0" />
</GridPane.margin> </GridPane.margin>
<buttons> <buttons>
<Button fx:id="attachmentButton" disable="true"
mnemonicParsing="false" onAction="#attachmentButtonClicked">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</Button>
<Button fx:id="voiceButton" disable="true" <Button fx:id="voiceButton" disable="true"
onAction="#voiceButtonClicked"> onAction="#voiceButtonClicked">
<padding> <padding>
@ -175,7 +180,7 @@
</Label> </Label>
<Label fx:id="infoLabel" text="Something happened" <Label fx:id="infoLabel" text="Something happened"
textFill="#faa007" visible="false" wrapText="true" textFill="#faa007" visible="false" wrapText="true"
GridPane.columnIndex="1" GridPane.rowIndex="1"> GridPane.columnIndex="1">
<GridPane.margin> <GridPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</GridPane.margin> </GridPane.margin>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB