Merge pull request #172 from informatik-ag-ngl/f/voice_messages
Voice Messaging
This commit is contained in:
		@@ -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_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_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_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_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_closing_brace_in_array_initializer=do not insert
 | 
				
			||||||
org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=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_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_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_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_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_imple_if_on_one_line=false
 | 
				
			||||||
org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_always
 | 
					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_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_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_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.lineSplit=150
 | 
				
			||||||
org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
 | 
					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
 | 
					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
 | 
						@Override
 | 
				
			||||||
	public void run() {
 | 
						public void run() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		try {
 | 
							while (true) {
 | 
				
			||||||
			while (true) {
 | 
								try {
 | 
				
			||||||
				// Read object length
 | 
									// Read object length
 | 
				
			||||||
				final byte[] lenBytes = new byte[4];
 | 
									final byte[] lenBytes = new byte[4];
 | 
				
			||||||
				in.read(lenBytes);
 | 
									in.read(lenBytes);
 | 
				
			||||||
				final int len = SerializationUtils.bytesToInt(lenBytes, 0);
 | 
									final int len = SerializationUtils.bytesToInt(lenBytes, 0);
 | 
				
			||||||
 | 
									logger.log(Level.FINEST, "Expecting object of length " + len + ".");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// Read object into byte array
 | 
									// Read object into byte array
 | 
				
			||||||
				final byte[] objBytes = new byte[len];
 | 
									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))) {
 | 
									try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
 | 
				
			||||||
					final Object obj = oin.readObject();
 | 
										final Object obj = oin.readObject();
 | 
				
			||||||
@@ -69,16 +78,16 @@ public class Receiver extends Thread {
 | 
				
			|||||||
					// Get appropriate processor
 | 
										// Get appropriate processor
 | 
				
			||||||
					@SuppressWarnings("rawtypes")
 | 
										@SuppressWarnings("rawtypes")
 | 
				
			||||||
					final Consumer processor = processors.get(obj.getClass());
 | 
										final Consumer processor = processors.get(obj.getClass());
 | 
				
			||||||
					if (processor == null)
 | 
										if (processor == null) logger.log(Level.WARNING,
 | 
				
			||||||
						logger.log(Level.WARNING, String.format(
 | 
												String.format("The received object has the class %s for which no processor is defined.", obj.getClass()));
 | 
				
			||||||
							"The received object has the class %s for which no processor is defined.", obj.getClass()));
 | 
					 | 
				
			||||||
					else processor.accept(obj);
 | 
										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);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} catch (final SocketException e) {
 | 
					 | 
				
			||||||
			// Connection probably closed by client.
 | 
					 | 
				
			||||||
		} catch (final Exception e) {
 | 
					 | 
				
			||||||
			logger.log(Level.SEVERE, "Error on receiver thread", e);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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.Chat;
 | 
				
			||||||
import envoy.client.data.LocalDB;
 | 
					import envoy.client.data.LocalDB;
 | 
				
			||||||
import envoy.client.data.Settings;
 | 
					import envoy.client.data.Settings;
 | 
				
			||||||
 | 
					import envoy.client.data.audio.AudioRecorder;
 | 
				
			||||||
import envoy.client.event.MessageCreationEvent;
 | 
					import envoy.client.event.MessageCreationEvent;
 | 
				
			||||||
import envoy.client.net.Client;
 | 
					import envoy.client.net.Client;
 | 
				
			||||||
import envoy.client.net.WriteProxy;
 | 
					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.MessageControl;
 | 
				
			||||||
import envoy.client.ui.listcell.MessageListCellFactory;
 | 
					import envoy.client.ui.listcell.MessageListCellFactory;
 | 
				
			||||||
import envoy.data.*;
 | 
					import envoy.data.*;
 | 
				
			||||||
 | 
					import envoy.data.Attachment.AttachmentType;
 | 
				
			||||||
import envoy.event.EventBus;
 | 
					import envoy.event.EventBus;
 | 
				
			||||||
import envoy.event.MessageStatusChange;
 | 
					import envoy.event.MessageStatusChange;
 | 
				
			||||||
import envoy.event.UserStatusChange;
 | 
					import envoy.event.UserStatusChange;
 | 
				
			||||||
import envoy.event.contact.ContactOperation;
 | 
					import envoy.event.contact.ContactOperation;
 | 
				
			||||||
 | 
					import envoy.exception.EnvoyException;
 | 
				
			||||||
import envoy.util.EnvoyLog;
 | 
					import envoy.util.EnvoyLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@@ -58,6 +61,9 @@ public final class ChatScene implements Restorable {
 | 
				
			|||||||
	@FXML
 | 
						@FXML
 | 
				
			||||||
	private Button postButton;
 | 
						private Button postButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@FXML
 | 
				
			||||||
 | 
						private Button voiceButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@FXML
 | 
						@FXML
 | 
				
			||||||
	private Button settingsButton;
 | 
						private Button settingsButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -78,9 +84,11 @@ public final class ChatScene implements Restorable {
 | 
				
			|||||||
	private WriteProxy		writeProxy;
 | 
						private WriteProxy		writeProxy;
 | 
				
			||||||
	private SceneContext	sceneContext;
 | 
						private SceneContext	sceneContext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private boolean postingPermanentlyDisabled = false;
 | 
						private Chat			currentChat;
 | 
				
			||||||
 | 
						private AudioRecorder	recorder;
 | 
				
			||||||
	private Chat currentChat;
 | 
						private boolean			recording;
 | 
				
			||||||
 | 
						private Attachment		pendingAttachment;
 | 
				
			||||||
 | 
						private boolean			postingPermanentlyDisabled	= false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	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();
 | 
				
			||||||
@@ -172,6 +180,8 @@ public final class ChatScene implements Restorable {
 | 
				
			|||||||
		contactLabel.setText(localDB.getUser().getName());
 | 
							contactLabel.setText(localDB.getUser().getName());
 | 
				
			||||||
		MessageControl.setUser(localDB.getUser());
 | 
							MessageControl.setUser(localDB.getUser());
 | 
				
			||||||
		if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
 | 
							if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							recorder = new AudioRecorder();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
@@ -205,11 +215,20 @@ public final class ChatScene implements Restorable {
 | 
				
			|||||||
				logger.log(Level.WARNING, "Could not read current chat.", e);
 | 
									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.setVisible(true);
 | 
				
			||||||
			remainingChars
 | 
								remainingChars
 | 
				
			||||||
				.setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
 | 
									.setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled);
 | 
							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);
 | 
							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
 | 
						 * 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
 | 
				
			||||||
@@ -256,11 +295,14 @@ public final class ChatScene implements Restorable {
 | 
				
			|||||||
	 */
 | 
						 */
 | 
				
			||||||
	@FXML
 | 
						@FXML
 | 
				
			||||||
	private void checkPostConditions(KeyEvent e) {
 | 
						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 (!postingPermanentlyDisabled) {
 | 
				
			||||||
			if (!postButton.isDisabled() && (settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
 | 
								if (!postButton.isDisabled() && sendKeyPressed) postMessage();
 | 
				
			||||||
					|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown()))
 | 
								postButton.setDisable((messageTextArea.getText().isBlank() && pendingAttachment == null) || currentChat == null);
 | 
				
			||||||
				postMessage();
 | 
					 | 
				
			||||||
			postButton.setDisable(messageTextArea.getText().isBlank() || currentChat == null);
 | 
					 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			final var noMoreMessaging = "Go online to send messages";
 | 
								final var noMoreMessaging = "Go online to send messages";
 | 
				
			||||||
			if (!infoLabel.getText().equals(noMoreMessaging))
 | 
								if (!infoLabel.getText().equals(noMoreMessaging))
 | 
				
			||||||
@@ -315,12 +357,14 @@ public final class ChatScene implements Restorable {
 | 
				
			|||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		final var text = messageTextArea.getText().strip();
 | 
							final var text = messageTextArea.getText().strip();
 | 
				
			||||||
		if (text.isBlank()) throw new IllegalArgumentException("A message without visible text can not be sent.");
 | 
					 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			// Create and send message
 | 
								// Create and send message
 | 
				
			||||||
			final var message = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
 | 
								var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()).setText(text);
 | 
				
			||||||
				.setText(text)
 | 
								if (pendingAttachment != null) {
 | 
				
			||||||
				.build();
 | 
									builder.setAttachment(pendingAttachment);
 | 
				
			||||||
 | 
									pendingAttachment = null;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								var message = builder.build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Send message
 | 
								// Send message
 | 
				
			||||||
			writeProxy.writeMessage(message);
 | 
								writeProxy.writeMessage(message);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import javafx.scene.image.Image;
 | 
				
			|||||||
import javafx.scene.image.ImageView;
 | 
					import javafx.scene.image.ImageView;
 | 
				
			||||||
import javafx.scene.layout.VBox;
 | 
					import javafx.scene.layout.VBox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import envoy.client.ui.AudioControl;
 | 
				
			||||||
import envoy.client.ui.IconUtil;
 | 
					import envoy.client.ui.IconUtil;
 | 
				
			||||||
import envoy.data.Message;
 | 
					import envoy.data.Message;
 | 
				
			||||||
import envoy.data.Message.MessageStatus;
 | 
					import envoy.data.Message.MessageStatus;
 | 
				
			||||||
@@ -38,6 +39,20 @@ public class MessageControl extends VBox {
 | 
				
			|||||||
	public MessageControl(Message message) {
 | 
						public MessageControl(Message message) {
 | 
				
			||||||
		// Creating the underlying VBox, the dateLabel and the textLabel
 | 
							// Creating the underlying VBox, the dateLabel and the textLabel
 | 
				
			||||||
		super(new Label(dateFormat.format(message.getCreationDate())));
 | 
							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());
 | 
							final var textLabel = new Label(message.getText());
 | 
				
			||||||
		textLabel.setWrapText(true);
 | 
							textLabel.setWrapText(true);
 | 
				
			||||||
		getChildren().add(textLabel);
 | 
							getChildren().add(textLabel);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
.button, .list-cell {
 | 
					.button, .list-cell, .progress-bar * {
 | 
				
			||||||
	-fx-background-radius: 5.0em;
 | 
						-fx-background-radius: 5.0em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -26,6 +26,10 @@
 | 
				
			|||||||
	-fx-text-fill: transparent;
 | 
						-fx-text-fill: transparent;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.progress-bar{
 | 
				
			||||||
 | 
						-fx-progress-color: blue;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.online {
 | 
					.online {
 | 
				
			||||||
	-fx-text-fill: limegreen;
 | 
						-fx-text-fill: limegreen;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<?import javafx.geometry.Insets?>
 | 
					<?import javafx.geometry.Insets?>
 | 
				
			||||||
<?import javafx.scene.control.Button?>
 | 
					<?import javafx.scene.control.Button?>
 | 
				
			||||||
 | 
					<?import javafx.scene.control.ButtonBar?>
 | 
				
			||||||
<?import javafx.scene.control.ContextMenu?>
 | 
					<?import javafx.scene.control.ContextMenu?>
 | 
				
			||||||
<?import javafx.scene.control.Label?>
 | 
					<?import javafx.scene.control.Label?>
 | 
				
			||||||
<?import javafx.scene.control.ListView?>
 | 
					<?import javafx.scene.control.ListView?>
 | 
				
			||||||
@@ -18,29 +19,24 @@
 | 
				
			|||||||
	xmlns:fx="http://javafx.com/fxml/1"
 | 
						xmlns:fx="http://javafx.com/fxml/1"
 | 
				
			||||||
	fx:controller="envoy.client.ui.controller.ChatScene">
 | 
						fx:controller="envoy.client.ui.controller.ChatScene">
 | 
				
			||||||
	<columnConstraints>
 | 
						<columnConstraints>
 | 
				
			||||||
		<ColumnConstraints hgrow="SOMETIMES"
 | 
							<ColumnConstraints hgrow="NEVER" minWidth="60.0"
 | 
				
			||||||
			maxWidth="1.7976931348623157E308" minWidth="10.0" percentWidth="25.0"
 | 
								prefWidth="160.0" />
 | 
				
			||||||
			prefWidth="161.0" />
 | 
							<ColumnConstraints hgrow="ALWAYS"
 | 
				
			||||||
		<ColumnConstraints hgrow="SOMETIMES"
 | 
								maxWidth="1.7976931348623157E308" minWidth="10.0" prefWidth="357.0" />
 | 
				
			||||||
			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>
 | 
						</columnConstraints>
 | 
				
			||||||
	<rowConstraints>
 | 
						<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"
 | 
							<RowConstraints maxHeight="1.7976931348623157E308"
 | 
				
			||||||
			minHeight="10.0" percentHeight="10.0" prefHeight="70.0"
 | 
								minHeight="50.0" prefHeight="155.14286150251115" vgrow="ALWAYS" />
 | 
				
			||||||
			vgrow="SOMETIMES" />
 | 
							<RowConstraints maxHeight="-Infinity"
 | 
				
			||||||
		<RowConstraints maxHeight="1.7976931348623157E308"
 | 
								minHeight="-Infinity" prefHeight="20.0" vgrow="NEVER" />
 | 
				
			||||||
			minHeight="10.0" percentHeight="7.0" vgrow="SOMETIMES" />
 | 
							<RowConstraints maxHeight="120.0" minHeight="40.0"
 | 
				
			||||||
		<RowConstraints maxHeight="1.7976931348623157E308"
 | 
								prefHeight="60.0" vgrow="NEVER" />
 | 
				
			||||||
			minHeight="10.0" percentHeight="60.0" prefHeight="50.0"
 | 
							<RowConstraints maxHeight="-Infinity"
 | 
				
			||||||
			vgrow="SOMETIMES" />
 | 
								minHeight="-Infinity" prefHeight="40.0" vgrow="NEVER" />
 | 
				
			||||||
		<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" />
 | 
					 | 
				
			||||||
	</rowConstraints>
 | 
						</rowConstraints>
 | 
				
			||||||
	<children>
 | 
						<children>
 | 
				
			||||||
		<ListView fx:id="userList" onMouseClicked="#userListClicked"
 | 
							<ListView fx:id="userList" onMouseClicked="#userListClicked"
 | 
				
			||||||
@@ -61,8 +57,8 @@
 | 
				
			|||||||
				</ContextMenu>
 | 
									</ContextMenu>
 | 
				
			||||||
			</contextMenu>
 | 
								</contextMenu>
 | 
				
			||||||
		</ListView>
 | 
							</ListView>
 | 
				
			||||||
		<Label fx:id="contactLabel" prefHeight="16.0" prefWidth="250.0"
 | 
							<Label fx:id="contactLabel" prefHeight="27.0" prefWidth="134.0"
 | 
				
			||||||
			text="Select a contact to chat with" GridPane.columnSpan="2">
 | 
								GridPane.columnSpan="2">
 | 
				
			||||||
			<GridPane.margin>
 | 
								<GridPane.margin>
 | 
				
			||||||
				<Insets left="10.0" />
 | 
									<Insets left="10.0" />
 | 
				
			||||||
			</GridPane.margin>
 | 
								</GridPane.margin>
 | 
				
			||||||
@@ -72,8 +68,8 @@
 | 
				
			|||||||
		</Label>
 | 
							</Label>
 | 
				
			||||||
		<Button fx:id="settingsButton" mnemonicParsing="true"
 | 
							<Button fx:id="settingsButton" mnemonicParsing="true"
 | 
				
			||||||
			onAction="#settingsButtonClicked" text="_Settings"
 | 
								onAction="#settingsButtonClicked" text="_Settings"
 | 
				
			||||||
			GridPane.columnIndex="1" GridPane.columnSpan="2"
 | 
								GridPane.columnIndex="1" GridPane.halignment="RIGHT"
 | 
				
			||||||
			GridPane.halignment="RIGHT" GridPane.valignment="CENTER">
 | 
								GridPane.valignment="CENTER">
 | 
				
			||||||
			<GridPane.margin>
 | 
								<GridPane.margin>
 | 
				
			||||||
				<Insets bottom="10.0" right="10.0" top="10.0" />
 | 
									<Insets bottom="10.0" right="10.0" top="10.0" />
 | 
				
			||||||
			</GridPane.margin>
 | 
								</GridPane.margin>
 | 
				
			||||||
@@ -108,39 +104,45 @@
 | 
				
			|||||||
				</ContextMenu>
 | 
									</ContextMenu>
 | 
				
			||||||
			</contextMenu>
 | 
								</contextMenu>
 | 
				
			||||||
		</ListView>
 | 
							</ListView>
 | 
				
			||||||
		<Button fx:id="postButton" defaultButton="true" disable="true"
 | 
							<ButtonBar prefWidth="436.0" GridPane.columnIndex="1"
 | 
				
			||||||
			mnemonicParsing="true" onAction="#postMessage" prefHeight="10.0"
 | 
								GridPane.halignment="CENTER" GridPane.rowIndex="5"
 | 
				
			||||||
			prefWidth="75.0" text="_Post" GridPane.columnIndex="2"
 | 
					 | 
				
			||||||
			GridPane.halignment="CENTER" GridPane.rowIndex="4"
 | 
					 | 
				
			||||||
			GridPane.valignment="BOTTOM">
 | 
								GridPane.valignment="BOTTOM">
 | 
				
			||||||
			<GridPane.margin>
 | 
								<GridPane.margin>
 | 
				
			||||||
				<Insets bottom="10.0" right="10.0" />
 | 
									<Insets right="10.0" top="5.0" />
 | 
				
			||||||
			</GridPane.margin>
 | 
								</GridPane.margin>
 | 
				
			||||||
			<tooltip>
 | 
								<buttons>
 | 
				
			||||||
				<Tooltip anchorLocation="WINDOW_TOP_LEFT" autoHide="true"
 | 
									<Button fx:id="voiceButton" disable="true"
 | 
				
			||||||
					maxWidth="350.0"
 | 
										onAction="#voiceButtonClicked" text="_Record Voice Message" />
 | 
				
			||||||
					text="Click this button to send the message. If it is disabled, you first have to select a contact to send it to. A message may automatically be sent when you press (Ctrl + ) Enter, according to your preferences. Additionally sends a message when pressing "Alt" + "P"."
 | 
									<Button fx:id="postButton" defaultButton="true"
 | 
				
			||||||
					wrapText="true" />
 | 
										disable="true" mnemonicParsing="true" onAction="#postMessage"
 | 
				
			||||||
			</tooltip>
 | 
										prefHeight="10.0" prefWidth="75.0" text="_Post">
 | 
				
			||||||
			<contextMenu>
 | 
										<tooltip>
 | 
				
			||||||
				<ContextMenu anchorLocation="CONTENT_TOP_LEFT">
 | 
											<Tooltip anchorLocation="WINDOW_TOP_LEFT" autoHide="true"
 | 
				
			||||||
					<items>
 | 
												maxWidth="350.0"
 | 
				
			||||||
						<MenuItem mnemonicParsing="false"
 | 
												text="Click this button to send the message. If it is disabled, you first have to select a contact to send it to. A message may automatically be sent when you press (Ctrl + ) Enter, according to your preferences. Additionally sends a message when pressing "Alt" + "P"."
 | 
				
			||||||
							onAction="#copyAndPostMessage" text="Copy and Send" />
 | 
												wrapText="true" />
 | 
				
			||||||
					</items>
 | 
										</tooltip>
 | 
				
			||||||
				</ContextMenu>
 | 
										<contextMenu>
 | 
				
			||||||
			</contextMenu>
 | 
											<ContextMenu anchorLocation="CONTENT_TOP_LEFT">
 | 
				
			||||||
			<padding>
 | 
												<items>
 | 
				
			||||||
				<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
													<MenuItem mnemonicParsing="false"
 | 
				
			||||||
			</padding>
 | 
														onAction="#copyAndPostMessage" text="Copy and Send" />
 | 
				
			||||||
		</Button>
 | 
												</items>
 | 
				
			||||||
 | 
											</ContextMenu>
 | 
				
			||||||
 | 
										</contextMenu>
 | 
				
			||||||
 | 
										<padding>
 | 
				
			||||||
 | 
											<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
				
			||||||
 | 
										</padding>
 | 
				
			||||||
 | 
									</Button>
 | 
				
			||||||
 | 
								</buttons>
 | 
				
			||||||
 | 
							</ButtonBar>
 | 
				
			||||||
		<TextArea fx:id="messageTextArea" disable="true"
 | 
							<TextArea fx:id="messageTextArea" disable="true"
 | 
				
			||||||
			onInputMethodTextChanged="#messageTextUpdated"
 | 
								onInputMethodTextChanged="#messageTextUpdated"
 | 
				
			||||||
			onKeyPressed="#checkPostConditions" onKeyTyped="#checkKeyCombination"
 | 
								onKeyPressed="#checkPostConditions" onKeyTyped="#checkKeyCombination"
 | 
				
			||||||
			prefHeight="200.0" prefWidth="200.0" wrapText="true"
 | 
								prefHeight="200.0" prefWidth="200.0" wrapText="true"
 | 
				
			||||||
			GridPane.columnIndex="1" GridPane.rowIndex="4">
 | 
								GridPane.columnIndex="1" GridPane.rowIndex="4">
 | 
				
			||||||
			<GridPane.margin>
 | 
								<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>
 | 
								</GridPane.margin>
 | 
				
			||||||
			<opaqueInsets>
 | 
								<opaqueInsets>
 | 
				
			||||||
				<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" />
 | 
				
			||||||
@@ -148,13 +150,13 @@
 | 
				
			|||||||
		</TextArea>
 | 
							</TextArea>
 | 
				
			||||||
		<Button mnemonicParsing="true"
 | 
							<Button mnemonicParsing="true"
 | 
				
			||||||
			onAction="#addContactButtonClicked" text="_Add Contacts"
 | 
								onAction="#addContactButtonClicked" text="_Add Contacts"
 | 
				
			||||||
			GridPane.halignment="CENTER" GridPane.rowIndex="4"
 | 
								GridPane.halignment="CENTER" GridPane.rowIndex="5"
 | 
				
			||||||
			GridPane.valignment="CENTER">
 | 
								GridPane.valignment="CENTER">
 | 
				
			||||||
			<padding>
 | 
								<padding>
 | 
				
			||||||
				<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>
 | 
				
			||||||
			<GridPane.margin>
 | 
								<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>
 | 
								</GridPane.margin>
 | 
				
			||||||
		</Button>
 | 
							</Button>
 | 
				
			||||||
		<Label id="remainingCharsLabel" fx:id="remainingChars"
 | 
							<Label id="remainingCharsLabel" fx:id="remainingChars"
 | 
				
			||||||
@@ -178,7 +180,7 @@
 | 
				
			|||||||
			</tooltip>
 | 
								</tooltip>
 | 
				
			||||||
		</Label>
 | 
							</Label>
 | 
				
			||||||
		<Label fx:id="infoLabel" text="Something happened"
 | 
							<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.columnIndex="1" GridPane.rowIndex="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" />
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user