Merge pull request #172 from informatik-ag-ngl/f/voice_messages
Voice Messaging
This commit is contained in:
commit
da78d1cbb7
@ -245,7 +245,7 @@ org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_label=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
|
||||
@ -439,7 +439,7 @@ org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_if_empty
|
||||
org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=true
|
||||
org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
|
||||
org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never
|
||||
org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never
|
||||
org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_if_empty
|
||||
org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_if_single_item
|
||||
org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
|
||||
org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_always
|
||||
@ -450,7 +450,7 @@ org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false
|
||||
org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=true
|
||||
org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false
|
||||
org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=true
|
||||
org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never
|
||||
org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_if_empty
|
||||
org.eclipse.jdt.core.formatter.lineSplit=150
|
||||
org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
|
||||
org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
|
||||
|
64
src/main/java/envoy/client/data/audio/AudioPlayer.java
Normal file
64
src/main/java/envoy/client/data/audio/AudioPlayer.java
Normal file
@ -0,0 +1,64 @@
|
||||
package envoy.client.data.audio;
|
||||
|
||||
import javax.sound.sampled.*;
|
||||
|
||||
import envoy.exception.EnvoyException;
|
||||
|
||||
/**
|
||||
* Plays back audio from a byte array.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>AudioPlayer.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class AudioPlayer {
|
||||
|
||||
private final AudioFormat format;
|
||||
private final DataLine.Info info;
|
||||
|
||||
private Clip clip;
|
||||
|
||||
/**
|
||||
* Initializes the player with the default audio format.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioPlayer() { this(AudioRecorder.DEFAULT_AUDIO_FORMAT); }
|
||||
|
||||
/**
|
||||
* Initializes the player with a given audio format.
|
||||
*
|
||||
* @param format the audio format to use
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioPlayer(AudioFormat format) {
|
||||
this.format = format;
|
||||
info = new DataLine.Info(Clip.class, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if audio play back is supported
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isSupported() { return AudioSystem.isLineSupported(info); }
|
||||
|
||||
/**
|
||||
* Plays back an audio clip.
|
||||
*
|
||||
* @param data the data of the clip
|
||||
* @throws EnvoyException if the play back failed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void play(byte[] data) throws EnvoyException {
|
||||
try {
|
||||
clip = (Clip) AudioSystem.getLine(info);
|
||||
clip.open(format, data, 0, data.length);
|
||||
clip.start();
|
||||
} catch (final LineUnavailableException e) {
|
||||
throw new EnvoyException("Cannot play back audio", e);
|
||||
}
|
||||
}
|
||||
}
|
122
src/main/java/envoy/client/data/audio/AudioRecorder.java
Normal file
122
src/main/java/envoy/client/data/audio/AudioRecorder.java
Normal file
@ -0,0 +1,122 @@
|
||||
package envoy.client.data.audio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.sound.sampled.*;
|
||||
|
||||
import envoy.exception.EnvoyException;
|
||||
|
||||
/**
|
||||
* Records audio and exports it as a byte array.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>AudioRecorder.java</strong><br>
|
||||
* Created: <strong>02.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class AudioRecorder {
|
||||
|
||||
/**
|
||||
* The default audio format used for recording and play back.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
|
||||
|
||||
private final AudioFormat format;
|
||||
private final DataLine.Info info;
|
||||
|
||||
private TargetDataLine line;
|
||||
private Path tempFile;
|
||||
|
||||
/**
|
||||
* Initializes the recorder with the default audio format.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioRecorder() { this(DEFAULT_AUDIO_FORMAT); }
|
||||
|
||||
/**
|
||||
* Initializes the recorder with a given audio format.
|
||||
*
|
||||
* @param format the audio format to use
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioRecorder(AudioFormat format) {
|
||||
this.format = format;
|
||||
info = new DataLine.Info(TargetDataLine.class, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if audio recording is supported
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isSupported() { return AudioSystem.isLineSupported(info); }
|
||||
|
||||
/**
|
||||
* @return {@code true} if the recorder is active
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isRecording() { return line != null && line.isActive(); }
|
||||
|
||||
/**
|
||||
* Starts the audio recording.
|
||||
*
|
||||
* @throws EnvoyException if starting the recording failed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void start() throws EnvoyException {
|
||||
try {
|
||||
|
||||
// Open the line
|
||||
line = (TargetDataLine) AudioSystem.getLine(info);
|
||||
line.open(format);
|
||||
line.start();
|
||||
|
||||
// Prepare temp file
|
||||
tempFile = Files.createTempFile("recording", "wav");
|
||||
|
||||
// Start the recording
|
||||
final var ais = new AudioInputStream(line);
|
||||
AudioSystem.write(ais, AudioFileFormat.Type.WAVE, tempFile.toFile());
|
||||
} catch (IOException | LineUnavailableException e) {
|
||||
throw new EnvoyException("Cannot record voice", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the recording.
|
||||
*
|
||||
* @return the finished recording
|
||||
* @throws EnvoyException if finishing the recording failed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public byte[] finish() throws EnvoyException {
|
||||
try {
|
||||
line.stop();
|
||||
line.close();
|
||||
final byte[] data = Files.readAllBytes(tempFile);
|
||||
Files.delete(tempFile);
|
||||
return data;
|
||||
} catch (final IOException e) {
|
||||
throw new EnvoyException("Cannot save voice recording", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the active recording.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void cancel() {
|
||||
line.stop();
|
||||
line.close();
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
11
src/main/java/envoy/client/data/audio/package-info.java
Normal file
11
src/main/java/envoy/client/data/audio/package-info.java
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Contains classes related to recording and playing back audio clips.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>package-info.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
package envoy.client.data.audio;
|
@ -51,16 +51,25 @@ public class Receiver extends Thread {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
try {
|
||||
// Read object length
|
||||
final byte[] lenBytes = new byte[4];
|
||||
in.read(lenBytes);
|
||||
final int len = SerializationUtils.bytesToInt(lenBytes, 0);
|
||||
logger.log(Level.FINEST, "Expecting object of length " + len + ".");
|
||||
|
||||
// Read object into byte array
|
||||
final byte[] objBytes = new byte[len];
|
||||
in.read(objBytes);
|
||||
final int bytesRead = in.read(objBytes);
|
||||
logger.log(Level.FINEST, "Read " + bytesRead + " bytes.");
|
||||
|
||||
// Catch LV encoding errors
|
||||
if (len != bytesRead) {
|
||||
logger.log(Level.WARNING,
|
||||
String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead));
|
||||
continue;
|
||||
}
|
||||
|
||||
try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
|
||||
final Object obj = oin.readObject();
|
||||
@ -69,18 +78,18 @@ public class Receiver extends Thread {
|
||||
// Get appropriate processor
|
||||
@SuppressWarnings("rawtypes")
|
||||
final Consumer processor = processors.get(obj.getClass());
|
||||
if (processor == null)
|
||||
logger.log(Level.WARNING, String.format(
|
||||
"The received object has the class %s for which no processor is defined.", obj.getClass()));
|
||||
if (processor == null) logger.log(Level.WARNING,
|
||||
String.format("The received object has the class %s for which no processor is defined.", obj.getClass()));
|
||||
else processor.accept(obj);
|
||||
}
|
||||
}
|
||||
} catch (final SocketException e) {
|
||||
// Connection probably closed by client.
|
||||
return;
|
||||
} catch (final Exception e) {
|
||||
logger.log(Level.SEVERE, "Error on receiver thread", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an object processor to this {@link Receiver}. It will be called once an
|
||||
|
49
src/main/java/envoy/client/ui/AudioControl.java
Normal file
49
src/main/java/envoy/client/ui/AudioControl.java
Normal file
@ -0,0 +1,49 @@
|
||||
package envoy.client.ui;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
import envoy.client.data.audio.AudioPlayer;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Enables the play back of audio clips through a button.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>AudioControl.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class AudioControl extends HBox {
|
||||
|
||||
private AudioPlayer player = new AudioPlayer();
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(AudioControl.class);
|
||||
|
||||
/**
|
||||
* Initializes the audio control.
|
||||
*
|
||||
* @param audioData the audio data to play.
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioControl(byte[] audioData) {
|
||||
var button = new Button("Play");
|
||||
button.setOnAction(e -> {
|
||||
try {
|
||||
player.play(audioData);
|
||||
} catch (EnvoyException ex) {
|
||||
logger.log(Level.SEVERE, "Could not play back audio: ", ex);
|
||||
new Alert(AlertType.ERROR, "Could not play back audio").showAndWait();
|
||||
}
|
||||
});
|
||||
getChildren().add(button);
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ import javafx.scene.paint.Color;
|
||||
import envoy.client.data.Chat;
|
||||
import envoy.client.data.LocalDB;
|
||||
import envoy.client.data.Settings;
|
||||
import envoy.client.data.audio.AudioRecorder;
|
||||
import envoy.client.event.MessageCreationEvent;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.net.WriteProxy;
|
||||
@ -30,10 +31,12 @@ import envoy.client.ui.listcell.ContactListCellFactory;
|
||||
import envoy.client.ui.listcell.MessageControl;
|
||||
import envoy.client.ui.listcell.MessageListCellFactory;
|
||||
import envoy.data.*;
|
||||
import envoy.data.Attachment.AttachmentType;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.event.MessageStatusChange;
|
||||
import envoy.event.UserStatusChange;
|
||||
import envoy.event.contact.ContactOperation;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
@ -58,6 +61,9 @@ public final class ChatScene implements Restorable {
|
||||
@FXML
|
||||
private Button postButton;
|
||||
|
||||
@FXML
|
||||
private Button voiceButton;
|
||||
|
||||
@FXML
|
||||
private Button settingsButton;
|
||||
|
||||
@ -78,9 +84,11 @@ public final class ChatScene implements Restorable {
|
||||
private WriteProxy writeProxy;
|
||||
private SceneContext sceneContext;
|
||||
|
||||
private boolean postingPermanentlyDisabled = false;
|
||||
|
||||
private Chat currentChat;
|
||||
private AudioRecorder recorder;
|
||||
private boolean recording;
|
||||
private Attachment pendingAttachment;
|
||||
private boolean postingPermanentlyDisabled = false;
|
||||
|
||||
private static final Settings settings = Settings.getInstance();
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
@ -172,6 +180,8 @@ public final class ChatScene implements Restorable {
|
||||
contactLabel.setText(localDB.getUser().getName());
|
||||
MessageControl.setUser(localDB.getUser());
|
||||
if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
|
||||
|
||||
recorder = new AudioRecorder();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -205,11 +215,20 @@ public final class ChatScene implements Restorable {
|
||||
logger.log(Level.WARNING, "Could not read current chat.", e);
|
||||
}
|
||||
|
||||
// Discard the pending attachment
|
||||
if (recorder.isRecording()) {
|
||||
recorder.cancel();
|
||||
recording = false;
|
||||
voiceButton.setText("Record Voice Message");
|
||||
}
|
||||
pendingAttachment = null;
|
||||
|
||||
remainingChars.setVisible(true);
|
||||
remainingChars
|
||||
.setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
|
||||
}
|
||||
messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled);
|
||||
voiceButton.setDisable(!recorder.isSupported());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -234,6 +253,26 @@ public final class ChatScene implements Restorable {
|
||||
sceneContext.<ContactSearchScene>getController().initializeData(sceneContext, localDB);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void voiceButtonClicked() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
if (!recording) {
|
||||
recording = true;
|
||||
Platform.runLater(() -> voiceButton.setText("Recording..."));
|
||||
recorder.start();
|
||||
} else {
|
||||
pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE);
|
||||
recording = false;
|
||||
Platform.runLater(() -> { voiceButton.setText("Record Voice Message"); checkPostConditions(false); });
|
||||
}
|
||||
} catch (EnvoyException e) {
|
||||
logger.log(Level.SEVERE, "Could not record audio: ", e);
|
||||
Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the text length of the {@code messageTextArea}, adjusts the
|
||||
* {@code remainingChars} label and checks whether to send the message
|
||||
@ -256,11 +295,14 @@ public final class ChatScene implements Restorable {
|
||||
*/
|
||||
@FXML
|
||||
private void checkPostConditions(KeyEvent e) {
|
||||
checkPostConditions(settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
|
||||
|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown());
|
||||
}
|
||||
|
||||
private void checkPostConditions(boolean sendKeyPressed) {
|
||||
if (!postingPermanentlyDisabled) {
|
||||
if (!postButton.isDisabled() && (settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
|
||||
|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown()))
|
||||
postMessage();
|
||||
postButton.setDisable(messageTextArea.getText().isBlank() || currentChat == null);
|
||||
if (!postButton.isDisabled() && sendKeyPressed) postMessage();
|
||||
postButton.setDisable((messageTextArea.getText().isBlank() && pendingAttachment == null) || currentChat == null);
|
||||
} else {
|
||||
final var noMoreMessaging = "Go online to send messages";
|
||||
if (!infoLabel.getText().equals(noMoreMessaging))
|
||||
@ -315,12 +357,14 @@ public final class ChatScene implements Restorable {
|
||||
return;
|
||||
}
|
||||
final var text = messageTextArea.getText().strip();
|
||||
if (text.isBlank()) throw new IllegalArgumentException("A message without visible text can not be sent.");
|
||||
try {
|
||||
// Create and send message
|
||||
final var message = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
|
||||
.setText(text)
|
||||
.build();
|
||||
var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()).setText(text);
|
||||
if (pendingAttachment != null) {
|
||||
builder.setAttachment(pendingAttachment);
|
||||
pendingAttachment = null;
|
||||
}
|
||||
var message = builder.build();
|
||||
|
||||
// Send message
|
||||
writeProxy.writeMessage(message);
|
||||
|
@ -9,6 +9,7 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import envoy.client.ui.AudioControl;
|
||||
import envoy.client.ui.IconUtil;
|
||||
import envoy.data.Message;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
@ -38,6 +39,20 @@ public class MessageControl extends VBox {
|
||||
public MessageControl(Message message) {
|
||||
// Creating the underlying VBox, the dateLabel and the textLabel
|
||||
super(new Label(dateFormat.format(message.getCreationDate())));
|
||||
|
||||
// Handling message attachment display
|
||||
if (message.hasAttachment()) switch (message.getAttachment().getType()) {
|
||||
case PICTURE:
|
||||
break;
|
||||
case VIDEO:
|
||||
break;
|
||||
case VOICE:
|
||||
getChildren().add(new AudioControl(message.getAttachment().getData()));
|
||||
break;
|
||||
case DOCUMENT:
|
||||
break;
|
||||
}
|
||||
|
||||
final var textLabel = new Label(message.getText());
|
||||
textLabel.setWrapText(true);
|
||||
getChildren().add(textLabel);
|
||||
|
@ -1,4 +1,4 @@
|
||||
.button, .list-cell {
|
||||
.button, .list-cell, .progress-bar * {
|
||||
-fx-background-radius: 5.0em;
|
||||
}
|
||||
|
||||
@ -26,6 +26,10 @@
|
||||
-fx-text-fill: transparent;
|
||||
}
|
||||
|
||||
.progress-bar{
|
||||
-fx-progress-color: blue;
|
||||
}
|
||||
|
||||
.online {
|
||||
-fx-text-fill: limegreen;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.ContextMenu?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
@ -18,29 +19,24 @@
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="envoy.client.ui.controller.ChatScene">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES"
|
||||
maxWidth="1.7976931348623157E308" minWidth="10.0" percentWidth="25.0"
|
||||
prefWidth="161.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES"
|
||||
maxWidth="1.7976931348623157E308" minWidth="10.0" percentWidth="65.0"
|
||||
prefWidth="357.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="10.0"
|
||||
minWidth="10.0" percentWidth="10.0" prefWidth="10.0" />
|
||||
<ColumnConstraints hgrow="NEVER" minWidth="60.0"
|
||||
prefWidth="160.0" />
|
||||
<ColumnConstraints hgrow="ALWAYS"
|
||||
maxWidth="1.7976931348623157E308" minWidth="10.0" prefWidth="357.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints maxHeight="-Infinity"
|
||||
minHeight="-Infinity" prefHeight="50.0" vgrow="NEVER" />
|
||||
<RowConstraints maxHeight="-Infinity"
|
||||
minHeight="-Infinity" prefHeight="20.0" vgrow="NEVER" />
|
||||
<RowConstraints maxHeight="1.7976931348623157E308"
|
||||
minHeight="10.0" percentHeight="10.0" prefHeight="70.0"
|
||||
vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="1.7976931348623157E308"
|
||||
minHeight="10.0" percentHeight="7.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="1.7976931348623157E308"
|
||||
minHeight="10.0" percentHeight="60.0" prefHeight="50.0"
|
||||
vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="50.0" minHeight="10.0"
|
||||
percentHeight="2.0" prefHeight="50.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="1.7976931348623157E308"
|
||||
minHeight="10.0" percentHeight="21.0" prefHeight="100.0"
|
||||
vgrow="SOMETIMES" />
|
||||
minHeight="50.0" prefHeight="155.14286150251115" vgrow="ALWAYS" />
|
||||
<RowConstraints maxHeight="-Infinity"
|
||||
minHeight="-Infinity" prefHeight="20.0" vgrow="NEVER" />
|
||||
<RowConstraints maxHeight="120.0" minHeight="40.0"
|
||||
prefHeight="60.0" vgrow="NEVER" />
|
||||
<RowConstraints maxHeight="-Infinity"
|
||||
minHeight="-Infinity" prefHeight="40.0" vgrow="NEVER" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<ListView fx:id="userList" onMouseClicked="#userListClicked"
|
||||
@ -61,8 +57,8 @@
|
||||
</ContextMenu>
|
||||
</contextMenu>
|
||||
</ListView>
|
||||
<Label fx:id="contactLabel" prefHeight="16.0" prefWidth="250.0"
|
||||
text="Select a contact to chat with" GridPane.columnSpan="2">
|
||||
<Label fx:id="contactLabel" prefHeight="27.0" prefWidth="134.0"
|
||||
GridPane.columnSpan="2">
|
||||
<GridPane.margin>
|
||||
<Insets left="10.0" />
|
||||
</GridPane.margin>
|
||||
@ -72,8 +68,8 @@
|
||||
</Label>
|
||||
<Button fx:id="settingsButton" mnemonicParsing="true"
|
||||
onAction="#settingsButtonClicked" text="_Settings"
|
||||
GridPane.columnIndex="1" GridPane.columnSpan="2"
|
||||
GridPane.halignment="RIGHT" GridPane.valignment="CENTER">
|
||||
GridPane.columnIndex="1" GridPane.halignment="RIGHT"
|
||||
GridPane.valignment="CENTER">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
@ -108,14 +104,18 @@
|
||||
</ContextMenu>
|
||||
</contextMenu>
|
||||
</ListView>
|
||||
<Button fx:id="postButton" defaultButton="true" disable="true"
|
||||
mnemonicParsing="true" onAction="#postMessage" prefHeight="10.0"
|
||||
prefWidth="75.0" text="_Post" GridPane.columnIndex="2"
|
||||
GridPane.halignment="CENTER" GridPane.rowIndex="4"
|
||||
<ButtonBar prefWidth="436.0" GridPane.columnIndex="1"
|
||||
GridPane.halignment="CENTER" GridPane.rowIndex="5"
|
||||
GridPane.valignment="BOTTOM">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" right="10.0" />
|
||||
<Insets right="10.0" top="5.0" />
|
||||
</GridPane.margin>
|
||||
<buttons>
|
||||
<Button fx:id="voiceButton" disable="true"
|
||||
onAction="#voiceButtonClicked" text="_Record Voice Message" />
|
||||
<Button fx:id="postButton" defaultButton="true"
|
||||
disable="true" mnemonicParsing="true" onAction="#postMessage"
|
||||
prefHeight="10.0" prefWidth="75.0" text="_Post">
|
||||
<tooltip>
|
||||
<Tooltip anchorLocation="WINDOW_TOP_LEFT" autoHide="true"
|
||||
maxWidth="350.0"
|
||||
@ -134,13 +134,15 @@
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
<TextArea fx:id="messageTextArea" disable="true"
|
||||
onInputMethodTextChanged="#messageTextUpdated"
|
||||
onKeyPressed="#checkPostConditions" onKeyTyped="#checkKeyCombination"
|
||||
prefHeight="200.0" prefWidth="200.0" wrapText="true"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="4">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="5.0" top="3.0" />
|
||||
<Insets bottom="10.0" left="5.0" right="10.0" top="3.0" />
|
||||
</GridPane.margin>
|
||||
<opaqueInsets>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
@ -148,13 +150,13 @@
|
||||
</TextArea>
|
||||
<Button mnemonicParsing="true"
|
||||
onAction="#addContactButtonClicked" text="_Add Contacts"
|
||||
GridPane.halignment="CENTER" GridPane.rowIndex="4"
|
||||
GridPane.halignment="CENTER" GridPane.rowIndex="5"
|
||||
GridPane.valignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
<GridPane.margin>
|
||||
<Insets bottom="5.0" left="10.0" right="5.0" top="5.0" />
|
||||
<Insets bottom="10.0" left="10.0" right="5.0" top="5.0" />
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
<Label id="remainingCharsLabel" fx:id="remainingChars"
|
||||
@ -178,7 +180,7 @@
|
||||
</tooltip>
|
||||
</Label>
|
||||
<Label fx:id="infoLabel" text="Something happened"
|
||||
wrapText="true" textFill="#faa007" visible="false"
|
||||
textFill="#faa007" visible="false" wrapText="true"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="1">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
|
Reference in New Issue
Block a user